284 lines
9.3 KiB
Rust
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}");
|
|
}
|