use blockchain::blocks::loans::UnsignedLoanContractTransaction; use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; use blockchain::common::types::LENDER_FEE; use blockchain::json; use blockchain::records::wallet_registry::resolve_local_input_short_address; use blockchain::wallets::structures::Wallet; use blockchain::File; use blockchain::{create_dir_all, AsyncWriteExt}; use blockchain::{Local, LocalResult, NaiveDate, NaiveTime, TimeZone}; fn pad_to_width(input: &str, width: usize) -> String { // Asset names are fixed-width fields in the loan transaction bytes. let mut result = String::with_capacity(width); let _ = std::fmt::write(&mut result, format_args!("{input: f64 { // Fees are stored as atomic units and displayed as whole-coin decimals for the CLI. value as f64 / 100_000_000.0 } fn normalize_short_address_input(address: &str) -> Result { // Accept local vanity/short input and resolve it into the real short address. resolve_local_input_short_address(address.trim()) } fn parse_start_date(input: &str) -> Result { // Loan start dates are entered as calendar dates and stored as local midnight timestamps. let date = NaiveDate::parse_from_str(input.trim(), "%Y-%m-%d") .map_err(|_| "Please enter the date in YYYY-MM-DD format.".to_string())?; let datetime = date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()); let timestamp = match Local.from_local_datetime(&datetime) { LocalResult::Single(dt) => dt.timestamp(), LocalResult::Ambiguous(dt, _) => dt.timestamp(), LocalResult::None => { return Err("Loan start date could not be resolved in local time.".to_string()); } }; if timestamp < 0 || timestamp > u32::MAX as i64 { return Err("Loan start date is out of range.".to_string()); } Ok(timestamp as u32) } #[tokio::main] async fn main() { // Loan contracts use transaction type 7 and are first signed by the lender. let txtype = 7; // Coin/token and collateral names are padded to fixed-width transaction fields. let loan_coin_name = prompt_visible("What coin or token will you be lending out? ").await; let loan_coin_name = loan_coin_name.trim().to_lowercase(); let loan_coin = pad_to_width(&loan_coin_name, 15); let loan_amount_string = prompt_visible("How many coins or tokens will you be lending? ").await; let loan_amount_f64: f64 = loan_amount_string .trim() .parse() .expect("Please enter a valid amount"); let loan_amount = (loan_amount_f64 * 100_000_000.0).round() as u64; // Payment cadence is stored as a compact one-letter code. let payment_period = prompt_visible("Would you like each payment period to be daily, weekly, or monthly? ") .await; let payment_period = match payment_period.trim().to_lowercase().as_str() { "daily" | "d" => "d".to_string(), "weekly" | "w" => "w".to_string(), "monthly" | "m" => "m".to_string(), _ => { println!("Payment period must be daily, weekly, or monthly."); return; } }; let payment_number_string = prompt_visible("How many payments do you expect until the loan is considered paid? ").await; let payment_number: u8 = payment_number_string .trim() .parse() .expect("Please enter a valid number"); let payment_amount_string = prompt_visible("What should be the amount of each payment? ").await; let payment_amount_f64: f64 = payment_amount_string .trim() .parse() .expect("Please enter a valid amount"); let payment_amount = (payment_amount_f64 * 100_000_000.0).round() as u64; let grace_period_string = prompt_visible("How many payments can be missed before you will claim the collateral? ") .await; let grace_period: u8 = grace_period_string .trim() .parse() .expect("Please enter a valid number"); let max_late_value_string = prompt_visible( "How far behind in value can the borrower be before you will claim the collateral? ", ) .await; let max_late_value_f64: f64 = max_late_value_string .trim() .parse() .expect("Please enter a valid amount"); let max_late_value = (max_late_value_f64 * 100_000_000.0).round() as u64; // Collateral can be a coin, token, or NFT name; NFT collateral uses amount 1. let collateral_name = prompt_visible("What collateral coin, token, or NFT will you be accepting for this loan? ") .await; let collateral_name = collateral_name.trim().to_lowercase(); let collateral = pad_to_width(&collateral_name, 15); let collateral_amount_string = prompt_visible("How many collateral coins or tokens will you be accepting for this loan (enter 1 for NFT collateral)? ").await; let collateral_amount_f64: f64 = collateral_amount_string .trim() .parse() .expect("Please enter a valid amount"); let collateral_amount = (collateral_amount_f64 * 100_000_000.0).round() as u64; let start_date = prompt_visible("What is the date this loan should begin? (YYYY-MM-DD): ").await; let timestamp = match parse_start_date(&start_date) { Ok(timestamp) => timestamp, Err(err) => { println!("{err}"); return; } }; // Resolve the borrower input before writing it into the loan offer. let borrower = prompt_visible("What is the wallet address of the borrower? ").await; let borrower = match normalize_short_address_input(borrower.trim()) { Ok(address) => address, Err(_) => { println!("borrower wallet invalid"); return; } }; let txfee_prompt = format!( "Enter the txfee you will be paying for this transaction (minimum: {:.8})", display_fee(LENDER_FEE) ); let txfee_string = prompt_visible(&format!("{txfee_prompt}: ")).await; let txfee_f64: f64 = txfee_string .trim() .parse() .expect("Please enter a valid fee"); let txfee = (txfee_f64 * 100_000_000.0).round() as u64; // Load the lender wallet that creates the first loan-contract signature. let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", ) .await; let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); return; } }; let private_key = &wallet.saved.private_key; let lender = &wallet.saved.short_address; // The lender must sign with a valid current-network short address. if !Wallet::short_address_validation(lender.trim_matches('"')) { println!("lender wallet invalid: {lender}"); return; } // Build the unsigned loan so both lender and borrower sign the exact same hash. let unsigned_loan = UnsignedLoanContractTransaction::new( txtype, timestamp, &loan_coin, loan_amount, lender.trim_matches('"'), &collateral, collateral_amount, &borrower, &payment_period, payment_number, payment_amount, grace_period, max_late_value, txfee, ) .await; // This first signature is the lender's offer; the borrower adds signature2 later. let (hash, signature1) = match unsigned_loan.hash_and_sign(private_key).await { Ok(value) => value, Err(err) => { eprintln!("Failed to sign loan contract: {err}"); return; } }; // Save the partially signed loan JSON for the borrower to review and sign. let output = json!({ "txtype": txtype, "timestamp": timestamp, "loan_coin": loan_coin, "loan_amount": loan_amount, "lender": lender.trim_matches('"'), "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, }); let output_str = serde_json::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) = create_dir_all(&dir_path).await { eprintln!("Failed to create directory: {e}"); return; } let file_path = format!( "{}/{}.json", dir_path, output["hash"].as_str().unwrap_or_default() ); let mut file = File::create(&file_path) .await .expect("Failed to create file"); if let Err(e) = file.write_all(output_str.as_bytes()).await { eprintln!("Failed to write file: {e}"); return; } println!("transaction: {output_str}"); }