loan records fixes
This commit is contained in:
parent
650c079288
commit
7beeb6c2d5
|
|
@ -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<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)]
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<u8>, db: &Db) -> Result<String, String> {
|
||||
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<u8>, db: &Db) -> RpcResponse {
|
|||
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 {
|
||||
// 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<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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Wallet>,
|
||||
|
|
@ -57,19 +57,19 @@ async fn attempt_bootstrap_connections(
|
|||
let mut last_error: Option<String> = 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<Wallet>,
|
||||
map: Arc<Mutex<Command>>,
|
||||
) {
|
||||
pub fn spawn_isolated_bootstrap_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<Mutex<Command>>) {
|
||||
let outgoing_connections = crate::Settings::load()
|
||||
.map(|settings| settings.outgoing_connections)
|
||||
.unwrap_or(0);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Reference in New Issue