use std::{
collections::{hash_map::Entry, HashMap},
time::Duration,
};
use alloy::{
json_abi::{Event, Function},
primitives::{Selector, B256},
};
use reqwest::Client;
use serde::Deserialize;
use tokio::{sync::RwLock, time::sleep};
#[derive(Debug, Deserialize)]
struct FourByteResponse {
results: Vec<FunctionSignature>,
}
#[derive(Debug, Deserialize)]
struct FunctionSignature {
text_signature: String,
}
#[derive(Debug, Deserialize)]
struct EventSignatureResponse {
results: Vec<EventSignature>,
}
#[derive(Debug, Deserialize)]
struct EventSignature {
text_signature: String,
}
pub struct SignaturesIdentifier {
client: Client,
function_cache: RwLock<HashMap<Selector, Function>>,
event_cache: RwLock<HashMap<B256, Event>>,
offline: bool,
}
impl SignaturesIdentifier {
pub fn new(offline: bool) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()?;
Ok(Self {
client,
function_cache: RwLock::new(HashMap::new()),
event_cache: RwLock::new(HashMap::new()),
offline,
})
}
pub async fn identify_function(&self, selector: Selector) -> Option<Function> {
{
let lock_guard = self.function_cache.read().await;
if let Some(func) = lock_guard.get(&selector) {
return Some(func.clone());
}
}
if self.offline {
return None;
}
let hex_selector = format!("0x{}", hex::encode(selector.as_slice()));
let url = format!(
"https://www.4byte.directory/api/v1/signatures/?hex_signature={}",
hex_selector
);
match self.fetch_with_retry(&url).await {
Ok(response) => {
if let Ok(data) = response
.json::<FourByteResponse>()
.await
{
if let Some(sig) = data.results.first() {
if let Ok(function) = Function::parse(&sig.text_signature) {
let mut lock_guard = self.function_cache.write().await;
lock_guard.insert(selector, function.clone());
return Some(function);
}
}
}
}
Err(e) => {
tracing::warn!(target: "traces::signatures", "failed to fetch function signature: {}", e);
}
}
None
}
pub async fn identify_event(&self, topic: B256) -> Option<Event> {
{
let cache_guard = self.event_cache.read().await;
if let Some(event) = cache_guard.get(&topic) {
return Some(event.clone());
}
}
if self.offline {
return None;
}
let hex_topic = format!("0x{}", hex::encode(topic.as_slice()));
let url = format!(
"https://www.4byte.directory/api/v1/event-signatures/?hex_signature={}",
hex_topic
);
match self.fetch_with_retry(&url).await {
Ok(response) => {
if let Ok(data) = response
.json::<EventSignatureResponse>()
.await
{
if let Some(sig) = data.results.first() {
if let Ok(event) = Event::parse(&sig.text_signature) {
let mut cache_guard = self.event_cache.write().await;
cache_guard.insert(topic, event.clone());
return Some(event);
}
}
}
}
Err(e) => {
tracing::warn!(target: "traces::signatures", "failed to fetch event signature: {}", e);
}
}
None
}
pub async fn identify_batch(
&self,
selectors: &[SelectorKind],
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if self.offline {
return Ok(());
}
let functions: Vec<_> = selectors
.iter()
.filter_map(|s| match s {
SelectorKind::Function(sel) => Some(*sel),
_ => None,
})
.collect();
let events: Vec<_> = selectors
.iter()
.filter_map(|s| match s {
SelectorKind::Event(topic) => Some(*topic),
_ => None,
})
.collect();
{
let mut cache_guard = self.function_cache.write().await;
for selector in functions {
if let Entry::Vacant(e) = cache_guard.entry(selector) {
if let Some(func) = self.identify_function(selector).await {
e.insert(func);
}
sleep(Duration::from_millis(100)).await;
}
}
}
{
let mut cache_guard = self.event_cache.write().await;
for topic in events {
if let Entry::Vacant(e) = cache_guard.entry(topic) {
if let Some(event) = self.identify_event(topic).await {
e.insert(event);
}
sleep(Duration::from_millis(100)).await;
}
}
}
Ok(())
}
async fn fetch_with_retry(&self, url: &str) -> Result<reqwest::Response, reqwest::Error> {
let mut retries = 3;
loop {
match self.client.get(url).send().await {
Ok(response) => {
if response.status().is_success() {
return Ok(response);
} else if response.status().as_u16() == 429 && retries > 0 {
sleep(Duration::from_secs(1)).await;
retries -= 1;
continue;
} else {
return Ok(response);
}
}
Err(_e) if retries > 0 => {
sleep(Duration::from_millis(500)).await;
retries -= 1;
continue;
}
Err(e) => return Err(e),
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SelectorKind {
Function(Selector),
Event(B256),
}