use crate::blocks::loans::LoanContractTransaction; use crate::common::nft_assets::nft_asset_name; use crate::common::types::Transaction; use crate::decode; use crate::records::balance_sheet::get_wallet_balance::get_balance_with_db; use crate::records::memory::mempool::{ get_basecoin_balance, get_coin_balance, signature_exists, BASECOIN, }; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; use crate::rpc::responses::RpcResponse; use crate::sled::Db; use std::collections::{HashMap, HashSet}; #[derive(Clone)] struct BalanceDebit { address: String, coin: String, amount: u64, } struct TransactionBalanceView { hash: String, signatures: Vec, debits: Vec, check_saved_txid: bool, } pub(crate) struct TransactionBalanceReservation { hash: String, signatures: Vec, debits: HashMap<(String, String), u64>, available: HashMap<(String, String), u64>, } pub struct BlockBalanceTracker { transaction_hashes: HashSet, transaction_signatures: HashSet, reserved: HashMap<(String, String), u64>, } impl BlockBalanceTracker { pub fn new() -> Self { Self { transaction_hashes: HashSet::new(), transaction_signatures: HashSet::new(), reserved: HashMap::new(), } } pub(crate) fn reserve( &mut self, reservation: TransactionBalanceReservation, ) -> Result<(), String> { if !self.transaction_hashes.insert(reservation.hash) { return Err("Duplicate transaction hash inside block.".to_string()); } for signature in reservation.signatures { if !signature.is_empty() && !self.transaction_signatures.insert(signature) { return Err("Duplicate transaction signature inside block.".to_string()); } } let mut updates = Vec::new(); for (key, transaction_debit) in reservation.debits { let already_reserved = self.reserved.get(&key).copied().unwrap_or(0); let required = already_reserved .checked_add(transaction_debit) .ok_or_else(|| "Block balance reservation overflowed.".to_string())?; let available = reservation.available.get(&key).copied().unwrap_or(0); if required > available { let (address, coin) = key; return Err(format!( "Insufficient block-local funds: address={address} coin={} available={available} required={required}", coin.trim() )); } updates.push((key, required)); } for (key, required) in updates { self.reserved.insert(key, required); } Ok(()) } } impl Default for BlockBalanceTracker { fn default() -> Self { Self::new() } } fn add_debit(debits: &mut Vec, address: &str, coin: String, amount: u64) { if amount == 0 { return; } debits.push(BalanceDebit { address: address.to_string(), coin, amount, }); } fn add_to_total( totals: &mut HashMap<(String, String), u64>, debit: &BalanceDebit, ) -> Result<(), String> { let key = (debit.address.clone(), debit.coin.clone()); let current = totals.get(&key).copied().unwrap_or(0); let updated = current .checked_add(debit.amount) .ok_or_else(|| "Block balance reservation overflowed.".to_string())?; totals.insert(key, updated); Ok(()) } fn add_amount_to_total( totals: &mut HashMap<(String, String), u64>, key: (String, String), amount: u64, ) -> Result<(), String> { let current = totals.get(&key).copied().unwrap_or(0); let updated = current .checked_add(amount) .ok_or_else(|| "Block balance reservation overflowed.".to_string())?; totals.insert(key, updated); Ok(()) } async fn transaction_is_pending(view: &TransactionBalanceView) -> Result { for signature in &view.signatures { if signature.is_empty() { continue; } if signature_exists(signature, &view.hash) .await .map_err(|err| err.to_string())? { return Ok(true); } } Ok(false) } async fn loan_contract_for_hash( db: &Db, contract_hash: &str, ) -> Result { let contract_key = decode(contract_hash).map_err(|err| format!("Invalid loan contract hash: {err}"))?; let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, contract_key).await; if bytes.is_empty() || bytes[0] != 7 { return Err("Invalid loan contract: referenced transaction is not a loan contract".into()); } LoanContractTransaction::from_bytes(7, &bytes[1..]) .await .map_err(|err| format!("Invalid loan contract: {err}")) } fn saved_txid_exists(db: &Db, hash: &str) -> Result { let tree = db .open_tree("txid") .map_err(|err| format!("Failed to open txid tree: {err}"))?; let key = decode(hash).map_err(|err| format!("Invalid transaction hash: {err}"))?; tree.contains_key(key) .map_err(|err| format!("Failed to check txid tree: {err}")) } async fn transaction_balance_view( db: &Db, transaction: &Transaction, ) -> Result { match transaction { Transaction::Genesis(tx) => Ok(TransactionBalanceView { hash: tx.unsigned.hash().await, signatures: Vec::new(), debits: Vec::new(), check_saved_txid: false, }), Transaction::Rewards(tx) => Ok(TransactionBalanceView { hash: tx.unsigned.hash().await, signatures: Vec::new(), debits: Vec::new(), check_saved_txid: true, }), Transaction::Transfer(tx) => { let transfer = &tx.unsigned_transfer; let mut debits = Vec::new(); let asset = nft_asset_name(&transfer.coin, transfer.nft_series); // The sender pays the transferred asset plus the base-coin fee. add_debit(&mut debits, &transfer.sender, asset, transfer.value); add_debit( &mut debits, &transfer.sender, BASECOIN.clone(), transfer.txfee, ); Ok(TransactionBalanceView { hash: transfer.hash().await, signatures: vec![tx.signature.clone()], debits, check_saved_txid: true, }) } Transaction::Burn(tx) => { let burn = &tx.unsigned_burn; let mut debits = Vec::new(); let asset = nft_asset_name(&burn.coin, burn.nft_series); add_debit(&mut debits, &burn.address, asset, burn.value); add_debit(&mut debits, &burn.address, BASECOIN.clone(), burn.txfee); Ok(TransactionBalanceView { hash: burn.hash().await, signatures: vec![tx.signature.clone()], debits, check_saved_txid: true, }) } Transaction::Token(tx) => { let token = &tx.unsigned_create_token; let mut debits = Vec::new(); add_debit(&mut debits, &token.creator, BASECOIN.clone(), token.txfee); Ok(TransactionBalanceView { hash: token.hash().await, signatures: vec![tx.signature.clone()], debits, check_saved_txid: true, }) } Transaction::IssueToken(tx) => { let token = &tx.unsigned_issue_token; let mut debits = Vec::new(); add_debit(&mut debits, &token.creator, BASECOIN.clone(), token.txfee); Ok(TransactionBalanceView { hash: token.hash().await, signatures: vec![tx.signature.clone()], debits, check_saved_txid: true, }) } Transaction::Nft(tx) => { let nft = &tx.unsigned_create_nft; let mut debits = Vec::new(); add_debit(&mut debits, &nft.creator, BASECOIN.clone(), nft.txfee); Ok(TransactionBalanceView { hash: nft.hash().await, signatures: vec![tx.signature.clone()], debits, check_saved_txid: true, }) } Transaction::Marketing(tx) => { let marketing = &tx.unsigned_marketing; let mut debits = Vec::new(); add_debit( &mut debits, &marketing.advertiser, BASECOIN.clone(), marketing.txfee, ); Ok(TransactionBalanceView { hash: marketing.hash().await, signatures: vec![tx.signature.clone()], debits, check_saved_txid: true, }) } Transaction::Swap(tx) => { let swap = &tx.unsigned_swap; let mut debits = Vec::new(); let asset1 = nft_asset_name(&swap.ticker1, swap.nft_series1); let asset2 = nft_asset_name(&swap.ticker2, swap.nft_series2); let sender1_asset_total = swap .value1 .checked_add(swap.tip1) .ok_or_else(|| "Swap sender1 debit overflowed.".to_string())?; let sender2_asset_total = swap .value2 .checked_add(swap.tip2) .ok_or_else(|| "Swap sender2 debit overflowed.".to_string())?; add_debit(&mut debits, &swap.sender1, asset1, sender1_asset_total); add_debit(&mut debits, &swap.sender1, BASECOIN.clone(), swap.txfee1); add_debit(&mut debits, &swap.sender2, asset2, sender2_asset_total); add_debit(&mut debits, &swap.sender2, BASECOIN.clone(), swap.txfee2); Ok(TransactionBalanceView { hash: swap.hash().await, signatures: vec![tx.signature1.clone(), tx.signature2.clone()], debits, check_saved_txid: true, }) } Transaction::Lender(tx) => { let loan = &tx.unsigned_loan_contract; let mut debits = Vec::new(); add_debit( &mut debits, &loan.lender, loan.loan_coin.clone(), loan.loan_amount, ); add_debit(&mut debits, &loan.lender, BASECOIN.clone(), loan.txfee); add_debit( &mut debits, &loan.borrower, loan.collateral.clone(), loan.collateral_amount, ); Ok(TransactionBalanceView { hash: loan.hash().await, signatures: vec![tx.signature1.clone(), tx.signature2.clone()], debits, check_saved_txid: true, }) } Transaction::Borrower(tx) => { let payment = &tx.unsigned_contract_payment; let contract = loan_contract_for_hash(db, &payment.contract_hash).await?; let mut debits = Vec::new(); let payment_total = payment .payback_amount .checked_add(payment.tip) .ok_or_else(|| "Loan payment debit overflowed.".to_string())?; add_debit( &mut debits, &payment.address, contract.unsigned_loan_contract.loan_coin, payment_total, ); add_debit( &mut debits, &payment.address, BASECOIN.clone(), payment.txfee, ); Ok(TransactionBalanceView { hash: payment.hash().await, signatures: vec![tx.signature.clone()], debits, check_saved_txid: true, }) } Transaction::Collateral(tx) => { let collateral = &tx.unsigned_collateral_claim; let mut debits = Vec::new(); add_debit( &mut debits, &collateral.address, BASECOIN.clone(), collateral.txfee, ); Ok(TransactionBalanceView { hash: collateral.hash().await, signatures: vec![tx.signature.clone()], debits, check_saved_txid: true, }) } Transaction::Vanity(tx) => { let vanity = &tx.unsigned_vanity_address; let mut debits = Vec::new(); add_debit(&mut debits, &vanity.address, BASECOIN.clone(), vanity.txfee); Ok(TransactionBalanceView { hash: vanity.hash().await, signatures: vec![tx.signature.clone()], debits, check_saved_txid: true, }) } } } async fn pending_reserved_amount(db: &Db, address: &str, coin: &str) -> Result { if coin.trim().eq_ignore_ascii_case(BASECOIN.trim()) { get_basecoin_balance(db, address) .await .map_err(|err| err.to_string()) } else { get_coin_balance(db, address, coin) .await .map_err(|err| err.to_string()) } } pub(crate) async fn prepare_transaction_balance_reservation( db: &Db, transaction: &Transaction, ) -> Result { let view = transaction_balance_view(db, transaction).await?; if view.check_saved_txid && saved_txid_exists(db, &view.hash)? { return Err("This transaction already exists.".to_string()); } let already_reserved_in_mempool = transaction_is_pending(&view).await?; let mut debits = HashMap::new(); let mut available = HashMap::new(); // Aggregate each transaction by address/asset first. Base-coin transfers, // for example, can debit the same balance for both value and fee. for debit in &view.debits { add_to_total(&mut debits, debit)?; } for ((address, coin), transaction_debit) in &debits { let confirmed_balance = get_balance_with_db(db, address, coin) .await .map_err(|err| err.to_string())?; let mempool_reserved = pending_reserved_amount(db, address, coin).await?; let included_reserved = if already_reserved_in_mempool { *transaction_debit } else { 0 }; let external_mempool_reserved = mempool_reserved.saturating_sub(included_reserved); let spendable = confirmed_balance.saturating_sub(external_mempool_reserved); add_amount_to_total(&mut available, (address.clone(), coin.clone()), spendable)?; } Ok(TransactionBalanceReservation { hash: view.hash, signatures: view.signatures, debits, available, }) }