Contractless/src/verifications/async_funcs/verify_lender.rs

174 lines
8.2 KiB
Rust
Raw Normal View History

2026-05-24 17:56:57 +00:00
use crate::blocks::loans::LoanContractTransaction;
use crate::common::types::{COIN_LENGTH, LENDER_FEE};
use crate::encode;
use crate::records::memory::mempool::BASECOIN;
use crate::records::wallet_registry::{
require_canonical_registered_short_address, resolve_pubkey_from_short_address,
};
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::time_checks::is_within_30_days;
use crate::verifications::async_funcs::checks::verify_db::{
db_bytes_verification, db_hex_verification,
};
use crate::wallets::structures::Wallet;
impl LoanContractTransaction {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
let calculated_hash = &self.unsigned_loan_contract.hash().await;
let signature2 = &self.signature2;
// Transactions already present in the mempool can short-circuit
// the deeper verification path and reuse their stored signature.
if memcheck(signature2, calculated_hash).await {
return Ok(signature2.to_string());
};
// Loan contracts require valid lender and borrower wallet addresses.
let lender = &self.unsigned_loan_contract.lender;
let borrower = &self.unsigned_loan_contract.borrower;
if !(Wallet::short_address_validation(lender) && Wallet::short_address_validation(borrower))
{
return Err("Lender or Borrower Wallet Address is Invalid.")
.map_err(|s| s.to_string())?;
}
require_canonical_registered_short_address(db, lender, "Lender Wallet Address")?;
require_canonical_registered_short_address(db, borrower, "Borrower Wallet Address")?;
// The stored transaction hash must match the locally recomputed
// hash before either participant signature is trusted.
let hash = &self.hash;
if calculated_hash != hash {
return Err("Invalid hash calculation for this TransferTransaction.")
.map_err(|s| s.to_string())?;
}
// Both lender and borrower must sign the same contract payload.
let signature1 = &self.signature1;
let lender_pubkey = resolve_pubkey_from_short_address(db, lender)
.map_err(|_| "Lender Wallet Address is not registered.".to_string())?
.ok_or_else(|| "Lender Wallet Address is not registered.".to_string())?;
let lender_pubkey_hex = encode(&lender_pubkey);
if !Wallet::verify_transaction_with_public_key(hash, signature1, &lender_pubkey_hex).await {
return Err("Invalid signature1 the RewardsTransaction.").map_err(|s| s.to_string())?;
}
let signature2 = &self.signature2;
let borrower_pubkey = resolve_pubkey_from_short_address(db, borrower)
.map_err(|_| "Borrower Wallet Address is not registered.".to_string())?
.ok_or_else(|| "Borrower Wallet Address is not registered.".to_string())?;
let borrower_pubkey_hex = encode(&borrower_pubkey);
if !Wallet::verify_transaction_with_public_key(hash, signature2, &borrower_pubkey_hex).await
{
return Err("Invalid signature2 the RewardsTransaction.").map_err(|s| s.to_string())?;
}
// Loan offers expire if they are broadcast too long after signing.
let timestamp = self.unsigned_loan_contract.timestamp;
if !is_within_30_days(timestamp).await {
return Err("Timestamp is to old. LoanContractTransactions must be broadcast within 30 days of signing.").map_err(|s| s.to_string())?;
}
let loan_coin = self.unsigned_loan_contract.loan_coin.clone();
let loan_amount = self.unsigned_loan_contract.loan_amount;
let txfee = self.unsigned_loan_contract.txfee;
let loan_coin_length = &loan_coin.to_string().len();
let collateral = self.unsigned_loan_contract.collateral.clone();
let collateral_length = collateral.to_string().len();
let collateral_amount = self.unsigned_loan_contract.collateral_amount;
// Creating the loan contract has a fixed minimum lender fee.
if txfee < LENDER_FEE {
return Err(format!(
"Loan contract transaction fee is below the minimum required fee of {LENDER_FEE}."
));
}
// Asset names use the fixed padded coin-length format.
if loan_coin_length != &COIN_LENGTH || collateral_length != COIN_LENGTH {
return Err(
"Coin/Token lengths must be 15 characters each. Consider padding with empty spaces",
)
.map_err(|s| s.to_string())?;
}
// Loaned assets must already exist as either the network base coin
// or a previously created token.
let tree = "tokens";
if !(loan_coin.trim().to_lowercase() == BASECOIN.trim().to_lowercase()
|| db_bytes_verification(db, tree, &loan_coin).await)
{
return Err("Coin/Token does not exist.".to_string());
}
// Collateral can be a token or an NFT asset, but it must already
// exist before the contract can be created.
let nft_tree = "nfts";
if !(db_bytes_verification(db, tree, &collateral).await
|| db_bytes_verification(db, nft_tree, &collateral).await)
{
return Err("Coin/Token/Nft does not exist.".to_string());
}
// The lender must be able to fund the loan and fee, while the
// borrower must already own the pledged collateral.
if !balance_checkup(db, loan_amount, txfee, loan_coin, lender).await {
return Err("Insuficient funds for this Lender Transaction!".to_string());
}
if !balance_checkup(db, collateral_amount, 0, collateral, borrower).await {
return Err("Insuficient funds for this Transfer Transaction!".to_string());
}
// Payment schedules are limited to day, week, or month intervals.
let payment_period = &self.unsigned_loan_contract.payment_period;
if payment_period != "d" && payment_period != "w" && payment_period != "m" {
return Err(
"The payment period must be either 'd' for days, 'w' for weeks, or 'm' for months.",
)
.map_err(|s| s.to_string())?;
}
// Repayment structure must make sense: no single payment can exceed
// the loan, and the full schedule must cover at least the principal.
let payment_amount = self.unsigned_loan_contract.payment_amount;
if payment_amount > loan_amount {
return Err("The payment amount must be less than the loan amount.")
.map_err(|s| s.to_string())?;
}
let payment_number_u64 = self.unsigned_loan_contract.payment_number as u64;
let scheduled_total = payment_amount
.checked_mul(payment_number_u64)
.ok_or_else(|| "Loan repayment schedule overflowed.".to_string())?;
if scheduled_total < loan_amount {
return Err("The total of all scheduled payments must be at least the loan amount.")
.map_err(|s| s.to_string())?;
}
// The late-value threshold and grace-period settings are bounded
// so they cannot exceed the contract's own repayment schedule.
let max_late_value = self.unsigned_loan_contract.max_late_value;
if max_late_value > loan_amount {
return Err("The max late value cannot exceed the loan amount.")
.map_err(|s| s.to_string())?;
}
let payment_number = &self.unsigned_loan_contract.payment_number;
let grace_period = &self.unsigned_loan_contract.grace_period;
if grace_period > payment_number {
return Err("The grace period cannot exceed the number of payments.")
.map_err(|s| s.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())
}
}