loan records fixes

This commit is contained in:
viraladmin 2026-07-02 20:34:19 -06:00
parent 650c079288
commit 7beeb6c2d5
15 changed files with 379 additions and 97 deletions

View File

@ -69,9 +69,35 @@ pub fn overdue_payment_count(
Ok(overdue) Ok(overdue)
} }
pub fn final_grace_period_passed(
start_timestamp: u32,
period: &str,
payment_number: u8,
grace_period: u8,
as_of_timestamp: u32,
) -> Result<bool, String> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::overdue_payment_count; use super::{final_grace_period_passed, overdue_payment_count};
use chrono::{TimeZone, Utc}; use chrono::{TimeZone, Utc};
fn timestamp(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> u32 { fn timestamp(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> u32 {
@ -145,4 +171,16 @@ mod tests {
0 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()
);
}
} }

View File

@ -4,6 +4,7 @@ use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::decode; use crate::decode;
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; 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::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::nft_provenance::remove_nft_history_entry;
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index; use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; 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}"))?; .map_err(|e| format!("Failed to open txid tree: {e}"))?;
let tx_hash = transaction.unsigned_contract_payment.hash().await; 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 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 _ = let _ =
remove_wallet_transaction_index(db, &[borrower, &lender, mining_receiver], &tx_hash_bytes); remove_wallet_transaction_index(db, &[borrower, &lender, mining_receiver], &tx_hash_bytes);
txid_tree txid_tree

View File

@ -5,6 +5,7 @@ use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::decode; use crate::decode;
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
use crate::records::memory::mempool::BASECOIN; 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::nft_provenance::remove_nft_history_entry;
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index; use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; 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 txid_tree = db.open_tree("txid").unwrap();
let tx_hash = transaction.unsigned_collateral_claim.hash().await; 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 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); let _ = remove_wallet_transaction_index(db, &[claimer, mining_receiver], &tx_hash_bytes);
txid_tree.remove(tx_hash_bytes.clone()).unwrap(); txid_tree.remove(tx_hash_bytes.clone()).unwrap();

View File

@ -4,6 +4,7 @@ use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::decode; use crate::decode;
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
use crate::records::memory::mempool::BASECOIN; 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::nft_provenance::remove_nft_history_entry;
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index; use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db; use crate::sled::Db;
@ -80,6 +81,7 @@ pub async fn undo_loan_creation_transaction(
let hash_binary = decode(hash).unwrap(); let hash_binary = decode(hash).unwrap();
let _ = remove_wallet_transaction_index(db, &[lender, borrower, mining_receiver], &hash_binary); 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 // delete the txid and remove the active loan record
// so the contract no longer exists on-chain // so the contract no longer exists on-chain

View File

@ -2,6 +2,7 @@ use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction; use crate::blocks::loans::LoanContractTransaction;
use crate::decode; use crate::decode;
use crate::records::memory::mempool::BASECOIN; 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::pending_effects::{BalanceOperand, PendingEffects};
use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; use crate::records::record_chain::wallet_tx_index::index_wallet_transaction;
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
@ -114,6 +115,13 @@ pub async fn process_borrower(
**index as u32, **index as u32,
&txhash_bytes, &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( pending_effects.append_tree_if_key_exists(
"nfts", "nfts",
"nft_history", "nft_history",

View File

@ -3,6 +3,7 @@ use crate::blocks::loans::LoanContractTransaction;
use crate::common::asset_names::loan_collateral_asset_name_or_base; use crate::common::asset_names::loan_collateral_asset_name_or_base;
use crate::decode; use crate::decode;
use crate::records::memory::mempool::BASECOIN; 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::pending_effects::{BalanceOperand, PendingEffects};
use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; use crate::records::record_chain::wallet_tx_index::index_wallet_transaction;
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
@ -107,6 +108,13 @@ pub async fn process_collateral(
**index as u32, **index as u32,
&txhash_bytes, &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( pending_effects.append_tree_if_key_exists(
"nfts", "nfts",
"nft_history", "nft_history",

View File

@ -2,6 +2,7 @@ use crate::blocks::loans::LoanContractTransaction;
use crate::common::asset_names::loan_collateral_asset_name_or_base; use crate::common::asset_names::loan_collateral_asset_name_or_base;
use crate::decode; use crate::decode;
use crate::records::memory::mempool::BASECOIN; 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::pending_effects::{BalanceOperand, PendingEffects};
use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; use crate::records::record_chain::wallet_tx_index::index_wallet_transaction;
use crate::sled::Db; use crate::sled::Db;
@ -92,6 +93,16 @@ pub async fn process_lender(
**index as u32, **index as u32,
&txhash_bytes, &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}"))?; let loankey = decode(&txhash).map_err(|e| format!("Error decoding hex: {e}"))?;
pending_effects.set_tree("loan", loankey, b"true".to_vec()); pending_effects.set_tree("loan", loankey, b"true".to_vec());
pending_effects.append_tree_if_key_exists( pending_effects.append_tree_if_key_exists(

View File

@ -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<Vec<u8>> {
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<u8>,
) -> Result<Vec<Vec<u8>>, 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::<Vec<u8>>::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<u8>,
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::<Vec<_>>();
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<Option<u32>, 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::<u32>()
.map(Some)
.map_err(|err| format!("Invalid txid block height: {err}"))
}
fn loan_index_key(prefix: &[u8], block_height: u32, entry_index: u32) -> Vec<u8> {
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<u8>> = 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<Vec<Vec<u8>>, 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<Vec<Vec<u8>>, 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)
}

View File

@ -7,6 +7,7 @@ pub mod genesis_tx;
pub mod header_number; pub mod header_number;
pub mod issue_token_tx; pub mod issue_token_tx;
pub mod lender_tx; pub mod lender_tx;
pub mod loan_index;
pub mod marketing_campaign_index; pub mod marketing_campaign_index;
pub mod marketing_tx; pub mod marketing_tx;
pub mod nft_provenance; pub mod nft_provenance;

View File

@ -59,6 +59,7 @@ pub const RPC_VANITY_OWNER_LOOKUP: u8 = 45;
pub const RPC_LATEST_ADDRESS_TRANSACTIONS: u8 = 46; pub const RPC_LATEST_ADDRESS_TRANSACTIONS: u8 = 46;
pub const RPC_MARKETING_CAMPAIGN_HISTORY: u8 = 47; pub const RPC_MARKETING_CAMPAIGN_HISTORY: u8 = 47;
pub const RPC_TOKEN_CATALOG: u8 = 48; pub const RPC_TOKEN_CATALOG: u8 = 48;
pub const RPC_COLLATERAL_STATUS: u8 = 49;
pub const RPC_REPLY: u8 = 255; pub const RPC_REPLY: u8 = 255;
pub const MAX_RPC_REPLY_BYTES: usize = 64 * 1024 * 1024; pub const MAX_RPC_REPLY_BYTES: usize = 64 * 1024 * 1024;

View File

@ -4,14 +4,15 @@ use crate::blocks::loans::LoanContractTransaction;
use crate::common::loan_schedule::overdue_payment_count; use crate::common::loan_schedule::overdue_payment_count;
use crate::encode; use crate::encode;
use crate::records::memory::mempool::get_pending_payments_for_contract; 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::commands::transaction_by_txid::request_transaction_by_txid;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::sled::Db; use crate::sled::Db;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::{Local, TimeZone, Utc}; use crate::{Local, TimeZone, Utc};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::BTreeMap;
use std::ops::Bound;
fn format_amount(value: u64) -> f64 { fn format_amount(value: u64) -> f64 {
// Contract RPC output presents coin amounts as user-facing decimal values. // 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<u8>, db: &Db) -> Result<String, String> { async fn build_contract_summary(hash: Vec<u8>, db: &Db) -> Result<String, String> {
let contract_hash = encode(&hash); let contract_hash = encode(&hash);
let contract = load_contract(db, &hash).await?; let contract = load_contract(db, &hash).await?;
let loan = &contract.unsigned_loan_contract; let txids = indexed_activity_txids_for_contract(db, &contract_hash)?;
let txids = indexed_txids_for_addresses(db, &[loan.lender.trim(), loan.borrower.trim()])?;
let (payments, collateral_claim) = let (payments, collateral_claim) =
collect_contract_activity_from_txids(db, &contract_hash, txids).await?; collect_contract_activity_from_txids(db, &contract_hash, txids).await?;
build_contract_summary_from_parts(contract_hash, contract, payments, collateral_claim, db).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<u8>, db: &Db) -> RpcResponse {
RpcResponse::Binary(output.into_bytes()) RpcResponse::Binary(output.into_bytes())
} }
fn prefix_successor(prefix: &[u8]) -> Option<Vec<u8>> {
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<Vec<Vec<u8>>, 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<Vec<Vec<u8>>, 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 { pub async fn contract_details_by_address(address: String, db: &Db) -> RpcResponse {
// Return every saved contract where the address appears as either the // Return every saved contract where the address appears as either the
// lender or borrower, each expanded into the same text summary view. // 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()); 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, Ok(txids) => txids,
Err(err) => return RpcResponse::Binary(err.into_bytes()), 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(); let mut summaries = Vec::new();
for (contract_hash, contract) in contracts { for (contract_hash, contract) in contracts {
let loan = &contract.unsigned_loan_contract; let txids = match indexed_activity_txids_for_contract(db, &contract_hash) {
let txids = Ok(txids) => txids,
match indexed_txids_for_addresses(db, &[loan.lender.trim(), loan.borrower.trim()]) { Err(err) => return RpcResponse::Binary(err.into_bytes()),
Ok(txids) => txids, };
Err(err) => return RpcResponse::Binary(err.into_bytes()),
};
let (payments, collateral_claim) = let (payments, collateral_claim) =
match collect_contract_activity_from_txids(db, &contract_hash, txids).await { match collect_contract_activity_from_txids(db, &contract_hash, txids).await {
Ok(activity) => activity, Ok(activity) => activity,
@ -361,3 +308,27 @@ pub async fn contract_details_by_address(address: String, db: &Db) -> RpcRespons
RpcResponse::Binary(output.into_bytes()) RpcResponse::Binary(output.into_bytes())
} }
pub async fn collateral_status(hash: Vec<u8>, 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(),
),
}
}

View File

@ -956,6 +956,25 @@ pub async fn start_loop(
.send(&stream_locked, Some(&connections_key), uid) .send(&stream_locked, Some(&connections_key), uid)
.await; .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 => { 255 => {
commands::route_reply::route_reply( commands::route_reply::route_reply(
&connections_key, &connections_key,

View File

@ -4,14 +4,14 @@ use crate::io;
use crate::records::memory::response_channels::Byte3; use crate::records::memory::response_channels::Byte3;
use crate::rpc::command_maps::{ use crate::rpc::command_maps::{
MAX_RPC_REPLY_BYTES, RPC_ADDRESS_HISTORY, RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_BY_HASH, 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_BLOCK_BY_HEIGHT, RPC_BLOCK_HEIGHT, RPC_BLOCK_IP, RPC_COLLATERAL_STATUS,
RPC_LARGEST_TX_FEE, RPC_LATEST_ADDRESS_TRANSACTIONS, RPC_LOAN_CONTRACT, RPC_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY, RPC_LARGEST_TX_FEE, RPC_LATEST_ADDRESS_TRANSACTIONS,
RPC_MARKETING_CAMPAIGN_HISTORY, RPC_MEMPOOL_TX_BY_ADDRESS, RPC_MEMPOOL_TX_BY_SIGNATURE, RPC_LOAN_CONTRACT, RPC_MARKETING_CAMPAIGN_HISTORY, RPC_MEMPOOL_TX_BY_ADDRESS,
RPC_MEMPOOL_TX_COUNT, RPC_NETWORK_INFO, RPC_NFT_DETAILS, RPC_NFT_LIST, RPC_REGISTER_WALLET, RPC_MEMPOOL_TX_BY_SIGNATURE, RPC_MEMPOOL_TX_COUNT, RPC_NETWORK_INFO, RPC_NFT_DETAILS,
RPC_TIME, RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, RPC_NFT_LIST, RPC_REGISTER_WALLET, RPC_TIME, RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS,
RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP, RPC_VALIDATE_MESSAGE, RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID,
RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP, RPC_WALLET_REGISTRATION_STATUS, RPC_UNBLOCK_IP, RPC_VALIDATE_MESSAGE, RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP,
RPC_WALLET_REGISTRY_SYNC, RPC_WALLET_REGISTRATION_STATUS, RPC_WALLET_REGISTRY_SYNC,
}; };
use crate::standalone_tools::transaction_creator::create_transaction_request; use crate::standalone_tools::transaction_creator::create_transaction_request;
use crate::standalone_tools::vanity_resolver::canonical_vanity_address_input; 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(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key); 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( return Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,

View File

@ -12,9 +12,9 @@ use crate::sled::Db;
use crate::sleep; use crate::sleep;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::Arc; use crate::Arc;
use crate::Duration; use crate::Duration;
use crate::Mutex; use crate::Mutex;
pub async fn handle_connections( pub async fn handle_connections(
db: Db, db: Db,
wallet: Arc<Wallet>, wallet: Arc<Wallet>,
@ -57,19 +57,19 @@ async fn attempt_bootstrap_connections(
let mut last_error: Option<String> = None; let mut last_error: Option<String> = None;
for server in filtered_servers { for server in filtered_servers {
// build the outbound handshake request using cloned // build the outbound handshake request using cloned
// shared state so each attempt can run independently // shared state so each attempt can run independently
let db_clone = db.clone(); let db_clone = db.clone();
// parse the configured peer string once before spawning // parse the configured peer string once before spawning
// the outbound connection attempt // the outbound connection attempt
let socket_address = server.parse().expect("Failed to parse the socket address"); let socket_address = server.parse().expect("Failed to parse the socket address");
// Clone the Arc for use in other async functions // Clone the Arc for use in other async functions
let map_clone = Arc::clone(&map); let map_clone = Arc::clone(&map);
let first: bool = true; let first: bool = true;
let connect_params = Connect { let connect_params = Connect {
addr: socket_address, addr: socket_address,
db: db_clone, db: db_clone,
node_ip: server.to_string(), node_ip: server.to_string(),
@ -77,7 +77,7 @@ async fn attempt_bootstrap_connections(
map: map_clone, map: map_clone,
first, first,
}; };
let err_string = match connect_and_handshake(connect_params).await { let err_string = match connect_and_handshake(connect_params).await {
Ok(()) => { Ok(()) => {
if context == "startup" { if context == "startup" {
@ -89,9 +89,9 @@ async fn attempt_bootstrap_connections(
} }
Err(err) => err.to_string(), Err(err) => err.to_string(),
}; };
// A peer can reject us because it already has this connection recorded. // 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. // 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") { if err_string.contains("The connection is already in the connection manager Please wait 10 minutes and try again") {
return Err(err_string); return Err(err_string);
} }
@ -121,11 +121,7 @@ async fn attempt_bootstrap_connections(
Ok(false) Ok(false)
} }
pub fn spawn_isolated_bootstrap_recovery( pub fn spawn_isolated_bootstrap_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<Mutex<Command>>) {
db: Db,
wallet: Arc<Wallet>,
map: Arc<Mutex<Command>>,
) {
let outgoing_connections = crate::Settings::load() let outgoing_connections = crate::Settings::load()
.map(|settings| settings.outgoing_connections) .map(|settings| settings.outgoing_connections)
.unwrap_or(0); .unwrap_or(0);

View File

@ -1,6 +1,6 @@
use crate::blocks::collateral::CollateralClaimTransaction; use crate::blocks::collateral::CollateralClaimTransaction;
use crate::blocks::loans::LoanContractTransaction; 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::common::types::COLLATERAL_FEE;
use crate::records::memory::mempool::BASECOIN; use crate::records::memory::mempool::BASECOIN;
use crate::records::wallet_registry::{ use crate::records::wallet_registry::{
@ -123,6 +123,7 @@ impl CollateralClaimTransaction {
.payment_amount .payment_amount
.checked_mul(contract.unsigned_loan_contract.payment_number as u64) .checked_mul(contract.unsigned_loan_contract.payment_number as u64)
.ok_or_else(|| "Loan repayment total overflowed.".to_string())?; .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(); let address = self.unsigned_collateral_claim.address.clone();
if address != contract.unsigned_loan_contract.borrower if address != contract.unsigned_loan_contract.borrower
&& address != contract.unsigned_loan_contract.lender && address != contract.unsigned_loan_contract.lender
@ -180,6 +181,19 @@ impl CollateralClaimTransaction {
as_of_timestamp, 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. // The lender must wait until the grace period has expired.
if payments_due <= contract.unsigned_loan_contract.grace_period as usize { if payments_due <= contract.unsigned_loan_contract.grace_period as usize {
return Err("To grace period has not yet passed.".to_string()); return Err("To grace period has not yet passed.".to_string());