use sqlx::{
postgres::PgArguments,
query::{Query, QueryAs},
Postgres, Row,
};
use zksync_db_connection::{connection::Connection, error::DalResult, instrument::InstrumentExt};
use zksync_types::{
api::{GetLogsFilter, Log},
Address, L2BlockNumber, H256,
};
use crate::{models::storage_event::StorageWeb3Log, Core};
#[derive(Debug)]
pub struct EventsWeb3Dal<'a, 'c> {
pub(crate) storage: &'a mut Connection<'c, Core>,
}
impl EventsWeb3Dal<'_, '_> {
pub async fn get_log_block_number(
&mut self,
filter: &GetLogsFilter,
offset: usize,
) -> DalResult<Option<L2BlockNumber>> {
let (where_sql, arg_index) = self.build_get_logs_where_clause(filter);
let query = format!(
r#"
SELECT miniblock_number
FROM events
WHERE {}
ORDER BY miniblock_number ASC, event_index_in_block ASC
LIMIT 1 OFFSET ${}
"#,
where_sql, arg_index
);
let mut query = sqlx::query(&query);
query = Self::bind_params_for_optional_filter_query(
query,
filter.addresses.iter().map(Address::as_bytes).collect(),
);
for (_, topics) in &filter.topics {
query = Self::bind_params_for_optional_filter_query(
query,
topics.iter().map(H256::as_bytes).collect(),
);
}
query = query.bind(offset as i32);
let log = query
.instrument("get_log_block_number")
.report_latency()
.with_arg("filter", filter)
.with_arg("offset", &offset)
.fetch_optional(self.storage)
.await?;
Ok(log.map(|row| L2BlockNumber(row.get::<i64, _>("miniblock_number") as u32)))
}
#[allow(clippy::type_complexity)]
pub async fn get_logs(&mut self, filter: GetLogsFilter, limit: usize) -> DalResult<Vec<Log>> {
let (where_sql, arg_index) = self.build_get_logs_where_clause(&filter);
let query = format!(
r#"
WITH events_select AS (
SELECT
address, topic1, topic2, topic3, topic4, value,
miniblock_number, tx_hash, tx_index_in_block,
event_index_in_block, event_index_in_tx
FROM events
WHERE {}
ORDER BY miniblock_number ASC, event_index_in_block ASC
LIMIT ${}
)
SELECT miniblocks.hash as "block_hash", miniblocks.l1_batch_number as "l1_batch_number",
miniblocks.timestamp as block_timestamp, events_select.*
FROM events_select
INNER JOIN miniblocks ON events_select.miniblock_number = miniblocks.number
ORDER BY miniblock_number ASC, event_index_in_block ASC
"#,
where_sql, arg_index
);
let mut query = sqlx::query_as(&query);
query = Self::bind_params_for_optional_filter_query_as(
query,
filter.addresses.iter().map(Address::as_bytes).collect(),
);
for (_, topics) in &filter.topics {
query = Self::bind_params_for_optional_filter_query_as(
query,
topics.iter().map(H256::as_bytes).collect(),
);
}
query = query.bind(limit as i32);
let db_logs: Vec<StorageWeb3Log> = query
.instrument("get_logs")
.report_latency()
.with_arg("filter", &filter)
.with_arg("limit", &limit)
.fetch_all(self.storage)
.await?;
let logs = db_logs.into_iter().map(Into::into).collect();
Ok(logs)
}
fn build_get_logs_where_clause(&self, filter: &GetLogsFilter) -> (String, u8) {
let mut arg_index = 1;
let mut where_sql = format!("(miniblock_number >= {})", filter.from_block.0);
where_sql += &format!(" AND (miniblock_number <= {})", filter.to_block.0);
if let Some(filter_sql) =
Self::build_sql_filter(filter.addresses.len() as u32, "address", arg_index)
{
where_sql += &filter_sql;
arg_index += 1;
}
for (topic_index, topics) in filter.topics.iter() {
if let Some(filter_sql) = Self::build_sql_filter(
topics.len() as u32,
&format!("topic{}", topic_index),
arg_index,
) {
where_sql += &filter_sql;
arg_index += 1;
}
}
(where_sql, arg_index)
}
fn build_sql_filter(
number_of_entities: u32,
field_name: &str,
arg_index: u8,
) -> Option<String> {
match number_of_entities {
0 => None,
1 => Some(format!(" AND ({} = ${})", field_name, arg_index)),
_ => Some(format!(" AND ({} = ANY(${}))", field_name, arg_index)),
}
}
fn bind_params_for_optional_filter_query_as<'q, O>(
query: QueryAs<'q, Postgres, O, PgArguments>,
values: Vec<&'q [u8]>,
) -> QueryAs<'q, Postgres, O, PgArguments> {
match values.len() {
0 => query,
1 => query.bind(values[0]),
_ => query.bind(values),
}
}
fn bind_params_for_optional_filter_query<'q>(
query: Query<'q, Postgres, PgArguments>,
values: Vec<&'q [u8]>,
) -> Query<'q, Postgres, PgArguments> {
match values.len() {
0 => query,
1 => query.bind(values[0]),
_ => query.bind(values),
}
}
pub async fn get_all_logs(&mut self, from_block: L2BlockNumber) -> DalResult<Vec<Log>> {
let db_logs: Vec<StorageWeb3Log> = sqlx::query_as!(
StorageWeb3Log,
r#"
WITH
events_select AS (
SELECT
address,
topic1,
topic2,
topic3,
topic4,
value,
miniblock_number,
tx_hash,
tx_index_in_block,
event_index_in_block,
event_index_in_tx
FROM
events
WHERE
miniblock_number > $1
ORDER BY
miniblock_number ASC,
event_index_in_block ASC
)
SELECT
miniblocks.hash AS "block_hash?",
address AS "address!",
topic1 AS "topic1!",
topic2 AS "topic2!",
topic3 AS "topic3!",
topic4 AS "topic4!",
value AS "value!",
miniblock_number AS "miniblock_number!",
miniblocks.l1_batch_number AS "l1_batch_number?",
tx_hash AS "tx_hash!",
tx_index_in_block AS "tx_index_in_block!",
event_index_in_block AS "event_index_in_block!",
event_index_in_tx AS "event_index_in_tx!",
miniblocks.timestamp AS "block_timestamp"
FROM
events_select
INNER JOIN miniblocks ON events_select.miniblock_number = miniblocks.number
ORDER BY
miniblock_number ASC,
event_index_in_block ASC
"#,
i64::from(from_block.0)
)
.instrument("get_all_logs")
.with_arg("from_block", &from_block)
.fetch_all(self.storage)
.await?;
let logs = db_logs.into_iter().map(Into::into).collect();
Ok(logs)
}
}
#[cfg(test)]
mod tests {
use zksync_types::{Address, H256};
use super::*;
use crate::{ConnectionPool, Core};
#[tokio::test]
async fn test_build_get_logs_where_clause() {
let connection_pool = ConnectionPool::<Core>::test_pool().await;
let storage = &mut connection_pool.connection().await.unwrap();
let events_web3_dal = EventsWeb3Dal { storage };
let filter = GetLogsFilter {
from_block: L2BlockNumber(100),
to_block: L2BlockNumber(200),
addresses: vec![Address::from_low_u64_be(123)],
topics: vec![(0, vec![H256::from_low_u64_be(456)])],
};
let expected_sql = "(miniblock_number >= 100) AND (miniblock_number <= 200) AND (address = $1) AND (topic0 = $2)";
let expected_arg_index = 3;
let (actual_sql, actual_arg_index) = events_web3_dal.build_get_logs_where_clause(&filter);
assert_eq!(actual_sql, expected_sql);
assert_eq!(actual_arg_index, expected_arg_index);
}
#[tokio::test]
async fn test_build_get_logs_with_multiple_topics_where_clause() {
let connection_pool = ConnectionPool::<Core>::test_pool().await;
let storage = &mut connection_pool.connection().await.unwrap();
let events_web3_dal = EventsWeb3Dal { storage };
let filter = GetLogsFilter {
from_block: L2BlockNumber(10),
to_block: L2BlockNumber(400),
addresses: vec![
Address::from_low_u64_be(123),
Address::from_low_u64_be(1233),
],
topics: vec![
(
0,
vec![
H256::from_low_u64_be(456),
H256::from_low_u64_be(789),
H256::from_low_u64_be(101),
],
),
(2, vec![H256::from_low_u64_be(789)]),
],
};
let expected_sql = "(miniblock_number >= 10) AND (miniblock_number <= 400) AND (address = ANY($1)) AND (topic0 = ANY($2)) AND (topic2 = $3)";
let expected_arg_index = 4;
let (actual_sql, actual_arg_index) = events_web3_dal.build_get_logs_where_clause(&filter);
assert_eq!(actual_sql, expected_sql);
assert_eq!(actual_arg_index, expected_arg_index);
}
#[tokio::test]
async fn test_build_get_logs_with_no_address_where_clause() {
let connection_pool = ConnectionPool::<Core>::test_pool().await;
let storage = &mut connection_pool.connection().await.unwrap();
let events_web3_dal = EventsWeb3Dal { storage };
let filter = GetLogsFilter {
from_block: L2BlockNumber(10),
to_block: L2BlockNumber(400),
addresses: vec![],
topics: vec![(2, vec![H256::from_low_u64_be(789)])],
};
let expected_sql =
"(miniblock_number >= 10) AND (miniblock_number <= 400) AND (topic2 = $1)";
let expected_arg_index = 2;
let (actual_sql, actual_arg_index) = events_web3_dal.build_get_logs_where_clause(&filter);
assert_eq!(actual_sql, expected_sql);
assert_eq!(actual_arg_index, expected_arg_index);
}
}