Contractless/src/bin/verify_sign_loan_tx.rs

284 lines
9.3 KiB
Rust

use blockchain::blocks::loans::UnsignedLoanContractTransaction;
use blockchain::common::cli_prompts::{
ask_yes_no_question, prompt_hidden_nonempty, prompt_wallet_path,
};
use blockchain::env;
use blockchain::from_str;
use blockchain::fs;
use blockchain::json;
use blockchain::read_to_string;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
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(),
}
}
#[tokio::main]
async fn main() {
// Borrowers use this tool to review a lender-signed loan and add signature2.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./validate_sign_loan_tx <path/to/file.json>");
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<Value, _> = from_str(&contents);
let json = match json {
Ok(json) => json,
Err(_) => {
println!("Error parsing JSON in file: {filename}");
return;
}
};
// Load the borrower wallet before resolving vanity inputs; remote vanity lookup needs a signed handshake.
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let address = &wallet.saved.short_address;
// Extract every field that participates in the hash so the borrower signs the same bytes.
let txtype = 7;
let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32;
let loan_coin = json["loan_coin"].as_str().unwrap_or_default().to_string();
let loan_amount = json["loan_amount"].as_u64().unwrap_or_default();
let lender =
match resolve_wallet_address_input(json["lender"].as_str().unwrap_or_default(), &wallet)
.await
{
Ok(address) => address,
Err(err) => {
println!("Transaction is not valid. Lender address is not valid: {err}");
return;
}
};
let collateral = json["collateral"].as_str().unwrap_or_default().to_string();
let collateral_amount = json["collateral_amount"].as_u64().unwrap_or_default();
let borrower =
match resolve_wallet_address_input(json["borrower"].as_str().unwrap_or_default(), &wallet)
.await
{
Ok(address) => address,
Err(err) => {
println!("Transaction is not valid. Borrower address is not valid: {err}");
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;
}
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}");
}