174 lines
8.2 KiB
Rust
174 lines
8.2 KiB
Rust
|
|
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())
|
||
|
|
}
|
||
|
|
}
|