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;
|
2026-06-27 16:28:13 +00:00
|
|
|
use crate::records::memory::mempool::get_pending_payments_for_contract;
|
2026-05-24 17:56:57 +00:00
|
|
|
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> {
|
2026-06-27 16:28:13 +00:00
|
|
|
self.verify_internal(db, true).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn verify_for_block(&self, db: &Db) -> Result<String, String> {
|
|
|
|
|
self.verify_internal(db, false).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn verify_internal(
|
|
|
|
|
&self,
|
|
|
|
|
db: &Db,
|
|
|
|
|
check_pending_payments: bool,
|
|
|
|
|
) -> Result<String, String> {
|
2026-05-24 17:56:57 +00:00
|
|
|
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());
|
|
|
|
|
}
|
2026-06-27 16:28:13 +00:00
|
|
|
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(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-24 17:56:57 +00:00
|
|
|
|
|
|
|
|
// 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())
|
|
|
|
|
}
|
|
|
|
|
}
|