use std::collections::HashMap;
use std::sync::LazyLock;
use alloy_dyn_abi::{DynSolType, DynSolValue};
use alloy_primitives::{Address, U256, keccak256};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecodedParam {
pub name: String,
pub sol_type: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenMeta {
pub symbol: String,
pub decimals: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecodedCalldata {
pub selector: String,
pub signature: String,
pub function_name: String,
pub params: Vec<DecodedParam>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<CalldataWarning>,
#[serde(default)]
pub abi_verified: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_meta: Option<TokenMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum CalldataWarning {
#[serde(rename = "unlimited_approval")]
UnlimitedApproval { spender: String },
#[serde(rename = "approval_for_all")]
ApprovalForAll { operator: String },
}
static KNOWN_SELECTORS: LazyLock<HashMap<[u8; 4], &'static str>> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert([0xa9, 0x05, 0x9c, 0xbb], "transfer(address,uint256)");
m.insert([0x09, 0x5e, 0xa7, 0xb3], "approve(address,uint256)");
m.insert(
[0x23, 0xb8, 0x72, 0xdd],
"transferFrom(address,address,uint256)",
);
m.insert(
[0x42, 0x84, 0x2e, 0x0e],
"safeTransferFrom(address,address,uint256)",
);
m.insert(
[0xb8, 0x8d, 0x4f, 0xde],
"safeTransferFrom(address,address,uint256,bytes)",
);
m.insert([0xa2, 0x2c, 0xb4, 0x65], "setApprovalForAll(address,bool)");
m.insert(
[0xf2, 0x42, 0x43, 0x2a],
"safeTransferFrom(address,address,uint256,uint256,bytes)",
);
m.insert(
[0x2e, 0xb2, 0xc2, 0xd6],
"safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)",
);
m
});
pub async fn decode_calldata(
client: &reqwest::Client,
selector_cache: &DashMap<[u8; 4], String>,
rpc_url: &str,
chain_id: Option<&str>,
to: Option<&str>,
calldata_hex: &str,
) -> Option<DecodedCalldata> {
let raw = hex::decode(calldata_hex.strip_prefix("0x").unwrap_or(calldata_hex)).ok()?;
if raw.len() < 4 {
return None;
}
let selector: [u8; 4] = raw[..4].try_into().ok()?;
let selector_hex = format!("0x{}", hex::encode(selector));
let params_bytes = &raw[4..];
let signature = if let Some(sig) = KNOWN_SELECTORS.get(&selector) {
sig.to_string()
} else if let Some(sig) = selector_cache.get(&selector) {
sig.clone()
} else {
match lookup_selector_remote(client, &selector_hex).await {
Some(sig) => {
selector_cache.insert(selector, sig.clone());
sig
}
None => return None,
}
};
let open = signature.find('(')?;
let close = signature.rfind(')')?;
let function_name = signature[..open].to_string();
let types_str = &signature[open + 1..close];
let param_types: Vec<&str> = if types_str.is_empty() {
vec![]
} else {
types_str.split(',').collect()
};
let tuple_type_str = format!("({})", types_str);
let tuple_type = DynSolType::parse(&tuple_type_str).ok()?;
let decoded = tuple_type.abi_decode_params(params_bytes).ok()?;
let values = match decoded {
DynSolValue::Tuple(vals) => vals,
single => vec![single],
};
let params: Vec<DecodedParam> = values
.into_iter()
.enumerate()
.map(|(i, val)| {
let sol_type = param_types.get(i).unwrap_or(&"unknown").to_string();
let value = format_sol_value(&val);
DecodedParam {
name: format!("param{}", i),
sol_type,
value,
}
})
.collect();
let warnings = detect_warnings(&function_name, ¶ms);
let mut decoded = DecodedCalldata {
selector: selector_hex,
signature,
function_name,
params,
warnings,
abi_verified: false,
token_meta: None,
};
if let Some(to) = to {
let chain_dec = chain_id.and_then(hex_to_decimal);
if let Some(chain_dec) = chain_dec.as_deref() {
apply_verified_abi(client, chain_dec, to, selector, &mut decoded).await;
}
if matches!(
decoded.function_name.as_str(),
"transfer" | "approve" | "transferFrom"
) {
decoded.token_meta = fetch_token_meta(client, rpc_url, to).await;
}
}
Some(decoded)
}
fn hex_to_decimal(hex: &str) -> Option<String> {
u64::from_str_radix(hex.trim_start_matches("0x"), 16)
.ok()
.map(|n| n.to_string())
}
async fn apply_verified_abi(
client: &reqwest::Client,
chain_dec: &str,
to: &str,
selector: [u8; 4],
decoded: &mut DecodedCalldata,
) {
let Some(abi) = fetch_sourcify_abi(client, chain_dec, to).await else {
return;
};
let Some(functions) = abi.as_array() else {
return;
};
for entry in functions {
if entry.get("type").and_then(|t| t.as_str()) != Some("function") {
continue;
}
let Some(name) = entry.get("name").and_then(|n| n.as_str()) else {
continue;
};
let inputs = entry.get("inputs").and_then(|i| i.as_array());
let canonical = format!("{name}({})", abi_input_types(inputs));
if keccak256(canonical.as_bytes())[..4] != selector {
continue;
}
if let Some(inputs) = inputs {
for (i, input) in inputs.iter().enumerate() {
if let (Some(p), Some(n)) = (
decoded.params.get_mut(i),
input.get("name").and_then(|n| n.as_str()),
) && !n.is_empty()
{
p.name = n.to_string();
}
}
}
decoded.abi_verified = true;
return;
}
}
fn abi_input_types(inputs: Option<&Vec<Value>>) -> String {
let Some(inputs) = inputs else {
return String::new();
};
inputs
.iter()
.map(|input| {
let ty = input.get("type").and_then(|t| t.as_str()).unwrap_or("");
if let Some(suffix) = ty.strip_prefix("tuple") {
let components = input.get("components").and_then(|c| c.as_array());
format!("({}){suffix}", abi_input_types(components))
} else {
ty.to_string()
}
})
.collect::<Vec<_>>()
.join(",")
}
async fn fetch_sourcify_abi(client: &reqwest::Client, chain_dec: &str, to: &str) -> Option<Value> {
let address = to.parse::<Address>().ok()?.to_checksum(None);
let timeout = std::time::Duration::from_secs(2);
for match_type in ["full_match", "partial_match"] {
let url = format!(
"https://repo.sourcify.dev/contracts/{match_type}/{chain_dec}/{address}/metadata.json"
);
let Ok(resp) = client.get(&url).timeout(timeout).send().await else {
continue;
};
if !resp.status().is_success() {
continue;
}
if let Ok(meta) = resp.json::<Value>().await
&& let Some(abi) = meta.get("output").and_then(|o| o.get("abi"))
{
return Some(abi.clone());
}
}
None
}
async fn fetch_token_meta(
client: &reqwest::Client,
rpc_url: &str,
token: &str,
) -> Option<TokenMeta> {
let (symbol_raw, decimals_raw) = tokio::join!(
eth_call(client, rpc_url, token, "0x95d89b41"),
eth_call(client, rpc_url, token, "0x313ce567"),
);
let symbol = decode_string_return(symbol_raw?.as_str())?;
let decimals = decode_u8_return(decimals_raw?.as_str())?;
Some(TokenMeta { symbol, decimals })
}
async fn eth_call(client: &reqwest::Client, rpc_url: &str, to: &str, data: &str) -> Option<String> {
let body = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "eth_call",
"params": [{ "to": to, "data": data }, "latest"],
});
let resp = client
.post(rpc_url)
.json(&body)
.timeout(std::time::Duration::from_secs(2))
.send()
.await
.ok()?;
let v: Value = resp.json().await.ok()?;
v.get("result")?.as_str().map(String::from)
}
fn decode_string_return(hex: &str) -> Option<String> {
let bytes = hex::decode(hex.strip_prefix("0x").unwrap_or(hex)).ok()?;
if let Ok(DynSolValue::String(s)) = DynSolType::String.abi_decode(&bytes) {
let s = s.trim().to_string();
if !s.is_empty() {
return Some(s);
}
}
if bytes.len() >= 32 {
let s: String = String::from_utf8_lossy(&bytes[..32])
.trim_matches(char::from(0))
.trim()
.to_string();
if !s.is_empty() {
return Some(s);
}
}
None
}
fn decode_u8_return(hex: &str) -> Option<u8> {
let bytes = hex::decode(hex.strip_prefix("0x").unwrap_or(hex)).ok()?;
bytes.last().copied()
}
async fn lookup_selector_remote(client: &reqwest::Client, selector_hex: &str) -> Option<String> {
let timeout = std::time::Duration::from_secs(2);
if let Some(sig) = lookup_sourcify_format(
client,
&format!(
"https://api.4byte.sourcify.dev/signature-database/v1/lookup?function={}&filter=true",
selector_hex
),
selector_hex,
timeout,
)
.await
{
return Some(sig);
}
if let Some(sig) = lookup_sourcify_format(
client,
&format!(
"https://api.openchain.xyz/signature-database/v1/lookup?function={}&filter=true",
selector_hex
),
selector_hex,
timeout,
)
.await
{
return Some(sig);
}
lookup_4byte(client, selector_hex, timeout).await
}
async fn lookup_sourcify_format(
client: &reqwest::Client,
url: &str,
selector_hex: &str,
timeout: std::time::Duration,
) -> Option<String> {
let resp = client.get(url).timeout(timeout).send().await.ok()?;
let body: serde_json::Value = resp.json().await.ok()?;
body.get("result")?
.get("function")?
.get(selector_hex)?
.as_array()?
.first()?
.get("name")?
.as_str()
.map(String::from)
}
async fn lookup_4byte(
client: &reqwest::Client,
selector_hex: &str,
timeout: std::time::Duration,
) -> Option<String> {
let url = format!(
"https://www.4byte.directory/api/v1/signatures/?hex_signature={}",
selector_hex
);
let resp = client.get(&url).timeout(timeout).send().await.ok()?;
let body: serde_json::Value = resp.json().await.ok()?;
body.get("results")?
.as_array()?
.first()?
.get("text_signature")?
.as_str()
.map(String::from)
}
fn format_sol_value(val: &DynSolValue) -> String {
match val {
DynSolValue::Address(addr) => format!("{addr}"),
DynSolValue::Uint(n, _) => n.to_string(),
DynSolValue::Int(n, _) => n.to_string(),
DynSolValue::Bool(b) => b.to_string(),
DynSolValue::Bytes(b) => format!("0x{}", hex::encode(b)),
DynSolValue::FixedBytes(w, _) => format!("0x{}", hex::encode(w.as_slice())),
DynSolValue::String(s) => s.clone(),
DynSolValue::Array(arr) | DynSolValue::FixedArray(arr) => {
let items: Vec<String> = arr.iter().map(format_sol_value).collect();
format!("[{}]", items.join(", "))
}
DynSolValue::Tuple(vals) => {
let items: Vec<String> = vals.iter().map(format_sol_value).collect();
format!("({})", items.join(", "))
}
other => format!("{:?}", other),
}
}
fn detect_warnings(function_name: &str, params: &[DecodedParam]) -> Vec<CalldataWarning> {
let mut warnings = Vec::new();
match function_name {
"approve" => {
if params.len() >= 2
&& let Ok(amount) = params[1].value.parse::<U256>()
&& amount == U256::MAX {
warnings.push(CalldataWarning::UnlimitedApproval {
spender: params[0].value.clone(),
});
}
}
"setApprovalForAll"
if params.len() >= 2 && params[1].value == "true" => {
warnings.push(CalldataWarning::ApprovalForAll {
operator: params[0].value.clone(),
});
}
_ => {}
}
warnings
}