use crate::blocks::loan_payment::ContractPaymentTransaction; use crate::blocks::loans::LoanContractTransaction; use crate::common::types::BORROWER_FEE; use crate::records::memory::mempool::get_pending_payments_for_contract; 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 { self.verify_internal(db, true).await } pub async fn verify_for_block(&self, db: &Db) -> Result { self.verify_internal(db, false).await } async fn verify_internal( &self, db: &Db, check_pending_payments: bool, ) -> Result { 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()); } if check_pending_payments { let pending_payments = get_pending_payments_for_contract(contract_hash) .await .map_err(|err| format!("Failed to check pending loan payments: {err}"))?; let pending_with_new = pending_payments .checked_add(payback_amount) .ok_or_else(|| "Pending loan payment total overflowed.".to_string())?; if pending_with_new > remaining_balance { return Err( "Payment exceeds the remaining contract balance after pending payments." .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()) } }