use crate::blocks::loans::LoanContractTransaction; use crate::common::asset_names::{ is_canonical_padded_asset_identifier, is_canonical_padded_asset_or_base, }; 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 { 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())?; } if !is_canonical_padded_asset_or_base(&loan_coin, &BASECOIN) || !is_canonical_padded_asset_identifier(&collateral) { return Err( "Loan assets must use the base coin or canonical alphanumeric asset ids." .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()) } }