diff --git a/src/common/loan_schedule.rs b/src/common/loan_schedule.rs index 4e6b52c..eceb591 100644 --- a/src/common/loan_schedule.rs +++ b/src/common/loan_schedule.rs @@ -69,9 +69,35 @@ pub fn overdue_payment_count( Ok(overdue) } +pub fn final_grace_period_passed( + start_timestamp: u32, + period: &str, + payment_number: u8, + grace_period: u8, + as_of_timestamp: u32, +) -> Result { + let start = Utc + .timestamp_opt(start_timestamp as i64, 0) + .single() + .ok_or_else(|| "Invalid loan start timestamp.".to_string())? + .date_naive(); + let final_index = (payment_number as u32) + .checked_add(grace_period as u32) + .ok_or_else(|| "Loan final grace period overflowed.".to_string())?; + let due = due_date(start, period, final_index)?; + let final_claim_at = due + .checked_add_signed(Duration::days(1)) + .and_then(|date| date.and_hms_opt(0, 0, 0)) + .ok_or_else(|| "Loan final grace timestamp overflowed.".to_string())? + .and_utc() + .timestamp(); + + Ok(as_of_timestamp as i64 >= final_claim_at) +} + #[cfg(test)] mod tests { - use super::overdue_payment_count; + use super::{final_grace_period_passed, overdue_payment_count}; use chrono::{TimeZone, Utc}; fn timestamp(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> u32 { @@ -145,4 +171,16 @@ mod tests { 0 ); } + + #[test] + fn final_grace_period_passes_after_full_term_plus_grace_due_date() { + let start = timestamp(2026, 3, 15, 12, 0, 0); + assert!( + !final_grace_period_passed(start, "d", 5, 1, timestamp(2026, 3, 21, 23, 59, 59)) + .unwrap() + ); + assert!( + final_grace_period_passed(start, "d", 5, 1, timestamp(2026, 3, 22, 0, 0, 0)).unwrap() + ); + } } diff --git a/src/orphans/undo_transactions/undo_borrower.rs b/src/orphans/undo_transactions/undo_borrower.rs index bdec875..81a4b1f 100644 --- a/src/orphans/undo_transactions/undo_borrower.rs +++ b/src/orphans/undo_transactions/undo_borrower.rs @@ -4,6 +4,7 @@ use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; use crate::records::record_chain::add_payments_db::remove_payment; +use crate::records::record_chain::loan_index::remove_loan_activity_index; use crate::records::record_chain::nft_provenance::remove_nft_history_entry; use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; @@ -86,6 +87,11 @@ pub async fn undo_borrower_transaction( .map_err(|e| format!("Failed to open txid tree: {e}"))?; let tx_hash = transaction.unsigned_contract_payment.hash().await; let tx_hash_bytes = decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?; + let _ = remove_loan_activity_index( + db, + &transaction.unsigned_contract_payment.contract_hash, + &tx_hash_bytes, + ); let _ = remove_wallet_transaction_index(db, &[borrower, &lender, mining_receiver], &tx_hash_bytes); txid_tree diff --git a/src/orphans/undo_transactions/undo_collateral.rs b/src/orphans/undo_transactions/undo_collateral.rs index c91c021..36cc512 100644 --- a/src/orphans/undo_transactions/undo_collateral.rs +++ b/src/orphans/undo_transactions/undo_collateral.rs @@ -5,6 +5,7 @@ use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; use crate::records::memory::mempool::BASECOIN; +use crate::records::record_chain::loan_index::remove_loan_activity_index; use crate::records::record_chain::nft_provenance::remove_nft_history_entry; use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; @@ -91,6 +92,11 @@ pub async fn undo_collateral_transaction( let txid_tree = db.open_tree("txid").unwrap(); let tx_hash = transaction.unsigned_collateral_claim.hash().await; let tx_hash_bytes = decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?; + let _ = remove_loan_activity_index( + db, + &transaction.unsigned_collateral_claim.contract_hash, + &tx_hash_bytes, + ); let _ = remove_wallet_transaction_index(db, &[claimer, mining_receiver], &tx_hash_bytes); txid_tree.remove(tx_hash_bytes.clone()).unwrap(); diff --git a/src/orphans/undo_transactions/undo_loan_creation.rs b/src/orphans/undo_transactions/undo_loan_creation.rs index e0056db..3046923 100644 --- a/src/orphans/undo_transactions/undo_loan_creation.rs +++ b/src/orphans/undo_transactions/undo_loan_creation.rs @@ -4,6 +4,7 @@ use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; use crate::records::memory::mempool::BASECOIN; +use crate::records::record_chain::loan_index::remove_loan_contract_index; use crate::records::record_chain::nft_provenance::remove_nft_history_entry; use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index; use crate::sled::Db; @@ -80,6 +81,7 @@ pub async fn undo_loan_creation_transaction( let hash_binary = decode(hash).unwrap(); let _ = remove_wallet_transaction_index(db, &[lender, borrower, mining_receiver], &hash_binary); + let _ = remove_loan_contract_index(db, &[lender, borrower], &hash_binary); // delete the txid and remove the active loan record // so the contract no longer exists on-chain diff --git a/src/records/record_chain/borrower_tx.rs b/src/records/record_chain/borrower_tx.rs index 596a190..f28cc49 100644 --- a/src/records/record_chain/borrower_tx.rs +++ b/src/records/record_chain/borrower_tx.rs @@ -2,6 +2,7 @@ use crate::blocks::loan_payment::ContractPaymentTransaction; use crate::blocks::loans::LoanContractTransaction; use crate::decode; use crate::records::memory::mempool::BASECOIN; +use crate::records::record_chain::loan_index::index_loan_activity_transaction; use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects}; use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; @@ -114,6 +115,13 @@ pub async fn process_borrower( **index as u32, &txhash_bytes, )?; + index_loan_activity_transaction( + pending_effects, + &transaction.unsigned_contract_payment.contract_hash, + block_header_number, + **index as u32, + &txhash_bytes, + )?; pending_effects.append_tree_if_key_exists( "nfts", "nft_history", diff --git a/src/records/record_chain/collateral_tx.rs b/src/records/record_chain/collateral_tx.rs index 40ba679..2077a4e 100644 --- a/src/records/record_chain/collateral_tx.rs +++ b/src/records/record_chain/collateral_tx.rs @@ -3,6 +3,7 @@ use crate::blocks::loans::LoanContractTransaction; use crate::common::asset_names::loan_collateral_asset_name_or_base; use crate::decode; use crate::records::memory::mempool::BASECOIN; +use crate::records::record_chain::loan_index::index_loan_activity_transaction; use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects}; use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; @@ -107,6 +108,13 @@ pub async fn process_collateral( **index as u32, &txhash_bytes, )?; + index_loan_activity_transaction( + pending_effects, + &transaction.unsigned_collateral_claim.contract_hash, + block_header_number, + **index as u32, + &txhash_bytes, + )?; pending_effects.append_tree_if_key_exists( "nfts", "nft_history", diff --git a/src/records/record_chain/lender_tx.rs b/src/records/record_chain/lender_tx.rs index 31974db..7be0ef3 100644 --- a/src/records/record_chain/lender_tx.rs +++ b/src/records/record_chain/lender_tx.rs @@ -2,6 +2,7 @@ use crate::blocks::loans::LoanContractTransaction; use crate::common::asset_names::loan_collateral_asset_name_or_base; use crate::decode; use crate::records::memory::mempool::BASECOIN; +use crate::records::record_chain::loan_index::index_loan_contract_transaction; use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects}; use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; use crate::sled::Db; @@ -92,6 +93,16 @@ pub async fn process_lender( **index as u32, &txhash_bytes, )?; + index_loan_contract_transaction( + pending_effects, + &[ + &transaction.unsigned_loan_contract.lender, + &transaction.unsigned_loan_contract.borrower, + ], + block_header_number, + **index as u32, + &txhash_bytes, + )?; let loankey = decode(&txhash).map_err(|e| format!("Error decoding hex: {e}"))?; pending_effects.set_tree("loan", loankey, b"true".to_vec()); pending_effects.append_tree_if_key_exists( diff --git a/src/records/record_chain/loan_index.rs b/src/records/record_chain/loan_index.rs new file mode 100644 index 0000000..7b29fb8 --- /dev/null +++ b/src/records/record_chain/loan_index.rs @@ -0,0 +1,183 @@ +use crate::records::record_chain::pending_effects::PendingEffects; +use crate::sled::Db; +use crate::wallets::structures::Wallet; +use crate::{decode, encode}; +use std::ops::Bound; + +pub const LOAN_CONTRACT_INDEX_TREE: &str = "loan_contract_index"; +pub const LOAN_ACTIVITY_INDEX_TREE: &str = "loan_activity_index"; + +fn prefix_successor(prefix: &[u8]) -> Option> { + let mut next = prefix.to_vec(); + for index in (0..next.len()).rev() { + if next[index] != u8::MAX { + next[index] += 1; + next.truncate(index + 1); + return Some(next); + } + } + None +} + +fn indexed_values_for_prefix( + db: &Db, + tree_name: &'static str, + prefix: Vec, +) -> Result>, String> { + let tree = db + .open_tree(tree_name) + .map_err(|err| format!("Failed to open {tree_name}: {err}"))?; + let start = Bound::Included(prefix.clone()); + let end = prefix_successor(&prefix) + .map(Bound::Excluded) + .unwrap_or(Bound::Unbounded); + let mut seen = Vec::>::new(); + + for entry in tree.range((start, end)) { + let (_key, value) = entry.map_err(|err| format!("Failed to read {tree_name}: {err}"))?; + let value = value.to_vec(); + if value.len() == 32 && !seen.iter().any(|existing| existing == &value) { + seen.push(value); + } + } + + Ok(seen) +} + +fn remove_index_value( + db: &Db, + tree_name: &'static str, + prefix: Vec, + txid: &[u8], +) -> Result<(), String> { + let tree = db + .open_tree(tree_name) + .map_err(|err| format!("Failed to open {tree_name}: {err}"))?; + let keys = tree + .scan_prefix(prefix) + .filter_map(|entry| match entry { + Ok((key, value)) if value.as_ref() == txid => Some(key.to_vec()), + _ => None, + }) + .collect::>(); + + for key in keys { + tree.remove(key) + .map_err(|err| format!("Failed to remove {tree_name} entry: {err}"))?; + } + + Ok(()) +} + +fn txid_block_height(db: &Db, txid: &[u8]) -> Result, String> { + let tree = db + .open_tree("txid") + .map_err(|err| format!("Failed to open txid tree: {err}"))?; + let Some(value) = tree + .get(txid) + .map_err(|err| format!("Failed to read txid tree: {err}"))? + else { + return Ok(None); + }; + let value = String::from_utf8_lossy(&value); + let Some((block_height, _entry_index)) = value.split_once(':') else { + return Err("Invalid txid location value.".to_string()); + }; + block_height + .parse::() + .map(Some) + .map_err(|err| format!("Invalid txid block height: {err}")) +} + +fn loan_index_key(prefix: &[u8], block_height: u32, entry_index: u32) -> Vec { + let mut key = Vec::with_capacity(prefix.len() + 8); + key.extend_from_slice(prefix); + key.extend_from_slice(&block_height.to_be_bytes()); + key.extend_from_slice(&entry_index.to_be_bytes()); + key +} + +pub fn index_loan_contract_transaction( + pending_effects: &mut PendingEffects, + addresses: &[&str], + block_height: u32, + entry_index: u32, + txid: &[u8], +) -> Result<(), String> { + let mut indexed_addresses: Vec> = Vec::new(); + + for address in addresses { + let address_bytes = Wallet::short_address_to_bytes(address) + .ok_or_else(|| format!("Invalid loan contract index address: {address}"))?; + if indexed_addresses + .iter() + .any(|stored| stored == &address_bytes) + { + continue; + } + + indexed_addresses.push(address_bytes.clone()); + let key = loan_index_key(&address_bytes, block_height, entry_index); + pending_effects.set_tree(LOAN_CONTRACT_INDEX_TREE, key, txid.to_vec()); + } + + Ok(()) +} + +pub fn index_loan_activity_transaction( + pending_effects: &mut PendingEffects, + contract_hash: &str, + block_height: u32, + entry_index: u32, + txid: &[u8], +) -> Result<(), String> { + let contract_bytes = decode(contract_hash) + .map_err(|err| format!("Invalid loan activity contract hash: {err}"))?; + let key = loan_index_key(&contract_bytes, block_height, entry_index); + pending_effects.set_tree(LOAN_ACTIVITY_INDEX_TREE, key, txid.to_vec()); + Ok(()) +} + +pub fn indexed_contract_txids_for_address(db: &Db, address: &str) -> Result>, String> { + let address_bytes = Wallet::short_address_to_bytes(address) + .ok_or_else(|| "error: Invalid wallet address".to_string())?; + indexed_values_for_prefix(db, LOAN_CONTRACT_INDEX_TREE, address_bytes) +} + +pub fn indexed_activity_txids_for_contract( + db: &Db, + contract_hash: &str, +) -> Result>, String> { + let contract_bytes = + decode(contract_hash).map_err(|err| format!("error: Invalid contract hash: {err}"))?; + indexed_values_for_prefix(db, LOAN_ACTIVITY_INDEX_TREE, contract_bytes) +} + +pub fn remove_loan_contract_index(db: &Db, addresses: &[&str], txid: &[u8]) -> Result<(), String> { + let Some(block_height) = txid_block_height(db, txid)? else { + return Ok(()); + }; + + for address in addresses { + let mut prefix = Wallet::short_address_to_bytes(address) + .ok_or_else(|| format!("Invalid loan contract index address: {address}"))?; + prefix.extend_from_slice(&block_height.to_be_bytes()); + remove_index_value(db, LOAN_CONTRACT_INDEX_TREE, prefix, txid)?; + } + + Ok(()) +} + +pub fn remove_loan_activity_index(db: &Db, contract_hash: &str, txid: &[u8]) -> Result<(), String> { + let Some(block_height) = txid_block_height(db, txid)? else { + return Ok(()); + }; + let mut prefix = decode(contract_hash) + .map_err(|err| format!("Invalid loan activity contract hash: {err}"))?; + prefix.extend_from_slice(&block_height.to_be_bytes()); + remove_index_value(db, LOAN_ACTIVITY_INDEX_TREE, prefix, txid) +} + +pub fn format_txid(txid: &[u8]) -> String { + encode(txid) +} diff --git a/src/records/record_chain/mod.rs b/src/records/record_chain/mod.rs index 78828bd..40f211f 100644 --- a/src/records/record_chain/mod.rs +++ b/src/records/record_chain/mod.rs @@ -7,6 +7,7 @@ pub mod genesis_tx; pub mod header_number; pub mod issue_token_tx; pub mod lender_tx; +pub mod loan_index; pub mod marketing_campaign_index; pub mod marketing_tx; pub mod nft_provenance; diff --git a/src/rpc/command_maps.rs b/src/rpc/command_maps.rs index 359770b..70e0e58 100644 --- a/src/rpc/command_maps.rs +++ b/src/rpc/command_maps.rs @@ -59,6 +59,7 @@ pub const RPC_VANITY_OWNER_LOOKUP: u8 = 45; pub const RPC_LATEST_ADDRESS_TRANSACTIONS: u8 = 46; pub const RPC_MARKETING_CAMPAIGN_HISTORY: u8 = 47; pub const RPC_TOKEN_CATALOG: u8 = 48; +pub const RPC_COLLATERAL_STATUS: u8 = 49; pub const RPC_REPLY: u8 = 255; pub const MAX_RPC_REPLY_BYTES: usize = 64 * 1024 * 1024; diff --git a/src/rpc/commands/contract.rs b/src/rpc/commands/contract.rs index 80c2e39..0e103c0 100644 --- a/src/rpc/commands/contract.rs +++ b/src/rpc/commands/contract.rs @@ -4,14 +4,15 @@ use crate::blocks::loans::LoanContractTransaction; use crate::common::loan_schedule::overdue_payment_count; use crate::encode; use crate::records::memory::mempool::get_pending_payments_for_contract; -use crate::records::record_chain::wallet_tx_index::WALLET_TX_INDEX_TREE; +use crate::records::record_chain::loan_index::{ + format_txid, indexed_activity_txids_for_contract, indexed_contract_txids_for_address, +}; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; use crate::rpc::responses::RpcResponse; use crate::sled::Db; use crate::wallets::structures::Wallet; use crate::{Local, TimeZone, Utc}; -use std::collections::{BTreeMap, BTreeSet}; -use std::ops::Bound; +use std::collections::BTreeMap; fn format_amount(value: u64) -> f64 { // Contract RPC output presents coin amounts as user-facing decimal values. @@ -224,8 +225,7 @@ async fn build_contract_summary_from_parts( async fn build_contract_summary(hash: Vec, db: &Db) -> Result { let contract_hash = encode(&hash); let contract = load_contract(db, &hash).await?; - let loan = &contract.unsigned_loan_contract; - let txids = indexed_txids_for_addresses(db, &[loan.lender.trim(), loan.borrower.trim()])?; + let txids = indexed_activity_txids_for_contract(db, &contract_hash)?; let (payments, collateral_claim) = collect_contract_activity_from_txids(db, &contract_hash, txids).await?; build_contract_summary_from_parts(contract_hash, contract, payments, collateral_claim, db).await @@ -240,57 +240,6 @@ pub async fn contract_details(hash: Vec, db: &Db) -> RpcResponse { RpcResponse::Binary(output.into_bytes()) } -fn prefix_successor(prefix: &[u8]) -> Option> { - let mut next = prefix.to_vec(); - for index in (0..next.len()).rev() { - if next[index] != u8::MAX { - next[index] += 1; - next.truncate(index + 1); - return Some(next); - } - } - None -} - -fn indexed_txids_for_address(db: &Db, address: &str) -> Result>, String> { - let address_bytes = Wallet::short_address_to_bytes(address) - .ok_or_else(|| "error: Invalid wallet address".to_string())?; - let tree = db - .open_tree(WALLET_TX_INDEX_TREE) - .map_err(|err| format!("error: Failed to open wallet transaction index: {err}"))?; - let start = Bound::Included(address_bytes.clone()); - let end = prefix_successor(&address_bytes) - .map(Bound::Excluded) - .unwrap_or(Bound::Unbounded); - let mut txids = Vec::new(); - let mut seen = BTreeSet::new(); - - for entry in tree.range((start, end)) { - let (_key, value) = entry - .map_err(|err| format!("error: Failed to read wallet transaction index: {err}"))?; - if value.len() == 32 && seen.insert(value.to_vec()) { - txids.push(value.to_vec()); - } - } - - Ok(txids) -} - -fn indexed_txids_for_addresses(db: &Db, addresses: &[&str]) -> Result>, String> { - let mut seen = BTreeSet::new(); - let mut txids = Vec::new(); - - for address in addresses { - for txid in indexed_txids_for_address(db, address.trim())? { - if seen.insert(txid.clone()) { - txids.push(txid); - } - } - } - - Ok(txids) -} - pub async fn contract_details_by_address(address: String, db: &Db) -> RpcResponse { // Return every saved contract where the address appears as either the // lender or borrower, each expanded into the same text summary view. @@ -298,7 +247,7 @@ pub async fn contract_details_by_address(address: String, db: &Db) -> RpcRespons return RpcResponse::Binary(b"error: Invalid wallet address".to_vec()); }; - let txids = match indexed_txids_for_address(db, &address) { + let txids = match indexed_contract_txids_for_address(db, &address) { Ok(txids) => txids, Err(err) => return RpcResponse::Binary(err.into_bytes()), }; @@ -328,12 +277,10 @@ pub async fn contract_details_by_address(address: String, db: &Db) -> RpcRespons let mut summaries = Vec::new(); for (contract_hash, contract) in contracts { - let loan = &contract.unsigned_loan_contract; - let txids = - match indexed_txids_for_addresses(db, &[loan.lender.trim(), loan.borrower.trim()]) { - Ok(txids) => txids, - Err(err) => return RpcResponse::Binary(err.into_bytes()), - }; + let txids = match indexed_activity_txids_for_contract(db, &contract_hash) { + Ok(txids) => txids, + Err(err) => return RpcResponse::Binary(err.into_bytes()), + }; let (payments, collateral_claim) = match collect_contract_activity_from_txids(db, &contract_hash, txids).await { Ok(activity) => activity, @@ -361,3 +308,27 @@ pub async fn contract_details_by_address(address: String, db: &Db) -> RpcRespons RpcResponse::Binary(output.into_bytes()) } + +pub async fn collateral_status(hash: Vec, db: &Db) -> RpcResponse { + if hash.len() != 32 { + return RpcResponse::Binary(b"error: Invalid contract hash length".to_vec()); + } + let tree = match db.open_tree("loan") { + Ok(tree) => tree, + Err(err) => { + return RpcResponse::Binary( + format!("error: Failed to open loan tree: {err}").into_bytes(), + ) + } + }; + match tree.get(&hash) { + Ok(Some(value)) if value == b"false" => RpcResponse::Binary(vec![1]), + Ok(Some(_)) => RpcResponse::Binary(vec![0]), + Ok(None) => RpcResponse::Binary( + format!("error: Contract not found: {}", format_txid(&hash)).into_bytes(), + ), + Err(err) => RpcResponse::Binary( + format!("error: Failed to read collateral status: {err}").into_bytes(), + ), + } +} diff --git a/src/rpc/server/rpc_command_loop.rs b/src/rpc/server/rpc_command_loop.rs index 7c8f756..be1bf86 100644 --- a/src/rpc/server/rpc_command_loop.rs +++ b/src/rpc/server/rpc_command_loop.rs @@ -956,6 +956,25 @@ pub async fn start_loop( .send(&stream_locked, Some(&connections_key), uid) .await; } + 49 => { + // request whether a loan contract's collateral has been claimed + let (uid, _) = read_bytes_from_stream::read_uid_from_stream( + &connections_key, + stream_locked.clone(), + ) + .await?; + let contract_hash = read_bytes_from_stream::read_usize_from_stream( + &connections_key, + 32, + stream_locked.clone(), + ) + .await?; + + let result = commands::contract::collateral_status(contract_hash, &db).await; + result + .send(&stream_locked, Some(&connections_key), uid) + .await; + } 255 => { commands::route_reply::route_reply( &connections_key, diff --git a/src/standalone_tools/connections/sending_request.rs b/src/standalone_tools/connections/sending_request.rs index eb56b0d..6021fe1 100644 --- a/src/standalone_tools/connections/sending_request.rs +++ b/src/standalone_tools/connections/sending_request.rs @@ -4,14 +4,14 @@ use crate::io; use crate::records::memory::response_channels::Byte3; use crate::rpc::command_maps::{ MAX_RPC_REPLY_BYTES, RPC_ADDRESS_HISTORY, RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_BY_HASH, - RPC_BLOCK_BY_HEIGHT, RPC_BLOCK_HEIGHT, RPC_BLOCK_IP, RPC_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY, - RPC_LARGEST_TX_FEE, RPC_LATEST_ADDRESS_TRANSACTIONS, RPC_LOAN_CONTRACT, - RPC_MARKETING_CAMPAIGN_HISTORY, RPC_MEMPOOL_TX_BY_ADDRESS, RPC_MEMPOOL_TX_BY_SIGNATURE, - RPC_MEMPOOL_TX_COUNT, RPC_NETWORK_INFO, RPC_NFT_DETAILS, RPC_NFT_LIST, RPC_REGISTER_WALLET, - RPC_TIME, RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, - RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP, RPC_VALIDATE_MESSAGE, - RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP, RPC_WALLET_REGISTRATION_STATUS, - RPC_WALLET_REGISTRY_SYNC, + RPC_BLOCK_BY_HEIGHT, RPC_BLOCK_HEIGHT, RPC_BLOCK_IP, RPC_COLLATERAL_STATUS, + RPC_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY, RPC_LARGEST_TX_FEE, RPC_LATEST_ADDRESS_TRANSACTIONS, + RPC_LOAN_CONTRACT, RPC_MARKETING_CAMPAIGN_HISTORY, RPC_MEMPOOL_TX_BY_ADDRESS, + RPC_MEMPOOL_TX_BY_SIGNATURE, RPC_MEMPOOL_TX_COUNT, RPC_NETWORK_INFO, RPC_NFT_DETAILS, + RPC_NFT_LIST, RPC_REGISTER_WALLET, RPC_TIME, RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS, + RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID, + RPC_UNBLOCK_IP, RPC_VALIDATE_MESSAGE, RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP, + RPC_WALLET_REGISTRATION_STATUS, RPC_WALLET_REGISTRY_SYNC, }; use crate::standalone_tools::transaction_creator::create_transaction_request; use crate::standalone_tools::vanity_resolver::canonical_vanity_address_input; @@ -625,6 +625,24 @@ async fn build_request_bytes( bin_msg.extend_from_slice(&command_number.to_le_bytes()); bin_msg.extend_from_slice(&hashmap_key); } + 49 => { + let command_number: u8 = RPC_COLLATERAL_STATUS; + let contract_bytes = decode(&command_input).map_err(|err| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid contract hash: {err}"), + ) + })?; + if contract_bytes.len() != 32 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Contract hash must decode to 32 bytes", + )); + } + bin_msg.extend_from_slice(&command_number.to_le_bytes()); + bin_msg.extend_from_slice(&hashmap_key); + bin_msg.extend_from_slice(&contract_bytes); + } _ => { return Err(io::Error::new( io::ErrorKind::InvalidInput, diff --git a/src/startup/connections.rs b/src/startup/connections.rs index 2d690dc..b9e06c7 100644 --- a/src/startup/connections.rs +++ b/src/startup/connections.rs @@ -12,9 +12,9 @@ use crate::sled::Db; use crate::sleep; use crate::wallets::structures::Wallet; use crate::Arc; -use crate::Duration; -use crate::Mutex; - +use crate::Duration; +use crate::Mutex; + pub async fn handle_connections( db: Db, wallet: Arc, @@ -57,19 +57,19 @@ async fn attempt_bootstrap_connections( let mut last_error: Option = None; for server in filtered_servers { - // build the outbound handshake request using cloned - // shared state so each attempt can run independently - let db_clone = db.clone(); - + // build the outbound handshake request using cloned + // shared state so each attempt can run independently + let db_clone = db.clone(); + // parse the configured peer string once before spawning - // the outbound connection attempt - let socket_address = server.parse().expect("Failed to parse the socket address"); - - // Clone the Arc for use in other async functions - let map_clone = Arc::clone(&map); - - let first: bool = true; - let connect_params = Connect { + // the outbound connection attempt + let socket_address = server.parse().expect("Failed to parse the socket address"); + + // Clone the Arc for use in other async functions + let map_clone = Arc::clone(&map); + + let first: bool = true; + let connect_params = Connect { addr: socket_address, db: db_clone, node_ip: server.to_string(), @@ -77,7 +77,7 @@ async fn attempt_bootstrap_connections( map: map_clone, first, }; - + let err_string = match connect_and_handshake(connect_params).await { Ok(()) => { if context == "startup" { @@ -89,9 +89,9 @@ async fn attempt_bootstrap_connections( } Err(err) => err.to_string(), }; - - // A peer can reject us because it already has this connection recorded. - // In that case retrying other bootstrap peers would not fix the local duplicate state. + + // A peer can reject us because it already has this connection recorded. + // In that case retrying other bootstrap peers would not fix the local duplicate state. if err_string.contains("The connection is already in the connection manager Please wait 10 minutes and try again") { return Err(err_string); } @@ -121,11 +121,7 @@ async fn attempt_bootstrap_connections( Ok(false) } -pub fn spawn_isolated_bootstrap_recovery( - db: Db, - wallet: Arc, - map: Arc>, -) { +pub fn spawn_isolated_bootstrap_recovery(db: Db, wallet: Arc, map: Arc>) { let outgoing_connections = crate::Settings::load() .map(|settings| settings.outgoing_connections) .unwrap_or(0); diff --git a/src/verifications/async_funcs/verify_collateral.rs b/src/verifications/async_funcs/verify_collateral.rs index 30a2299..265e8f2 100644 --- a/src/verifications/async_funcs/verify_collateral.rs +++ b/src/verifications/async_funcs/verify_collateral.rs @@ -1,6 +1,6 @@ use crate::blocks::collateral::CollateralClaimTransaction; use crate::blocks::loans::LoanContractTransaction; -use crate::common::loan_schedule::overdue_payment_count; +use crate::common::loan_schedule::{final_grace_period_passed, overdue_payment_count}; use crate::common::types::COLLATERAL_FEE; use crate::records::memory::mempool::BASECOIN; use crate::records::wallet_registry::{ @@ -123,6 +123,7 @@ impl CollateralClaimTransaction { .payment_amount .checked_mul(contract.unsigned_loan_contract.payment_number as u64) .ok_or_else(|| "Loan repayment total overflowed.".to_string())?; + let remaining_balance = total_to_be_paidback.saturating_sub(total_paidback_amount); let address = self.unsigned_collateral_claim.address.clone(); if address != contract.unsigned_loan_contract.borrower && address != contract.unsigned_loan_contract.lender @@ -180,6 +181,19 @@ impl CollateralClaimTransaction { as_of_timestamp, )?; + if remaining_balance > 0 + && final_grace_period_passed( + contract.unsigned_loan_contract.timestamp, + &contract.unsigned_loan_contract.payment_period, + contract.unsigned_loan_contract.payment_number, + contract.unsigned_loan_contract.grace_period, + as_of_timestamp, + )? + { + let sign = ""; + return Ok(sign.to_string()); + } + // The lender must wait until the grace period has expired. if payments_due <= contract.unsigned_loan_contract.grace_period as usize { return Err("To grace period has not yet passed.".to_string());