Contractless/src/verifications/async_funcs/verify_borrower.rs

144 lines
6.5 KiB
Rust
Raw Normal View History

2026-05-24 17:56:57 +00:00
use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction;
use crate::common::types::BORROWER_FEE;
use crate::records::wallet_registry::{
require_canonical_registered_short_address, resolve_pubkey_from_short_address,
};
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
use crate::verifications::async_funcs::checks::balance_check::balance_checkup;
use crate::verifications::async_funcs::checks::mempool_check::memcheck;
use crate::verifications::async_funcs::checks::verify_db::db_hex_verification;
use crate::verifications::async_funcs::total_payments::get_total_payments;
use crate::wallets::structures::Wallet;
use crate::{decode, encode};
impl ContractPaymentTransaction {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
let hash = self.unsigned_contract_payment.hash().await;
let signature = &self.signature;
// Transactions already present in the mempool can short-circuit
// the deeper verification path and reuse their stored signature.
if memcheck(signature, &hash).await {
return Ok(signature.to_string());
}
// Loan payments must come from a valid wallet address and carry a
// valid signature from the payer.
let address = &self.unsigned_contract_payment.address;
if !Wallet::short_address_validation(address) {
return Err("Lender Wallet Address is Invalid.").map_err(|s| s.to_string())?;
}
require_canonical_registered_short_address(db, address, "Lender Wallet Address")?;
let payer_pubkey = resolve_pubkey_from_short_address(db, address)
.map_err(|_| "Lender Wallet Address is not registered.".to_string())?
.ok_or_else(|| "Lender Wallet Address is not registered.".to_string())?;
let payer_pubkey_hex = encode(&payer_pubkey);
if !Wallet::verify_transaction_with_public_key(&hash, signature, &payer_pubkey_hex).await {
return Err("Invalid signature the ContractPaymentTransaction.")
.map_err(|s| s.to_string())?;
}
// Resolve the referenced loan contract from the saved chain so the
// payment can be validated against its repayment terms.
let contract_hash = &self.unsigned_contract_payment.contract_hash;
let contract_key =
decode(contract_hash).map_err(|e| format!("Invalid contract hash: {e}"))?;
let contract_bytes = request_transaction_by_txid(db, contract_key.clone()).await;
let RpcResponse::Binary(bytes) = contract_bytes;
if bytes.is_empty() {
return Err("Invalid loan contract: empty transaction bytes".to_string());
}
let txtype = bytes[0];
if txtype != 7 {
return Err(
"Invalid loan contract: referenced transaction is not a loan contract".to_string(),
);
}
let body = &bytes[1..];
let contract = LoanContractTransaction::from_bytes(txtype, body)
.await
.map_err(|e| format!("Invalid loan contract: {e}"))?;
// Payments can only be made against an active contract whose
// collateral has not already been claimed or closed out.
let loantree = db.open_tree("loan").unwrap();
match loantree.get(contract_key) {
Ok(Some(loan_value)) => {
if loan_value == "false" {
return Err(
"The collateral has already been claimed on this contract".to_string()
);
}
}
Ok(None) => {
return Err("This contract is not valid".to_string());
}
Err(_) => {
return Err("This contract is not valid".to_string());
}
}
// Use the saved payment history to prevent overpayment and to
// determine how much of the contract remains outstanding.
let total_paidback_amount = get_total_payments(db, contract_hash).await;
let total_to_be_paidback = contract
.unsigned_loan_contract
.payment_amount
.checked_mul(contract.unsigned_loan_contract.payment_number as u64)
.ok_or_else(|| "Loan repayment total overflowed.".to_string())?;
if total_paidback_amount >= total_to_be_paidback {
return Err("All payments have been made".to_string());
}
// Reject payments that would overshoot the remaining balance.
let payback_amount = self.unsigned_contract_payment.payback_amount;
let remaining_balance = total_to_be_paidback.saturating_sub(total_paidback_amount);
if payback_amount > remaining_balance {
return Err("Payment exceeds the remaining contract balance".to_string());
}
// Borrower payments must cover the fixed base-coin fee and an
// asset-denominated tip of at least 1% of the payment amount.
let txfee = self.unsigned_contract_payment.txfee;
if txfee < BORROWER_FEE {
return Err(format!(
"Loan payment transaction fee is below the minimum required fee of {BORROWER_FEE}."
));
}
let tip = self.unsigned_contract_payment.tip;
let loaned_coin = contract.unsigned_loan_contract.loan_coin;
let payback_amount = self.unsigned_contract_payment.payback_amount;
let minimum_tip = payback_amount.div_ceil(100);
if tip < minimum_tip {
return Err("Loan payment tip must be at least 1% of the payment amount".to_string());
}
// Balance checks combine confirmed wallet state with pending
// mempool usage so in-flight loan payments cannot overspend.
let value = tip
.checked_add(payback_amount)
.ok_or_else(|| "Loan payment total overflowed.".to_string())?;
if !balance_checkup(db, value, txfee, loaned_coin, address).await {
return Err("Insuficient funds for this Transfer Transaction!".to_string());
}
// Saved-chain duplicates are rejected by txid even if the mempool
// did not already contain the transaction.
let tree = "txid";
if !db_hex_verification(db, tree, &hash).await {
return Err("This transaction already exists.".to_string());
}
// Verification returns no auxiliary cleanup marker for this transaction type.
let sign = "";
Ok(sign.to_string())
}
}