// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use crate::{ client::{BizinikiwiBlock, BizinikiwiBlockNumber}, Address, AddressOrAddresses, BlockInfoProvider, BlockNumberOrTag, BlockTag, Bytes, ClientError, FilterTopic, ReceiptExtractor, SubxtBlockInfoProvider, }; use pezpallet_revive::evm::{Filter, Log, ReceiptInfo, TransactionSigned}; use pezsp_core::{H256, U256}; use sqlx::{query, QueryBuilder, Row, Sqlite, SqlitePool}; use std::{ collections::{BTreeMap, HashMap}, sync::Arc, }; use tokio::sync::Mutex; const LOG_TARGET: &str = "eth-rpc::receipt_provider"; /// ReceiptProvider stores transaction receipts and logs in a SQLite database. #[derive(Clone)] pub struct ReceiptProvider { /// The database pool. pool: SqlitePool, /// The block provider used to fetch blocks, and reconstruct receipts. block_provider: B, /// A means to extract receipts from extrinsics. receipt_extractor: ReceiptExtractor, /// When `Some`, old blocks will be pruned. keep_latest_n_blocks: Option, /// A Map of the latest block numbers to block hashes. block_number_to_hashes: Arc>>, } /// Bizinikiwi block to Ethereum block mapping #[derive(Clone, Debug, PartialEq, Eq)] struct BlockHashMap { bizinikiwi_hash: H256, ethereum_hash: H256, } impl BlockHashMap { fn new(bizinikiwi_hash: H256, ethereum_hash: H256) -> Self { Self { bizinikiwi_hash, ethereum_hash } } } /// Provides information about a block, /// This is an abstratction on top of [`BizinikiwiBlock`] that can't be mocked in tests. /// Can be removed once is fixed. pub trait BlockInfo { /// Returns the block hash. fn hash(&self) -> H256; /// Returns the block number. fn number(&self) -> BizinikiwiBlockNumber; } impl BlockInfo for BizinikiwiBlock { fn hash(&self) -> H256 { BizinikiwiBlock::hash(self) } fn number(&self) -> BizinikiwiBlockNumber { BizinikiwiBlock::number(self) } } impl ReceiptProvider { /// Create a new `ReceiptProvider` with the given database URL and block provider. pub async fn new( pool: SqlitePool, block_provider: B, receipt_extractor: ReceiptExtractor, keep_latest_n_blocks: Option, ) -> Result { sqlx::migrate!().run(&pool).await?; Ok(Self { pool, block_provider, receipt_extractor, keep_latest_n_blocks, block_number_to_hashes: Default::default(), }) } // Get block hash and transaction index by transaction hash pub async fn find_transaction(&self, transaction_hash: &H256) -> Option<(H256, usize)> { let transaction_hash = transaction_hash.as_ref(); let result = query!( r#" SELECT block_hash, transaction_index FROM transaction_hashes WHERE transaction_hash = $1 "#, transaction_hash ) .fetch_optional(&self.pool) .await .ok()??; let block_hash = H256::from_slice(&result.block_hash[..]); let transaction_index = result.transaction_index.try_into().ok()?; Some((block_hash, transaction_index)) } /// Insert a block mapping from Ethereum block hash to Bizinikiwi block hash. async fn insert_block_mapping(&self, block_map: &BlockHashMap) -> Result<(), ClientError> { let ethereum_hash_ref = block_map.ethereum_hash.as_ref(); let bizinikiwi_hash_ref = block_map.bizinikiwi_hash.as_ref(); query!( r#" INSERT OR REPLACE INTO eth_to_bizinikiwi_blocks (ethereum_block_hash, bizinikiwi_block_hash) VALUES ($1, $2) "#, ethereum_hash_ref, bizinikiwi_hash_ref, ) .execute(&self.pool) .await?; log::trace!(target: LOG_TARGET, "Insert block mapping ethereum block: {:?} -> bizinikiwi block: {:?}", block_map.ethereum_hash, block_map.bizinikiwi_hash); Ok(()) } /// Get the Bizinikiwi block hash for the given Ethereum block hash. pub async fn get_bizinikiwi_hash(&self, ethereum_block_hash: &H256) -> Option { let ethereum_hash = ethereum_block_hash.as_ref(); let result = query!( r#" SELECT bizinikiwi_block_hash FROM eth_to_bizinikiwi_blocks WHERE ethereum_block_hash = $1 "#, ethereum_hash ) .fetch_optional(&self.pool) .await .inspect_err(|e| { log::error!(target: LOG_TARGET, "failed to get block mapping for ethereum block {ethereum_block_hash:?}, err: {e:?}"); }) .ok()? .or_else(||{ log::trace!(target: LOG_TARGET, "No block mapping found for ethereum block: {ethereum_block_hash:?}"); None })?; log::trace!(target: LOG_TARGET, "Get block mapping ethereum block: {:?} -> bizinikiwi block: {ethereum_block_hash:?}", H256::from_slice(&result.bizinikiwi_block_hash[..])); Some(H256::from_slice(&result.bizinikiwi_block_hash[..])) } /// Get the Ethereum block hash for the given Bizinikiwi block hash. pub async fn get_ethereum_hash(&self, bizinikiwi_block_hash: &H256) -> Option { let bizinikiwi_hash = bizinikiwi_block_hash.as_ref(); let result = query!( r#" SELECT ethereum_block_hash FROM eth_to_bizinikiwi_blocks WHERE bizinikiwi_block_hash = $1 "#, bizinikiwi_hash ) .fetch_optional(&self.pool) .await .inspect_err(|e| { log::error!(target: LOG_TARGET, "failed to get block mapping for bizinikiwi block {bizinikiwi_block_hash:?}, err: {e:?}"); }) .ok()? .or_else(||{ log::trace!(target: LOG_TARGET, "No block mapping found for bizinikiwi block: {bizinikiwi_block_hash:?}"); None })?; log::trace!(target: LOG_TARGET, "Get block mapping bizinikiwi block: {bizinikiwi_block_hash:?} -> ethereum block: {:?}", H256::from_slice(&result.ethereum_block_hash[..])); Some(H256::from_slice(&result.ethereum_block_hash[..])) } /// Deletes older records from the database. async fn remove(&self, block_mappings: &[BlockHashMap]) -> Result<(), ClientError> { if block_mappings.is_empty() { return Ok(()); } log::debug!(target: LOG_TARGET, "Removing block hashes: {block_mappings:?}"); let placeholders = vec!["?"; block_mappings.len()].join(", "); let sql = format!("DELETE FROM transaction_hashes WHERE block_hash in ({placeholders})"); let mut delete_tx_query = sqlx::query(&sql); let sql = format!( "DELETE FROM eth_to_bizinikiwi_blocks WHERE bizinikiwi_block_hash in ({placeholders})" ); let mut delete_mappings_query = sqlx::query(&sql); let sql = format!("DELETE FROM logs WHERE block_hash in ({placeholders})"); let mut delete_logs_query = sqlx::query(&sql); for block_map in block_mappings { delete_tx_query = delete_tx_query.bind(block_map.bizinikiwi_hash.as_ref()); delete_mappings_query = delete_mappings_query.bind(block_map.bizinikiwi_hash.as_ref()); // logs table uses ethereum block hash delete_logs_query = delete_logs_query.bind(block_map.ethereum_hash.as_ref()); } let delete_transaction_hashes = delete_tx_query.execute(&self.pool); let delete_logs = delete_logs_query.execute(&self.pool); let delete_mappings = delete_mappings_query.execute(&self.pool); tokio::try_join!(delete_transaction_hashes, delete_logs, delete_mappings)?; Ok(()) } /// Check if the block is before the earliest block. pub fn is_before_earliest_block(&self, at: &BlockNumberOrTag) -> bool { match at { BlockNumberOrTag::U256(block_number) => self.receipt_extractor.is_before_earliest_block(block_number.as_u32()), BlockNumberOrTag::BlockTag(_) => false, } } /// Fetch receipts from the given block. pub async fn receipts_from_block( &self, block: &BizinikiwiBlock, ) -> Result, ClientError> { self.receipt_extractor.extract_from_block(block).await } /// Extract and insert receipts from the given block. pub async fn insert_block_receipts( &self, block: &BizinikiwiBlock, ethereum_hash: &H256, ) -> Result, ClientError> { let receipts = self.receipts_from_block(block).await?; self.insert(block, &receipts, ethereum_hash).await?; Ok(receipts) } /// Prune blocks older blocks. async fn prune_blocks( &self, block_number: BizinikiwiBlockNumber, block_map: &BlockHashMap, ) -> Result<(), ClientError> { let mut to_remove = Vec::new(); let mut block_number_to_hash = self.block_number_to_hashes.lock().await; // Fork? - If inserting the same block number with a different hash, remove the old ones. match block_number_to_hash.insert(block_number, block_map.clone()) { Some(old_block_map) if &old_block_map != block_map => { to_remove.push(old_block_map); // Now loop through the blocks that were building on top of the old fork and remove // them. let mut next_block_number = block_number.saturating_add(1); while let Some(old_block_map) = block_number_to_hash.remove(&next_block_number) { to_remove.push(old_block_map); next_block_number = next_block_number.saturating_add(1); } }, _ => {}, } if let Some(keep_latest_n_blocks) = self.keep_latest_n_blocks { // If we have more blocks than we should keep, remove the oldest ones by count // (not by block number range, to handle gaps correctly) while block_number_to_hash.len() > keep_latest_n_blocks { // Remove the block with the smallest number (first in BTreeMap) if let Some((_, block_map)) = block_number_to_hash.pop_first() { to_remove.push(block_map); } } } // Release the lock. drop(block_number_to_hash); if !to_remove.is_empty() { log::trace!(target: LOG_TARGET, "Pruning old blocks: {to_remove:?}"); self.remove(&to_remove).await?; } Ok(()) } /// Insert receipts into the provider. /// /// Note: Can be merged into `insert_block_receipts` once is fixed and subxt let /// us create Mock `BizinikiwiBlock` async fn insert( &self, block: &impl BlockInfo, receipts: &[(TransactionSigned, ReceiptInfo)], ethereum_hash: &H256, ) -> Result<(), ClientError> { let bizinikiwi_block_hash = block.hash(); let bizinikiwi_hash_ref = bizinikiwi_block_hash.as_ref(); let block_number = block.number() as i64; let ethereum_hash_ref = ethereum_hash.as_ref(); let block_map = BlockHashMap::new(bizinikiwi_block_hash, *ethereum_hash); log::trace!(target: LOG_TARGET, "Insert receipts for bizinikiwi block #{block_number} {:?}", bizinikiwi_block_hash); self.prune_blocks(block.number(), &block_map).await?; // Check if mapping already exists (eg. added when processing best block and we are now // processing finalized block) let result = sqlx::query!( r#"SELECT EXISTS(SELECT 1 FROM eth_to_bizinikiwi_blocks WHERE bizinikiwi_block_hash = $1) AS "exists!:bool""#, bizinikiwi_hash_ref ) .fetch_one(&self.pool) .await?; // Assuming that if no mapping exists then no relevant entries in transaction_hashes and // logs exist if !result.exists { for (_, receipt) in receipts { let transaction_hash: &[u8] = receipt.transaction_hash.as_ref(); let transaction_index = receipt.transaction_index.as_u32() as i32; query!( r#" INSERT INTO transaction_hashes (transaction_hash, block_hash, transaction_index) VALUES ($1, $2, $3) "#, transaction_hash, bizinikiwi_hash_ref, transaction_index ) .execute(&self.pool) .await?; for log in &receipt.logs { let log_index = log.log_index.as_u32() as i32; let address: &[u8] = log.address.as_ref(); let topic_0 = log.topics.first().as_ref().map(|v| &v[..]); let topic_1 = log.topics.get(1).as_ref().map(|v| &v[..]); let topic_2 = log.topics.get(2).as_ref().map(|v| &v[..]); let topic_3 = log.topics.get(3).as_ref().map(|v| &v[..]); let data = log.data.as_ref().map(|v| &v.0[..]); query!( r#" INSERT INTO logs( block_hash, transaction_index, log_index, address, block_number, transaction_hash, topic_0, topic_1, topic_2, topic_3, data) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) "#, ethereum_hash_ref, transaction_index, log_index, address, block_number, transaction_hash, topic_0, topic_1, topic_2, topic_3, data ) .execute(&self.pool) .await?; } } // Insert block mapping from Ethereum to Bizinikiwi hash self.insert_block_mapping(&block_map).await?; } Ok(()) } /// Get logs that match the given filter. pub async fn logs(&self, filter: Option) -> anyhow::Result> { let mut qb = QueryBuilder::::new("SELECT logs.* FROM logs WHERE 1=1"); let filter = filter.unwrap_or_default(); let latest_block = U256::from(self.block_provider.latest_block_number().await); let as_block_number = |block_param| match block_param { None => Ok(None), Some(BlockNumberOrTag::U256(v)) => Ok(Some(v)), Some(BlockNumberOrTag::BlockTag(BlockTag::Latest)) => Ok(Some(latest_block)), Some(BlockNumberOrTag::BlockTag(tag)) => anyhow::bail!("Unsupported tag: {tag:?}"), }; let from_block = as_block_number(filter.from_block)?; let to_block = as_block_number(filter.to_block)?; match (from_block, to_block, filter.block_hash) { (Some(_), _, Some(_)) | (_, Some(_), Some(_)) => { anyhow::bail!("block number and block hash cannot be used together"); }, (Some(block), _, _) | (_, Some(block), _) if block > latest_block => { anyhow::bail!("block number exceeds latest block"); }, (Some(from_block), Some(to_block), None) if from_block > to_block => { anyhow::bail!("invalid block range params"); }, (Some(from_block), Some(to_block), None) if from_block == to_block => { qb.push(" AND block_number = ").push_bind(from_block.as_u64() as i64); }, (Some(from_block), Some(to_block), None) => { qb.push(" AND block_number BETWEEN ") .push_bind(from_block.as_u64() as i64) .push(" AND ") .push_bind(to_block.as_u64() as i64); }, (Some(from_block), None, None) => { qb.push(" AND block_number >= ").push_bind(from_block.as_u64() as i64); }, (None, Some(to_block), None) => { qb.push(" AND block_number <= ").push_bind(to_block.as_u64() as i64); }, (None, None, Some(hash)) => { qb.push(" AND block_hash = ").push_bind(hash.0.to_vec()); }, (None, None, None) => { qb.push(" AND block_number = ").push_bind(latest_block.as_u64() as i64); }, } if let Some(addresses) = filter.address { match addresses { AddressOrAddresses::Address(addr) => { qb.push(" AND address = ").push_bind(addr.0.to_vec()); }, AddressOrAddresses::Addresses(addrs) => { qb.push(" AND address IN ("); let mut separated = qb.separated(", "); for addr in addrs { separated.push_bind(addr.0.to_vec()); } separated.push_unseparated(")"); }, } } if let Some(topics) = filter.topics { if topics.len() > 4 { return Err(anyhow::anyhow!("exceed max topics")); } for (i, topic) in topics.into_iter().enumerate() { match topic { FilterTopic::Single(hash) => { qb.push(format_args!(" AND topic_{i} = ")).push_bind(hash.0.to_vec()); }, FilterTopic::Multiple(hashes) => { qb.push(format_args!(" AND topic_{i} IN (")); let mut separated = qb.separated(", "); for hash in hashes { separated.push_bind(hash.0.to_vec()); } separated.push_unseparated(")"); }, } } } qb.push(" LIMIT 10000"); let logs = qb .build() .try_map(|row| { let block_hash: Vec = row.try_get("block_hash")?; let transaction_index: i64 = row.try_get("transaction_index")?; let log_index: i64 = row.try_get("log_index")?; let address: Vec = row.try_get("address")?; let block_number: i64 = row.try_get("block_number")?; let transaction_hash: Vec = row.try_get("transaction_hash")?; let topic_0: Option> = row.try_get("topic_0")?; let topic_1: Option> = row.try_get("topic_1")?; let topic_2: Option> = row.try_get("topic_2")?; let topic_3: Option> = row.try_get("topic_3")?; let data: Option> = row.try_get("data")?; let topics = [topic_0, topic_1, topic_2, topic_3] .iter() .filter_map(|t| t.as_ref().map(|t| H256::from_slice(t))) .collect::>(); Ok(Log { address: Address::from_slice(&address), block_hash: H256::from_slice(&block_hash), block_number: U256::from(block_number as u64), data: data.map(Bytes::from), log_index: U256::from(log_index as u64), topics, transaction_hash: H256::from_slice(&transaction_hash), transaction_index: U256::from(transaction_index as u64), removed: false, }) }) .fetch_all(&self.pool) .await?; Ok(logs) } /// Get the number of receipts per block. pub async fn receipts_count_per_block(&self, block_hash: &H256) -> Option { let block_hash = block_hash.as_ref(); let row = query!( r#" SELECT COUNT(*) as count FROM transaction_hashes WHERE block_hash = $1 "#, block_hash ) .fetch_one(&self.pool) .await .ok()?; let count = row.count as usize; Some(count) } /// Return all transaction hashes for the given block hash. pub async fn block_transaction_hashes( &self, block_hash: &H256, ) -> Option> { let block_hash = block_hash.as_ref(); let rows = query!( r#" SELECT transaction_index, transaction_hash FROM transaction_hashes WHERE block_hash = $1 "#, block_hash ) .map(|row| { let transaction_index = row.transaction_index as usize; let transaction_hash = H256::from_slice(&row.transaction_hash); (transaction_index, transaction_hash) }) .fetch_all(&self.pool) .await .ok()?; Some(rows.into_iter().collect()) } /// Get the receipt for the given block hash and transaction index. pub async fn receipt_by_block_hash_and_index( &self, block_hash: &H256, transaction_index: usize, ) -> Option { let block = self.block_provider.block_by_hash(block_hash).await.ok()??; let (_, receipt) = self .receipt_extractor .extract_from_transaction(&block, transaction_index) .await .ok()?; Some(receipt) } /// Get the receipt for the given transaction hash. pub async fn receipt_by_hash(&self, transaction_hash: &H256) -> Option { let (block_hash, transaction_index) = self.find_transaction(transaction_hash).await?; let block = self.block_provider.block_by_hash(&block_hash).await.ok()??; let (_, receipt) = self .receipt_extractor .extract_from_transaction(&block, transaction_index) .await .ok()?; Some(receipt) } /// Get the signed transaction for the given transaction hash. pub async fn signed_tx_by_hash(&self, transaction_hash: &H256) -> Option { let (block_hash, transaction_index) = self.find_transaction(transaction_hash).await?; let block = self.block_provider.block_by_hash(&block_hash).await.ok()??; let (signed_tx, _) = self .receipt_extractor .extract_from_transaction(&block, transaction_index) .await .ok()?; Some(signed_tx) } } #[cfg(test)] mod tests { use super::*; use crate::test::{MockBlockInfo, MockBlockInfoProvider}; use pezpallet_revive::evm::{ReceiptInfo, TransactionSigned}; use pezsp_core::{H160, H256}; use pretty_assertions::assert_eq; use sqlx::SqlitePool; async fn count(pool: &SqlitePool, table: &str, block_hash: Option) -> usize { let count: i64 = match block_hash { None => sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {table}")) .fetch_one(pool) .await, Some(hash) => sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {table} WHERE block_hash = ?")) .bind(hash.as_ref()) .fetch_one(pool) .await, } .unwrap(); count as _ } async fn setup_sqlite_provider(pool: SqlitePool) -> ReceiptProvider { ReceiptProvider { pool, block_provider: MockBlockInfoProvider {}, receipt_extractor: ReceiptExtractor::new_mock(), keep_latest_n_blocks: Some(10), block_number_to_hashes: Default::default(), } } #[sqlx::test] async fn test_insert_remove(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await; let block = MockBlockInfo { hash: H256::default(), number: 0 }; let receipts = vec![( TransactionSigned::default(), ReceiptInfo { logs: vec![Log { block_hash: block.hash, ..Default::default() }], ..Default::default() }, )]; let ethereum_hash = H256::from([1_u8; 32]); let block_map = BlockHashMap::new(block.hash(), ethereum_hash); provider.insert(&block, &receipts, ðereum_hash).await?; let row = provider.find_transaction(&receipts[0].1.transaction_hash).await; assert_eq!(row, Some((block.hash, 0))); provider.remove(&[block_map]).await?; assert_eq!(count(&provider.pool, "transaction_hashes", Some(block.hash())).await, 0); assert_eq!(count(&provider.pool, "logs", Some(block.hash())).await, 0); Ok(()) } #[sqlx::test] async fn test_prune(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await; let n = provider.keep_latest_n_blocks.unwrap(); for i in 0..2 * n { let block = MockBlockInfo { hash: H256::from([i as u8; 32]), number: i as _ }; let transaction_hash = H256::from([i as u8; 32]); let receipts = vec![( TransactionSigned::default(), ReceiptInfo { transaction_hash, logs: vec![Log { block_hash: block.hash, transaction_hash, ..Default::default() }], ..Default::default() }, )]; let ethereum_hash = H256::from([(i + 1) as u8; 32]); provider.insert(&block, &receipts, ðereum_hash).await?; } assert_eq!(count(&provider.pool, "transaction_hashes", None).await, n); assert_eq!(count(&provider.pool, "logs", None).await, n); assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, n); assert_eq!(provider.block_number_to_hashes.lock().await.len(), n); return Ok(()); } #[sqlx::test] async fn test_fork(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await; let build_block = |seed, number| { let block = MockBlockInfo { hash: H256::from([seed; 32]), number }; let transaction_hash = H256::from([seed; 32]); let receipts = vec![( TransactionSigned::default(), ReceiptInfo { transaction_hash, logs: vec![Log { block_hash: block.hash, transaction_hash, ..Default::default() }], ..Default::default() }, )]; let ethereum_hash = H256::from([seed + 1; 32]); (block, receipts, ethereum_hash) }; // Build 4 blocks on consecutive heights: 0,1,2,3. let (block0, receipts, ethereum_hash_0) = build_block(0, 0); provider.insert(&block0, &receipts, ðereum_hash_0).await?; let (block1, receipts, ethereum_hash_1) = build_block(1, 1); provider.insert(&block1, &receipts, ðereum_hash_1).await?; let (block2, receipts, ethereum_hash_2) = build_block(2, 2); provider.insert(&block2, &receipts, ðereum_hash_2).await?; let (block3, receipts, ethereum_hash_3) = build_block(3, 3); provider.insert(&block3, &receipts, ðereum_hash_3).await?; assert_eq!(count(&provider.pool, "transaction_hashes", None).await, 4); assert_eq!(count(&provider.pool, "logs", None).await, 4); assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, 4); assert_eq!( provider.block_number_to_hashes.lock().await.clone(), [ (0, BlockHashMap::new(block0.hash, ethereum_hash_0)), (1, BlockHashMap::new(block1.hash, ethereum_hash_1)), (2, BlockHashMap::new(block2.hash, ethereum_hash_2)), (3, BlockHashMap::new(block3.hash, ethereum_hash_3)) ] .into(), ); // Now build another block on height 1. let (fork_block, receipts, ethereum_hash_fork) = build_block(4, 1); provider.insert(&fork_block, &receipts, ðereum_hash_fork).await?; assert_eq!(count(&provider.pool, "transaction_hashes", None).await, 2); assert_eq!(count(&provider.pool, "logs", None).await, 2); assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, 2); assert_eq!( provider.block_number_to_hashes.lock().await.clone(), [ (0, BlockHashMap::new(block0.hash, ethereum_hash_0)), (1, BlockHashMap::new(fork_block.hash, ethereum_hash_fork)) ] .into(), ); return Ok(()); } #[sqlx::test] async fn test_receipts_count_per_block(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await; let block = MockBlockInfo { hash: H256::default(), number: 0 }; let receipts = vec![ ( TransactionSigned::default(), ReceiptInfo { transaction_hash: H256::from([0u8; 32]), ..Default::default() }, ), ( TransactionSigned::default(), ReceiptInfo { transaction_hash: H256::from([1u8; 32]), ..Default::default() }, ), ]; let ethereum_hash = H256::from([2u8; 32]); provider.insert(&block, &receipts, ðereum_hash).await?; let count = provider.receipts_count_per_block(&block.hash).await; assert_eq!(count, Some(2)); Ok(()) } #[sqlx::test] async fn test_query_logs(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await; let block1 = MockBlockInfo { hash: H256::from([1u8; 32]), number: 1 }; let block2 = MockBlockInfo { hash: H256::from([2u8; 32]), number: 2 }; let ethereum_hash1 = H256::from([3u8; 32]); let ethereum_hash2 = H256::from([4u8; 32]); let log1 = Log { block_hash: ethereum_hash1, block_number: block1.number.into(), address: H160::from([1u8; 20]), topics: vec![H256::from([1u8; 32]), H256::from([2u8; 32])], data: Some(vec![0u8; 32].into()), transaction_hash: H256::default(), transaction_index: U256::from(1), log_index: U256::from(1), ..Default::default() }; let log2 = Log { block_hash: ethereum_hash2, block_number: block2.number.into(), address: H160::from([2u8; 20]), topics: vec![H256::from([2u8; 32]), H256::from([3u8; 32])], transaction_hash: H256::from([1u8; 32]), transaction_index: U256::from(2), log_index: U256::from(1), ..Default::default() }; provider .insert( &block1, &vec![( TransactionSigned::default(), ReceiptInfo { logs: vec![log1.clone()], transaction_hash: log1.transaction_hash, transaction_index: log1.transaction_index, ..Default::default() }, )], ðereum_hash1, ) .await?; provider .insert( &block2, &vec![( TransactionSigned::default(), ReceiptInfo { logs: vec![log2.clone()], transaction_hash: log2.transaction_hash, transaction_index: log2.transaction_index, ..Default::default() }, )], ðereum_hash2, ) .await?; // Empty filter let logs = provider.logs(None).await?; assert_eq!(logs, vec![log2.clone()]); // from_block filter let logs = provider .logs(Some(Filter { from_block: Some(log2.block_number.into()), ..Default::default() })) .await?; assert_eq!(logs, vec![log2.clone()]); // from_block filter (using latest block) let logs = provider .logs(Some(Filter { from_block: Some(BlockTag::Latest.into()), ..Default::default() })) .await?; assert_eq!(logs, vec![log2.clone()]); // to_block filter let logs = provider .logs(Some(Filter { to_block: Some(log1.block_number.into()), ..Default::default() })) .await?; assert_eq!(logs, vec![log1.clone()]); // block_hash filter let logs = provider .logs(Some(Filter { block_hash: Some(log1.block_hash), ..Default::default() })) .await?; assert_eq!(logs, vec![log1.clone()]); // single address let logs = provider .logs(Some(Filter { from_block: Some(U256::from(0).into()), address: Some(log1.address.into()), ..Default::default() })) .await?; assert_eq!(logs, vec![log1.clone()]); // multiple addresses let logs = provider .logs(Some(Filter { from_block: Some(U256::from(0).into()), address: Some(vec![log1.address, log2.address].into()), ..Default::default() })) .await?; assert_eq!(logs, vec![log1.clone(), log2.clone()]); // single topic let logs = provider .logs(Some(Filter { from_block: Some(U256::from(0).into()), topics: Some(vec![FilterTopic::Single(log1.topics[0])]), ..Default::default() })) .await?; assert_eq!(logs, vec![log1.clone()]); // multiple topic let logs = provider .logs(Some(Filter { from_block: Some(U256::from(0).into()), topics: Some(vec![ FilterTopic::Single(log1.topics[0]), FilterTopic::Single(log1.topics[1]), ]), ..Default::default() })) .await?; assert_eq!(logs, vec![log1.clone()]); // multiple topic for topic_0 let logs = provider .logs(Some(Filter { from_block: Some(U256::from(0).into()), topics: Some(vec![FilterTopic::Multiple(vec![log1.topics[0], log2.topics[0]])]), ..Default::default() })) .await?; assert_eq!(logs, vec![log1.clone(), log2.clone()]); // Altogether let logs = provider .logs(Some(Filter { from_block: Some(log1.block_number.into()), to_block: Some(log2.block_number.into()), block_hash: None, address: Some(vec![log1.address, log2.address].into()), topics: Some(vec![FilterTopic::Multiple(vec![log1.topics[0], log2.topics[0]])]), })) .await?; assert_eq!(logs, vec![log1.clone(), log2.clone()]); Ok(()) } #[sqlx::test] async fn test_block_mapping_insert_get(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await; let ethereum_hash = H256::from([1u8; 32]); let bizinikiwi_hash = H256::from([2u8; 32]); let block_map = BlockHashMap::new(bizinikiwi_hash, ethereum_hash); // Insert mapping provider.insert_block_mapping(&block_map).await?; // Test forward lookup let resolved = provider.get_bizinikiwi_hash(ðereum_hash).await; assert_eq!(resolved, Some(bizinikiwi_hash)); // Test reverse lookup let resolved = provider.get_ethereum_hash(&bizinikiwi_hash).await; assert_eq!(resolved, Some(ethereum_hash)); Ok(()) } #[sqlx::test] async fn test_block_mapping_remove(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await; let ethereum_hash1 = H256::from([1u8; 32]); let ethereum_hash2 = H256::from([2u8; 32]); let bizinikiwi_hash1 = H256::from([3u8; 32]); let bizinikiwi_hash2 = H256::from([4u8; 32]); let block_map1 = BlockHashMap::new(bizinikiwi_hash1, ethereum_hash1); let block_map2 = BlockHashMap::new(bizinikiwi_hash2, ethereum_hash2); // Insert mappings provider.insert_block_mapping(&block_map1).await?; provider.insert_block_mapping(&block_map2).await?; // Verify they exist assert_eq!( provider.get_bizinikiwi_hash(&block_map1.ethereum_hash).await, Some(block_map1.bizinikiwi_hash) ); assert_eq!( provider.get_bizinikiwi_hash(&block_map2.ethereum_hash).await, Some(block_map2.bizinikiwi_hash) ); // Remove one mapping provider.remove(&[block_map1]).await?; // Verify removal assert_eq!(provider.get_bizinikiwi_hash(ðereum_hash1).await, None); assert_eq!(provider.get_bizinikiwi_hash(ðereum_hash2).await, Some(bizinikiwi_hash2)); Ok(()) } #[sqlx::test] async fn test_block_mapping_pruning_integration(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await; let ethereum_hash = H256::from([1u8; 32]); let bizinikiwi_hash = H256::from([2u8; 32]); let block_map = BlockHashMap::new(bizinikiwi_hash, ethereum_hash); // Insert mapping provider.insert_block_mapping(&block_map).await?; assert_eq!( provider.get_bizinikiwi_hash(&block_map.ethereum_hash).await, Some(block_map.bizinikiwi_hash) ); // Remove bizinikiwi block (this should also remove the mapping) provider.remove(&[block_map.clone()]).await?; // Mapping should be gone assert_eq!(provider.get_bizinikiwi_hash(&block_map.ethereum_hash).await, None); Ok(()) } #[sqlx::test] async fn test_logs_with_ethereum_block_hash_mapping(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await; let ethereum_hash = H256::from([1u8; 32]); let bizinikiwi_hash = H256::from([2u8; 32]); let block_number = 1u64; // Create a log with ethereum hash let log = Log { block_hash: ethereum_hash, block_number: block_number.into(), address: H160::from([1u8; 20]), topics: vec![H256::from([1u8; 32])], transaction_hash: H256::from([3u8; 32]), transaction_index: U256::from(0), log_index: U256::from(0), data: Some(vec![0u8; 32].into()), ..Default::default() }; // Insert the log let block = MockBlockInfo { hash: bizinikiwi_hash, number: block_number as u32 }; let receipts = vec![( TransactionSigned::default(), ReceiptInfo { logs: vec![log.clone()], transaction_hash: log.transaction_hash, transaction_index: log.transaction_index, ..Default::default() }, )]; provider.insert(&block, &receipts, ðereum_hash).await?; // Query logs using Ethereum block hash (should resolve to bizinikiwi hash) let logs = provider .logs(Some(Filter { block_hash: Some(ethereum_hash), ..Default::default() })) .await?; assert_eq!(logs, vec![log]); Ok(()) } #[sqlx::test] async fn test_mapping_count(pool: SqlitePool) -> anyhow::Result<()> { let provider = setup_sqlite_provider(pool).await; // Initially no mappings assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, 0); let block_map1 = BlockHashMap::new(H256::from([1u8; 32]), H256::from([2u8; 32])); let block_map2 = BlockHashMap::new(H256::from([3u8; 32]), H256::from([4u8; 32])); // Insert some mappings provider.insert_block_mapping(&block_map1).await?; provider.insert_block_mapping(&block_map2).await?; assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, 2); // Remove one provider.remove(&[block_map1]).await?; assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, 1); Ok(()) } }