Files
bitcoin-indexer/components/runes/src/db/cache/utils.rs
Rafael Cárdenas 9e9eac81ea refactor: standardize block download pipeline across indexers (#463)
* zmq after chain tip

* some progress

* block pool advance

* runloop finished

* renames

* runs first time

* log levels

* start connecting runes

* compress block opt

* remove dead code

* remove dead code chainhook sdk

* remove ordhook dead code

* rollback install

* auto fmt

* clippy fixes

* fmt

* rollback chain tip

* api test

* api metrics fix

* rename

* standard logs

* ordinals start chain tip

* chain tips

* ci

* clippy

* fix tests

* remove dead code
2025-03-13 08:26:46 -06:00

937 lines
34 KiB
Rust

use std::collections::{HashMap, VecDeque};
use bitcoin::{Address, ScriptBuf};
use bitcoind::{try_info, try_warn, utils::Context};
use chainhook_types::bitcoin::TxIn;
use lru::LruCache;
use ordinals::RuneId;
use tokio_postgres::Transaction;
use super::{input_rune_balance::InputRuneBalance, transaction_location::TransactionLocation};
use crate::db::{
models::{
db_ledger_entry::DbLedgerEntry, db_ledger_operation::DbLedgerOperation, db_rune::DbRune,
},
pg_get_input_rune_balances,
};
/// Takes all transaction inputs and transforms them into rune balances to be allocated for operations. Looks inside an output LRU
/// cache and the DB when there are cache misses.
///
/// # Arguments
///
/// * `inputs` - Raw transaction inputs
/// * `block_output_cache` - Cache with output balances produced by the current block
/// * `output_cache` - LRU cache with output balances
/// * `db_tx` - DB transaction
/// * `ctx` - Context
pub async fn input_rune_balances_from_tx_inputs(
inputs: &Vec<TxIn>,
block_output_cache: &HashMap<(String, u32), HashMap<RuneId, Vec<InputRuneBalance>>>,
output_cache: &mut LruCache<(String, u32), HashMap<RuneId, Vec<InputRuneBalance>>>,
db_tx: &mut Transaction<'_>,
ctx: &Context,
) -> HashMap<RuneId, VecDeque<InputRuneBalance>> {
// Maps input index to all of its rune balances. Useful in order to keep rune inputs in order.
let mut indexed_input_runes = HashMap::new();
let mut cache_misses = vec![];
// Look in both current block output cache and in long term LRU cache.
for (i, input) in inputs.iter().enumerate() {
let tx_id = input.previous_output.txid.hash[2..].to_string();
let vout = input.previous_output.vout;
let k = (tx_id.clone(), vout);
if let Some(map) = block_output_cache.get(&k) {
indexed_input_runes.insert(i as u32, map.clone());
} else if let Some(map) = output_cache.get(&k) {
indexed_input_runes.insert(i as u32, map.clone());
} else {
cache_misses.push((i as u32, tx_id, vout));
}
}
// Look for cache misses in database. We don't need to `flush` the DB cache here because we've already looked in the current
// block's output cache.
if !cache_misses.is_empty() {
let output_balances = pg_get_input_rune_balances(cache_misses, db_tx, ctx).await;
indexed_input_runes.extend(output_balances);
}
let mut final_input_runes: HashMap<RuneId, VecDeque<InputRuneBalance>> = HashMap::new();
let mut input_keys: Vec<u32> = indexed_input_runes.keys().copied().collect();
input_keys.sort();
for key in input_keys.iter() {
let input_value = indexed_input_runes.get(key).unwrap();
for (rune_id, vec) in input_value.iter() {
if let Some(rune) = final_input_runes.get_mut(rune_id) {
rune.extend(vec.clone());
} else {
final_input_runes.insert(*rune_id, VecDeque::from(vec.clone()));
}
}
}
final_input_runes
}
/// Moves data from the current block's output cache to the long-term LRU output cache. Clears the block output cache when done.
///
/// # Arguments
///
/// * `block_output_cache` - Block output cache
/// * `output_cache` - Output LRU cache
pub fn move_block_output_cache_to_output_cache(
block_output_cache: &mut HashMap<(String, u32), HashMap<RuneId, Vec<InputRuneBalance>>>,
output_cache: &mut LruCache<(String, u32), HashMap<RuneId, Vec<InputRuneBalance>>>,
) {
for (k, block_output_map) in block_output_cache.iter() {
if let Some(v) = output_cache.get_mut(k) {
for (rune_id, balances) in block_output_map.iter() {
if let Some(rune_balance) = v.get_mut(rune_id) {
rune_balance.extend(balances.clone());
} else {
v.insert(*rune_id, balances.clone());
}
}
} else {
output_cache.push(k.clone(), block_output_map.clone());
}
}
block_output_cache.clear();
}
/// Creates a new ledger entry while incrementing the `next_event_index`.
pub fn new_sequential_ledger_entry(
location: &TransactionLocation,
amount: Option<u128>,
rune_id: RuneId,
output: Option<u32>,
address: Option<&String>,
receiver_address: Option<&String>,
operation: DbLedgerOperation,
next_event_index: &mut u32,
) -> DbLedgerEntry {
let entry = DbLedgerEntry::from_values(
amount,
rune_id,
&location.block_hash,
location.block_height,
location.tx_index,
*next_event_index,
&location.tx_id,
output,
address,
receiver_address,
operation,
location.timestamp,
);
*next_event_index += 1;
entry
}
/// 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<u32>,
rune_id: &RuneId,
input_balances: &mut VecDeque<InputRuneBalance>,
outputs: &HashMap<u32, ScriptBuf>,
amount: u128,
next_event_index: &mut u32,
ctx: &Context,
) -> Vec<DbLedgerEntry> {
let mut results = vec![];
// Who is this balance going to?
let receiver_address = if let Some(output) = output {
match outputs.get(&output) {
Some(script) => match Address::from_script(script, location.network) {
Ok(address) => Some(address.to_string()),
Err(e) => {
try_warn!(
ctx,
"Unable to decode address for output {}, {} {}",
output,
e,
location
);
None
}
},
None => {
try_info!(
ctx,
"Attempted move to non-eligible output {}, runes will be burnt {}",
output,
location
);
None
}
}
} else {
None
};
let operation = if receiver_address.is_some() {
DbLedgerOperation::Send
} else {
DbLedgerOperation::Burn
};
// Gather balance to be received by taking it from the available inputs until the amount to move is satisfied.
let mut total_sent = 0;
let mut senders = vec![];
loop {
// Do we still have input balance left to move?
let Some(input_bal) = input_balances.pop_front() else {
break;
};
// Select the correct move amount.
let balance_taken = if amount == 0 {
input_bal.amount
} else {
input_bal.amount.min(amount - total_sent)
};
total_sent += balance_taken;
// If the input balance came from an address, add to `Send` operations.
if let Some(sender_address) = input_bal.address.clone() {
senders.push((balance_taken, sender_address));
}
// 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 {
input_balances.push_front(InputRuneBalance {
address: input_bal.address,
amount: input_bal.amount - balance_taken,
});
break;
}
// Have we finished moving balance?
if total_sent == amount {
break;
}
}
// Add the "receive" entry, if applicable.
if receiver_address.is_some() && total_sent > 0 {
results.push(new_sequential_ledger_entry(
location,
Some(total_sent),
*rune_id,
output,
receiver_address.as_ref(),
None,
DbLedgerOperation::Receive,
next_event_index,
));
try_info!(
ctx,
"{} {} ({}) {} {}",
DbLedgerOperation::Receive,
rune_id,
total_sent,
receiver_address.as_ref().unwrap(),
location
);
}
// Add the "send"/"burn" entries.
for (balance_taken, sender_address) in senders.iter() {
results.push(new_sequential_ledger_entry(
location,
Some(*balance_taken),
*rune_id,
output,
Some(sender_address),
receiver_address.as_ref(),
operation.clone(),
next_event_index,
));
try_info!(
ctx,
"{} {} ({}) {} -> {:?} {}",
operation,
rune_id,
balance_taken,
sender_address,
receiver_address,
location
);
}
results
}
/// Determines if a mint is valid depending on the rune's mint terms.
pub fn is_rune_mintable(
db_rune: &DbRune,
total_mints: u128,
location: &TransactionLocation,
) -> bool {
if db_rune.cenotaph {
return false;
}
if db_rune.terms_amount.is_none() {
return false;
}
if let Some(terms_cap) = db_rune.terms_cap {
if total_mints >= terms_cap.0 {
return false;
}
}
if let Some(terms_height_start) = db_rune.terms_height_start {
if location.block_height < terms_height_start.0 {
return false;
}
}
if let Some(terms_height_end) = db_rune.terms_height_end {
if location.block_height > terms_height_end.0 {
return false;
}
}
if let Some(terms_offset_start) = db_rune.terms_offset_start {
if location.block_height < db_rune.block_height.0 + terms_offset_start.0 {
return false;
}
}
if let Some(terms_offset_end) = db_rune.terms_offset_end {
if location.block_height > db_rune.block_height.0 + terms_offset_end.0 {
return false;
}
}
true
}
#[cfg(test)]
mod test {
mod move_balance {
use std::collections::{HashMap, VecDeque};
use bitcoin::ScriptBuf;
use bitcoind::utils::Context;
use maplit::hashmap;
use ordinals::RuneId;
use crate::db::{
cache::{
input_rune_balance::InputRuneBalance, transaction_location::TransactionLocation,
utils::move_rune_balance_to_output,
},
models::db_ledger_operation::DbLedgerOperation,
};
fn dummy_eligible_output() -> HashMap<u32, ScriptBuf> {
hashmap! {
0u32 => ScriptBuf::from_hex(
"5120388dfba1b0069bbb0ad5eef62c1a94c46e91a3454accf40bf34b80f75e2708db",
)
.unwrap()
}
}
#[test]
fn ledger_writes_receive_before_send() {
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 mut input2 = InputRuneBalance::dummy();
input2.address(None).amount(1000);
available_inputs.push_back(input2);
let eligible_outputs = dummy_eligible_output();
let mut next_event_index = 0;
let results = move_rune_balance_to_output(
&TransactionLocation::dummy(),
Some(0),
&RuneId::new(840000, 25).unwrap(),
&mut available_inputs,
&eligible_outputs,
0,
&mut next_event_index,
&Context::empty(),
);
let receive = results.get(0).unwrap();
assert_eq!(receive.event_index.0, 0u32);
assert_eq!(receive.operation, DbLedgerOperation::Receive);
assert_eq!(receive.amount.unwrap().0, 2000u128);
let send = results.get(1).unwrap();
assert_eq!(send.event_index.0, 1u32);
assert_eq!(send.operation, DbLedgerOperation::Send);
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);
}
}
mod mint_validation {
use chainhook_postgres::types::{PgNumericU128, PgNumericU64};
use test_case::test_case;
use crate::db::{
cache::{transaction_location::TransactionLocation, utils::is_rune_mintable},
models::db_rune::DbRune,
};
#[test_case(840000 => false; "early block")]
#[test_case(840500 => false; "late block")]
#[test_case(840150 => true; "block in window")]
#[test_case(840100 => true; "first block")]
#[test_case(840200 => true; "last block")]
fn mint_block_height_terms_are_validated(block_height: u64) -> bool {
let mut rune = DbRune::factory();
rune.terms_height_start(Some(PgNumericU64(840100)));
rune.terms_height_end(Some(PgNumericU64(840200)));
let mut location = TransactionLocation::dummy();
location.block_height(block_height);
is_rune_mintable(&rune, 0, &location)
}
#[test_case(840000 => false; "early block")]
#[test_case(840500 => false; "late block")]
#[test_case(840150 => true; "block in window")]
#[test_case(840100 => true; "first block")]
#[test_case(840200 => true; "last block")]
fn mint_block_offset_terms_are_validated(block_height: u64) -> bool {
let mut rune = DbRune::factory();
rune.terms_offset_start(Some(PgNumericU64(100)));
rune.terms_offset_end(Some(PgNumericU64(200)));
let mut location = TransactionLocation::dummy();
location.block_height(block_height);
is_rune_mintable(&rune, 0, &location)
}
#[test_case(0 => true; "first mint")]
#[test_case(49 => true; "last mint")]
#[test_case(50 => false; "out of range")]
fn mint_cap_is_validated(cap: u128) -> bool {
let mut rune = DbRune::factory();
rune.terms_cap(Some(PgNumericU128(50)));
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);
}
}
mod input_balances {
use std::num::NonZeroUsize;
use bitcoind::utils::Context;
use chainhook_types::{
bitcoin::{OutPoint, TxIn},
TransactionIdentifier,
};
use lru::LruCache;
use maplit::hashmap;
use ordinals::RuneId;
use crate::db::{
cache::{
input_rune_balance::InputRuneBalance, utils::input_rune_balances_from_tx_inputs,
},
models::{db_ledger_entry::DbLedgerEntry, db_ledger_operation::DbLedgerOperation},
pg_insert_ledger_entries, pg_test_client, pg_test_roll_back_migrations,
};
#[tokio::test]
async fn from_block_cache() {
let inputs = vec![TxIn {
previous_output: OutPoint {
txid: TransactionIdentifier {
hash: "0x045fe33f1174d6a72084e751735a89746a259c6d3e418b65c03ec0740f924c7b"
.to_string(),
},
vout: 1,
value: 100,
block_height: 840000,
},
script_sig: "".to_string(),
sequence: 0,
witness: vec![],
}];
let rune_id = RuneId::new(840000, 25).unwrap();
let block_output_cache = hashmap! {
("045fe33f1174d6a72084e751735a89746a259c6d3e418b65c03ec0740f924c7b"
.to_string(), 1) => hashmap! {
rune_id => vec![InputRuneBalance { address: None, amount: 2000 }]
}
};
let mut output_cache = LruCache::new(NonZeroUsize::new(1).unwrap());
let ctx = Context::empty();
let mut pg_client = pg_test_client(true, &ctx).await;
let mut db_tx = pg_client.transaction().await.unwrap();
let results = input_rune_balances_from_tx_inputs(
&inputs,
&block_output_cache,
&mut output_cache,
&mut db_tx,
&ctx,
)
.await;
let _ = db_tx.rollback().await;
pg_test_roll_back_migrations(&mut pg_client, &ctx).await;
assert_eq!(results.len(), 1);
let rune_results = results.get(&rune_id).unwrap();
assert_eq!(rune_results.len(), 1);
let input_bal = rune_results.get(0).unwrap();
assert_eq!(input_bal.address, None);
assert_eq!(input_bal.amount, 2000);
}
#[tokio::test]
async fn from_lru_cache() {
let inputs = vec![TxIn {
previous_output: OutPoint {
txid: TransactionIdentifier {
hash: "0x045fe33f1174d6a72084e751735a89746a259c6d3e418b65c03ec0740f924c7b"
.to_string(),
},
vout: 1,
value: 100,
block_height: 840000,
},
script_sig: "".to_string(),
sequence: 0,
witness: vec![],
}];
let rune_id = RuneId::new(840000, 25).unwrap();
let block_output_cache = hashmap! {};
let mut output_cache = LruCache::new(NonZeroUsize::new(1).unwrap());
output_cache.put(
(
"045fe33f1174d6a72084e751735a89746a259c6d3e418b65c03ec0740f924c7b".to_string(),
1,
),
hashmap! {
rune_id => vec![InputRuneBalance { address: None, amount: 2000 }]
},
);
let ctx = Context::empty();
let mut pg_client = pg_test_client(true, &ctx).await;
let mut db_tx = pg_client.transaction().await.unwrap();
let results = input_rune_balances_from_tx_inputs(
&inputs,
&block_output_cache,
&mut output_cache,
&mut db_tx,
&ctx,
)
.await;
let _ = db_tx.rollback().await;
pg_test_roll_back_migrations(&mut pg_client, &ctx).await;
assert_eq!(results.len(), 1);
let rune_results = results.get(&rune_id).unwrap();
assert_eq!(rune_results.len(), 1);
let input_bal = rune_results.get(0).unwrap();
assert_eq!(input_bal.address, None);
assert_eq!(input_bal.amount, 2000);
}
#[tokio::test]
async fn from_db() {
let inputs = vec![TxIn {
previous_output: OutPoint {
txid: TransactionIdentifier {
hash: "0x045fe33f1174d6a72084e751735a89746a259c6d3e418b65c03ec0740f924c7b"
.to_string(),
},
vout: 1,
value: 100,
block_height: 840000,
},
script_sig: "".to_string(),
sequence: 0,
witness: vec![],
}];
let rune_id = RuneId::new(840000, 25).unwrap();
let block_output_cache = hashmap! {};
let mut output_cache = LruCache::new(NonZeroUsize::new(1).unwrap());
let ctx = Context::empty();
let mut pg_client = pg_test_client(true, &ctx).await;
let mut db_tx = pg_client.transaction().await.unwrap();
let entry = DbLedgerEntry::from_values(
Some(2000),
rune_id,
&"0x0000000000000000000044642cc1f64c22579d46a2a149ef2a51f9c98cb622e1".to_string(),
840000,
0,
0,
&"0x045fe33f1174d6a72084e751735a89746a259c6d3e418b65c03ec0740f924c7b".to_string(),
Some(1),
None,
None,
DbLedgerOperation::Receive,
0,
);
let _ = pg_insert_ledger_entries(&vec![entry], &mut db_tx, &ctx).await;
let results = input_rune_balances_from_tx_inputs(
&inputs,
&block_output_cache,
&mut output_cache,
&mut db_tx,
&ctx,
)
.await;
let _ = db_tx.rollback().await;
pg_test_roll_back_migrations(&mut pg_client, &ctx).await;
assert_eq!(results.len(), 1);
let rune_results = results.get(&rune_id).unwrap();
assert_eq!(rune_results.len(), 1);
let input_bal = rune_results.get(0).unwrap();
assert_eq!(input_bal.address, None);
assert_eq!(input_bal.amount, 2000);
}
#[tokio::test]
async fn inputs_without_balances() {
let inputs = vec![TxIn {
previous_output: OutPoint {
txid: TransactionIdentifier {
hash: "0x045fe33f1174d6a72084e751735a89746a259c6d3e418b65c03ec0740f924c7b"
.to_string(),
},
vout: 1,
value: 100,
block_height: 840000,
},
script_sig: "".to_string(),
sequence: 0,
witness: vec![],
}];
let block_output_cache = hashmap! {};
let mut output_cache = LruCache::new(NonZeroUsize::new(1).unwrap());
let ctx = Context::empty();
let mut pg_client = pg_test_client(true, &ctx).await;
let mut db_tx = pg_client.transaction().await.unwrap();
let results = input_rune_balances_from_tx_inputs(
&inputs,
&block_output_cache,
&mut output_cache,
&mut db_tx,
&ctx,
)
.await;
let _ = db_tx.rollback().await;
pg_test_roll_back_migrations(&mut pg_client, &ctx).await;
assert_eq!(results.len(), 0);
}
}
mod cache_move {
use std::num::NonZeroUsize;
use lru::LruCache;
use maplit::hashmap;
use ordinals::RuneId;
use crate::db::cache::{
input_rune_balance::InputRuneBalance, utils::move_block_output_cache_to_output_cache,
};
#[test]
fn moves_to_lru_output_cache_and_clears() {
let rune_id = RuneId::new(840000, 25).unwrap();
let mut block_output_cache = hashmap! {
("045fe33f1174d6a72084e751735a89746a259c6d3e418b65c03ec0740f924c7b"
.to_string(), 1) => hashmap! {
rune_id => vec![InputRuneBalance { address: None, amount: 2000 }]
}
};
let mut output_cache = LruCache::new(NonZeroUsize::new(1).unwrap());
move_block_output_cache_to_output_cache(&mut block_output_cache, &mut output_cache);
let moved_val = output_cache
.get(&(
"045fe33f1174d6a72084e751735a89746a259c6d3e418b65c03ec0740f924c7b".to_string(),
1,
))
.unwrap();
assert_eq!(moved_val.len(), 1);
let balances = moved_val.get(&rune_id).unwrap();
assert_eq!(balances.len(), 1);
let balance = balances.get(0).unwrap();
assert_eq!(balance.address, None);
assert_eq!(balance.amount, 2000);
assert_eq!(block_output_cache.len(), 0);
}
}
}