diff --git a/src/db/cache/transaction_cache.rs b/src/db/cache/transaction_cache.rs index 3e4770c..f2fbfc1 100644 --- a/src/db/cache/transaction_cache.rs +++ b/src/db/cache/transaction_cache.rs @@ -9,7 +9,7 @@ use ordinals::{Cenotaph, Edict, Etching, Rune, RuneId}; use crate::{ db::{ - cache::utils::{is_rune_mintable, new_ledger_entry}, + cache::utils::{is_rune_mintable, new_sequential_ledger_entry}, models::{ db_ledger_entry::DbLedgerEntry, db_ledger_operation::DbLedgerOperation, db_rune::DbRune, }, @@ -66,7 +66,7 @@ impl TransactionCache { let mut results = vec![]; for (rune_id, unallocated) in self.input_runes.iter() { for balance in unallocated { - results.push(new_ledger_entry( + results.push(new_sequential_ledger_entry( &self.location, Some(balance.amount), *rune_id, @@ -132,7 +132,7 @@ impl TransactionCache { }, ); } - let entry = new_ledger_entry( + let entry = new_sequential_ledger_entry( &self.location, None, rune_id, @@ -154,7 +154,7 @@ impl TransactionCache { // If the runestone that produced the cenotaph contained an etching, the etched rune has supply zero and is unmintable. let db_rune = DbRune::from_cenotaph_etching(rune, number, &self.location); self.etching = Some(db_rune.clone()); - let entry = new_ledger_entry( + let entry = new_sequential_ledger_entry( &self.location, None, rune_id, @@ -194,7 +194,7 @@ impl TransactionCache { amount: terms_amount.0, }, ); - Some(new_ledger_entry( + Some(new_sequential_ledger_entry( &self.location, Some(terms_amount.0), rune_id.clone(), @@ -226,7 +226,7 @@ impl TransactionCache { self.location ); // This entry does not go in the input runes, it gets burned immediately. - Some(new_ledger_entry( + Some(new_sequential_ledger_entry( &self.location, Some(terms_amount.0), rune_id.clone(), diff --git a/src/db/cache/utils.rs b/src/db/cache/utils.rs index fc13ed3..cb1145d 100644 --- a/src/db/cache/utils.rs +++ b/src/db/cache/utils.rs @@ -89,7 +89,7 @@ pub fn move_block_output_cache_to_output_cache( } /// Creates a new ledger entry while incrementing the `next_event_index`. -pub fn new_ledger_entry( +pub fn new_sequential_ledger_entry( location: &TransactionLocation, amount: Option, rune_id: RuneId, @@ -117,15 +117,25 @@ pub fn new_ledger_entry( entry } -/// Takes `amount` rune balance from `available_inputs` and moves it to `output` by generating the correct ledger entries. -/// Modifies `available_inputs` to consume balance that is already moved. If `amount` is zero, all remaining balances will be -/// transferred. If `output` is `None`, the runes will be burnt. +/// Moves rune balance from transaction inputs into a transaction output. +/// +/// # Arguments +/// +/// * `location` - Transaction location. +/// * `output` - Output where runes will be moved to. If `None`, runes are burned. +/// * `rune_id` - Rune that is being moved. +/// * `input_balances` - Balances input to this transaction for this rune. This value will be modified by the moves happening in +/// this function. +/// * `outputs` - Transaction outputs eligible to receive runes. +/// * `amount` - Amount of balance to move. If value is zero, all inputs will be moved to the output. +/// * `next_event_index` - Next sequential event index to create. This value will be modified. +/// * `ctx` - Context. pub fn move_rune_balance_to_output( location: &TransactionLocation, output: Option, rune_id: &RuneId, - available_inputs: &mut VecDeque, - eligible_outputs: &HashMap, + input_balances: &mut VecDeque, + outputs: &HashMap, amount: u128, next_event_index: &mut u32, ctx: &Context, @@ -133,7 +143,7 @@ pub fn move_rune_balance_to_output( let mut results = vec![]; // Who is this balance going to? let receiver_address = if let Some(output) = output { - match eligible_outputs.get(&output) { + match outputs.get(&output) { Some(script) => match Address::from_script(script, location.network) { Ok(address) => Some(address.to_string()), Err(e) => { @@ -171,7 +181,7 @@ pub fn move_rune_balance_to_output( let mut senders = vec![]; loop { // Do we still have input balance left to move? - let Some(input_bal) = available_inputs.pop_front() else { + let Some(input_bal) = input_balances.pop_front() else { break; }; // Select the correct move amount. @@ -188,7 +198,7 @@ pub fn move_rune_balance_to_output( // Is there still some balance left on this input? If so, keep it for later but break the loop because we've satisfied the // move amount. if balance_taken < input_bal.amount { - available_inputs.push_front(InputRuneBalance { + input_balances.push_front(InputRuneBalance { address: input_bal.address, amount: input_bal.amount - balance_taken, }); @@ -201,7 +211,7 @@ pub fn move_rune_balance_to_output( } // Add the "receive" entry, if applicable. if receiver_address.is_some() && total_sent > 0 { - results.push(new_ledger_entry( + results.push(new_sequential_ledger_entry( location, Some(total_sent), *rune_id, @@ -223,7 +233,7 @@ pub fn move_rune_balance_to_output( } // Add the "send"/"burn" entries. for (balance_taken, sender_address) in senders.iter() { - results.push(new_ledger_entry( + results.push(new_sequential_ledger_entry( location, Some(*balance_taken), *rune_id, @@ -301,6 +311,18 @@ mod test { models::db_ledger_operation::DbLedgerOperation, }; + fn dummy_eligible_output() -> HashMap { + let mut eligible_outputs = HashMap::new(); + eligible_outputs.insert( + 0u32, + ScriptBuf::from_hex( + "5120388dfba1b0069bbb0ad5eef62c1a94c46e91a3454accf40bf34b80f75e2708db", + ) + .unwrap(), + ); + eligible_outputs + } + #[test] fn ledger_writes_receive_before_send() { let address = @@ -312,14 +334,7 @@ mod test { let mut input2 = InputRuneBalance::dummy(); input2.address(None).amount(1000); available_inputs.push_back(input2); - let mut eligible_outputs = HashMap::new(); - eligible_outputs.insert( - 0u32, - ScriptBuf::from_hex( - "5120388dfba1b0069bbb0ad5eef62c1a94c46e91a3454accf40bf34b80f75e2708db", - ) - .unwrap(), - ); + let eligible_outputs = dummy_eligible_output(); let mut next_event_index = 0; let results = move_rune_balance_to_output( @@ -344,6 +359,216 @@ mod test { assert_eq!(send.amount.unwrap().0, 1000u128); assert_eq!(results.len(), 2); + assert_eq!(available_inputs.len(), 0); + } + + #[test] + fn move_to_empty_output_is_burned() { + let address = + Some("bc1p8zxlhgdsq6dmkzk4ammzcx55c3hfrg69ftx0gzlnfwq0wh38prds0nzqwf".to_string()); + let mut available_inputs = VecDeque::new(); + let mut input1 = InputRuneBalance::dummy(); + input1.address(address.clone()).amount(1000); + available_inputs.push_back(input1); + + let results = move_rune_balance_to_output( + &TransactionLocation::dummy(), + None, // Burn + &RuneId::new(840000, 25).unwrap(), + &mut available_inputs, + &HashMap::new(), + 0, + &mut 0, + &Context::empty(), + ); + + assert_eq!(results.len(), 1); + let entry1 = results.get(0).unwrap(); + assert_eq!(entry1.operation, DbLedgerOperation::Burn); + assert_eq!(entry1.address, address); + assert_eq!(entry1.amount.unwrap().0, 1000); + assert_eq!(available_inputs.len(), 0); + } + + #[test] + fn moves_partial_input_balance() { + let mut available_inputs = VecDeque::new(); + let mut input1 = InputRuneBalance::dummy(); + input1.amount(5000); // More than required in this move. + available_inputs.push_back(input1); + let eligible_outputs = dummy_eligible_output(); + + let results = move_rune_balance_to_output( + &TransactionLocation::dummy(), + Some(0), + &RuneId::new(840000, 25).unwrap(), + &mut available_inputs, + &eligible_outputs, + 1000, // Less than total available in first input. + &mut 0, + &Context::empty(), + ); + + assert_eq!(results.len(), 2); + let entry1 = results.get(0).unwrap(); + assert_eq!(entry1.operation, DbLedgerOperation::Receive); + assert_eq!(entry1.amount.unwrap().0, 1000); + let entry2 = results.get(1).unwrap(); + assert_eq!(entry2.operation, DbLedgerOperation::Send); + assert_eq!(entry2.amount.unwrap().0, 1000); + // Remainder is still in available inputs. + let remaining = available_inputs.get(0).unwrap(); + assert_eq!(remaining.amount, 4000); + } + + #[test] + fn moves_insufficient_input_balance() { + let mut available_inputs = VecDeque::new(); + let mut input1 = InputRuneBalance::dummy(); + input1.amount(1000); // Insufficient. + available_inputs.push_back(input1); + let eligible_outputs = dummy_eligible_output(); + + let results = move_rune_balance_to_output( + &TransactionLocation::dummy(), + Some(0), + &RuneId::new(840000, 25).unwrap(), + &mut available_inputs, + &eligible_outputs, + 3000, // More than total available in input. + &mut 0, + &Context::empty(), + ); + + assert_eq!(results.len(), 2); + let entry1 = results.get(0).unwrap(); + assert_eq!(entry1.operation, DbLedgerOperation::Receive); + assert_eq!(entry1.amount.unwrap().0, 1000); + let entry2 = results.get(1).unwrap(); + assert_eq!(entry2.operation, DbLedgerOperation::Send); + assert_eq!(entry2.amount.unwrap().0, 1000); + assert_eq!(available_inputs.len(), 0); + } + + #[test] + fn moves_all_remaining_balance() { + let mut available_inputs = VecDeque::new(); + let mut input1 = InputRuneBalance::dummy(); + input1.amount(6000); + available_inputs.push_back(input1); + let mut input2 = InputRuneBalance::dummy(); + input2.amount(2000); + available_inputs.push_back(input2); + let mut input3 = InputRuneBalance::dummy(); + input3.amount(2000); + available_inputs.push_back(input3); + let eligible_outputs = dummy_eligible_output(); + + let results = move_rune_balance_to_output( + &TransactionLocation::dummy(), + Some(0), + &RuneId::new(840000, 25).unwrap(), + &mut available_inputs, + &eligible_outputs, + 0, // Move all. + &mut 0, + &Context::empty(), + ); + + assert_eq!(results.len(), 4); + let entry1 = results.get(0).unwrap(); + assert_eq!(entry1.operation, DbLedgerOperation::Receive); + assert_eq!(entry1.amount.unwrap().0, 10000); + let entry2 = results.get(1).unwrap(); + assert_eq!(entry2.operation, DbLedgerOperation::Send); + assert_eq!(entry2.amount.unwrap().0, 6000); + let entry3 = results.get(2).unwrap(); + assert_eq!(entry3.operation, DbLedgerOperation::Send); + assert_eq!(entry3.amount.unwrap().0, 2000); + let entry4 = results.get(3).unwrap(); + assert_eq!(entry4.operation, DbLedgerOperation::Send); + assert_eq!(entry4.amount.unwrap().0, 2000); + assert_eq!(available_inputs.len(), 0); + } + + #[test] + fn move_to_output_with_address_failure_is_burned() { + let mut available_inputs = VecDeque::new(); + let mut input1 = InputRuneBalance::dummy(); + input1.amount(1000); + available_inputs.push_back(input1); + let mut eligible_outputs = HashMap::new(); + // Broken script buf that yields no address. + eligible_outputs.insert(0u32, ScriptBuf::from_hex("0101010101").unwrap()); + + let results = move_rune_balance_to_output( + &TransactionLocation::dummy(), + Some(0), + &RuneId::new(840000, 25).unwrap(), + &mut available_inputs, + &eligible_outputs, + 1000, + &mut 0, + &Context::empty(), + ); + + assert_eq!(results.len(), 1); + let entry1 = results.get(0).unwrap(); + assert_eq!(entry1.operation, DbLedgerOperation::Burn); + assert_eq!(entry1.amount.unwrap().0, 1000); + assert_eq!(available_inputs.len(), 0); + } + + #[test] + fn move_to_nonexistent_output_is_burned() { + let mut available_inputs = VecDeque::new(); + let mut input1 = InputRuneBalance::dummy(); + input1.amount(1000); + available_inputs.push_back(input1); + let eligible_outputs = dummy_eligible_output(); + + let results = move_rune_balance_to_output( + &TransactionLocation::dummy(), + Some(5), // Output does not exist. + &RuneId::new(840000, 25).unwrap(), + &mut available_inputs, + &eligible_outputs, + 1000, + &mut 0, + &Context::empty(), + ); + + assert_eq!(results.len(), 1); + let entry1 = results.get(0).unwrap(); + assert_eq!(entry1.operation, DbLedgerOperation::Burn); + assert_eq!(entry1.amount.unwrap().0, 1000); + assert_eq!(available_inputs.len(), 0); + } + + #[test] + fn send_not_generated_on_minted_balance() { + let mut available_inputs = VecDeque::new(); + let mut input1 = InputRuneBalance::dummy(); + input1.amount(1000).address(None); // No address because it's a mint. + available_inputs.push_back(input1); + let eligible_outputs = dummy_eligible_output(); + + let results = move_rune_balance_to_output( + &TransactionLocation::dummy(), + Some(0), + &RuneId::new(840000, 25).unwrap(), + &mut available_inputs, + &eligible_outputs, + 1000, + &mut 0, + &Context::empty(), + ); + + assert_eq!(results.len(), 1); + let entry1 = results.get(0).unwrap(); + assert_eq!(entry1.operation, DbLedgerOperation::Receive); + assert_eq!(entry1.amount.unwrap().0, 1000); + assert_eq!(available_inputs.len(), 0); } } @@ -393,4 +618,51 @@ mod test { is_rune_mintable(&rune, cap, &TransactionLocation::dummy()) } } + + mod sequential_ledger_entry { + use ordinals::RuneId; + + use crate::db::{cache::{ + transaction_location::TransactionLocation, utils::new_sequential_ledger_entry, + }, models::db_ledger_operation::DbLedgerOperation}; + + #[test] + fn increments_event_index() { + let location = TransactionLocation::dummy(); + let rune_id = RuneId::new(840000, 25).unwrap(); + let address = + Some("bc1p8zxlhgdsq6dmkzk4ammzcx55c3hfrg69ftx0gzlnfwq0wh38prds0nzqwf".to_string()); + let mut event_index = 0u32; + + let event0 = new_sequential_ledger_entry( + &location, + Some(100), + rune_id, + Some(0), + address.as_ref(), + None, + DbLedgerOperation::Receive, + &mut event_index, + ); + assert_eq!(event0.event_index.0, 0); + assert_eq!(event0.amount.unwrap().0, 100); + assert_eq!(event0.address, address); + + let event1 = new_sequential_ledger_entry( + &location, + Some(300), + rune_id, + Some(0), + None, + None, + DbLedgerOperation::Receive, + &mut event_index, + ); + assert_eq!(event1.event_index.0, 1); + assert_eq!(event1.amount.unwrap().0, 300); + assert_eq!(event1.address, None); + + assert_eq!(event_index, 2); + } + } }