loan records fixes
This commit is contained in:
parent
650c079288
commit
7beeb6c2d5
|
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue