use crate::blocks::loan_payment::ContractPaymentTransaction; use crate::blocks::loans::LoanContractTransaction; 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::nft_provenance::remove_nft_history_entry; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; use crate::rpc::responses::RpcResponse; use crate::sled::Db; pub async fn undo_borrower_transaction( transaction: ContractPaymentTransaction, mining_receiver: &str, db: &Db, ) -> Result<(), String> { // restore balances and database state for a contract payment // that is being removed during orphan rollback let operand_subtraction = "subtraction"; let operand_addition = "addition"; let ( _network_name, _padded_base_coin, type_str, _torrentpath, _wallet_path, _blockpath, _db_path, _balance_path, _log_path, ) = block_extension_and_paths(); // reload the original loan contract so the rollback uses the // same lender and asset information the payment was based on let contract_hash = decode(&transaction.unsigned_contract_payment.contract_hash) .map_err(|e| format!("Error decoding contract hash: {e}"))?; let contract = request_transaction_by_txid(db, contract_hash.clone()).await; let loan_txtype = 7; let loan_tx = match contract { RpcResponse::Binary(contract_bytes) => { if contract_bytes.is_empty() { return Err("Invalid loan contract: empty transaction bytes".to_string()); } if contract_bytes[0] != loan_txtype { return Err( "Invalid loan contract: referenced transaction is not a loan contract" .to_string(), ); } LoanContractTransaction::from_bytes(loan_txtype, &contract_bytes[1..]) .await .map_err(|e| e.to_string())? } }; let lender = loan_tx.unsigned_loan_contract.lender; let loan_coin = loan_tx.unsigned_loan_contract.loan_coin; let borrower = &transaction.unsigned_contract_payment.address; let payback_amount = transaction.unsigned_contract_payment.payback_amount; let tip = transaction.unsigned_contract_payment.tip; let txfee = transaction.unsigned_contract_payment.txfee; // reverse the fee, tip, and repayment movements that were // applied when the borrower payment was originally saved let _ = balance_sheet_operation_with_db(db, mining_receiver, txfee, &type_str, operand_subtraction); let _ = balance_sheet_operation_with_db(db, borrower, txfee, &type_str, operand_addition); let _ = balance_sheet_operation_with_db(db, mining_receiver, tip, &loan_coin, operand_subtraction); let _ = balance_sheet_operation_with_db(db, borrower, tip, &loan_coin, operand_addition); let _ = balance_sheet_operation_with_db( db, &lender, payback_amount, &loan_coin, operand_subtraction, ); let _ = balance_sheet_operation_with_db(db, borrower, payback_amount, &loan_coin, operand_addition); // Remove the payment transaction lookup from the txid tree. let txid_tree = db .open_tree("txid") .map_err(|e| format!("Failed to open txid tree: {e}"))?; let tx_hash = transaction.unsigned_contract_payment.hash().await; txid_tree .remove(decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?) .map_err(|e| format!("Failed to remove borrower txid: {e}"))?; // Loan payments involving NFTs also add a provenance entry for the loan // asset, so remove it if this loan coin is tracked as an NFT. let nft_tree = db .open_tree("nfts") .map_err(|e| format!("Failed to open nfts tree: {e}"))?; let tx_hash_bytes = decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?; if nft_tree.contains_key(loan_coin.as_bytes()).unwrap_or(false) { let _ = remove_nft_history_entry(db, &loan_coin, &tx_hash_bytes); } // The aggregate payment record is reduced by the payment being undone. let _ = remove_payment(db, contract_hash, payback_amount); Ok(()) }