pub mod erc20;
pub mod permit;
use alloy_primitives::{Address, B256};
use serde::Deserialize;
use crate::{
config::contracts::{IMPLEMENTATION_STORAGE_SLOT, OWNER_STORAGE_SLOT},
error::CowError,
};
#[derive(Debug, Clone)]
pub struct OnchainReader {
client: reqwest::Client,
rpc_url: String,
}
impl OnchainReader {
#[allow(clippy::shadow_reuse, reason = "builder pattern chains naturally shadow")]
fn build_client() -> reqwest::Client {
let builder = reqwest::Client::builder();
#[cfg(not(target_arch = "wasm32"))]
let builder = builder.timeout(std::time::Duration::from_secs(30));
builder.build().unwrap_or_default()
}
#[must_use]
pub fn new(rpc_url: impl Into<String>) -> Self {
Self { client: Self::build_client(), rpc_url: rpc_url.into() }
}
pub(crate) async fn eth_call(&self, to: Address, data: &[u8]) -> Result<Vec<u8>, CowError> {
let to_hex = format!("{to:#x}");
let data_hex = format!("0x{}", alloy_primitives::hex::encode(data));
let body = serde_json::json!({
"jsonrpc": "2.0",
"method": "eth_call",
"params": [{"to": to_hex, "data": data_hex}, "latest"],
"id": 1u32
});
let resp = self.client.post(&self.rpc_url).json(&body).send().await?;
if !resp.status().is_success() {
let code = i64::from(resp.status().as_u16());
let msg = resp.text().await.unwrap_or_else(|_e| String::new());
return Err(CowError::Rpc { code, message: msg });
}
let rpc: RpcResponse = resp.json().await?;
if let Some(err) = rpc.error {
return Err(CowError::Rpc { code: err.code, message: err.message });
}
let hex_str = rpc
.result
.ok_or_else(|| CowError::Rpc { code: -1, message: "missing result field".into() })?;
let hex_clean = hex_str.as_str().trim_start_matches("0x");
alloy_primitives::hex::decode(hex_clean)
.map_err(|e| CowError::Rpc { code: -1, message: format!("hex decode: {e}") })
}
pub(crate) async fn eth_get_storage_at(
&self,
address: Address,
slot: &str,
) -> Result<B256, CowError> {
let addr_hex = format!("{address:#x}");
let body = serde_json::json!({
"jsonrpc": "2.0",
"method": "eth_getStorageAt",
"params": [addr_hex, slot, "latest"],
"id": 1u32
});
let resp = self.client.post(&self.rpc_url).json(&body).send().await?;
if !resp.status().is_success() {
let code = i64::from(resp.status().as_u16());
let msg = resp.text().await.unwrap_or_else(|_e| String::new());
return Err(CowError::Rpc { code, message: msg });
}
let rpc: RpcResponse = resp.json().await?;
if let Some(err) = rpc.error {
return Err(CowError::Rpc { code: err.code, message: err.message });
}
let hex_str = rpc
.result
.ok_or_else(|| CowError::Rpc { code: -1, message: "missing result field".into() })?;
let hex_clean = hex_str.as_str().trim_start_matches("0x");
let bytes = alloy_primitives::hex::decode(hex_clean)
.map_err(|e| CowError::Rpc { code: -1, message: format!("hex decode: {e}") })?;
if bytes.len() < 32 {
return Err(CowError::Rpc {
code: -1,
message: format!("expected 32 bytes, got {}", bytes.len()),
});
}
Ok(B256::from_slice(&bytes[..32]))
}
pub async fn implementation_address(&self, proxy: Address) -> Result<Address, CowError> {
let slot_value = self.eth_get_storage_at(proxy, IMPLEMENTATION_STORAGE_SLOT).await?;
Ok(Address::from_slice(&slot_value[12..]))
}
pub async fn owner_address(&self, proxy: Address) -> Result<Address, CowError> {
let slot_value = self.eth_get_storage_at(proxy, OWNER_STORAGE_SLOT).await?;
Ok(Address::from_slice(&slot_value[12..]))
}
}
#[derive(Deserialize)]
struct RpcResponse {
result: Option<String>,
error: Option<RpcError>,
}
#[derive(Deserialize)]
struct RpcError {
code: i64,
message: String,
}
pub(crate) fn decode_u256(bytes: &[u8]) -> Result<alloy_primitives::U256, CowError> {
if bytes.len() < 32 {
return Err(CowError::Parse {
field: "uint256",
reason: format!("expected ≥ 32 bytes, got {}", bytes.len()),
});
}
let arr: [u8; 32] = bytes[..32]
.try_into()
.map_err(|_e| CowError::Parse { field: "uint256", reason: "slice conversion".into() })?;
Ok(alloy_primitives::U256::from_be_bytes(arr))
}
pub(crate) fn decode_u8(bytes: &[u8]) -> Result<u8, CowError> {
if bytes.len() < 32 {
return Err(CowError::Parse {
field: "uint8",
reason: format!("expected ≥ 32 bytes, got {}", bytes.len()),
});
}
Ok(bytes[31])
}
pub(crate) fn decode_string(bytes: &[u8]) -> Result<String, CowError> {
if bytes.len() < 64 {
return Err(CowError::Parse {
field: "string",
reason: format!("expected ≥ 64 bytes, got {}", bytes.len()),
});
}
let len_arr: [u8; 32] = bytes[32..64]
.try_into()
.map_err(|_e| CowError::Parse { field: "string", reason: "length slice".into() })?;
let len_u256 = alloy_primitives::U256::from_be_bytes(len_arr);
let len = usize::try_from(len_u256).map_err(|_e| CowError::Parse {
field: "string",
reason: "length overflows usize".into(),
})?;
if bytes.len() < 64 + len {
return Err(CowError::Parse {
field: "string",
reason: format!("truncated: need {} + 64 bytes, got {}", len, bytes.len()),
});
}
String::from_utf8(bytes[64..64 + len].to_vec())
.map_err(|e| CowError::Parse { field: "string", reason: e.to_string() })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_u256_roundtrip() {
let mut buf = [0u8; 32];
buf[31] = 42;
let v = decode_u256(&buf).unwrap();
assert_eq!(v, alloy_primitives::U256::from(42u64));
}
#[test]
fn decode_u256_too_short() {
let result = decode_u256(&[0u8; 16]);
assert!(result.is_err());
}
#[test]
fn decode_u8_roundtrip() {
let mut buf = [0u8; 32];
buf[31] = 18;
assert_eq!(decode_u8(&buf).unwrap(), 18u8);
}
#[test]
fn decode_u8_too_short() {
assert!(decode_u8(&[0u8; 10]).is_err());
}
#[test]
fn decode_string_roundtrip() {
let mut buf = vec![0u8; 96];
buf[31] = 32;
buf[63] = 4;
buf[64..68].copy_from_slice(b"WETH");
assert_eq!(decode_string(&buf).unwrap(), "WETH");
}
#[test]
fn decode_string_too_short() {
assert!(decode_string(&[0u8; 32]).is_err());
}
#[test]
fn decode_string_truncated() {
let mut buf = vec![0u8; 64];
buf[31] = 32;
buf[63] = 100; assert!(decode_string(&buf).is_err());
}
#[test]
fn onchain_reader_new() {
let reader = OnchainReader::new("https://example.com");
assert_eq!(reader.rpc_url, "https://example.com");
}
#[test]
fn decode_string_invalid_utf8() {
let mut buf = vec![0u8; 96];
buf[31] = 32; buf[63] = 2; buf[64] = 0xFF;
buf[65] = 0xFE;
assert!(decode_string(&buf).is_err());
}
#[test]
fn decode_u256_large_value() {
let buf = [0xFFu8; 32];
let v = decode_u256(&buf).unwrap();
assert_eq!(v, alloy_primitives::U256::MAX);
}
#[test]
fn decode_u256_extra_bytes_ignored() {
let mut buf = vec![0u8; 64];
buf[31] = 7;
buf[63] = 99;
let v = decode_u256(&buf).unwrap();
assert_eq!(v, alloy_primitives::U256::from(7u64));
}
#[test]
fn decode_u8_zero() {
let buf = [0u8; 32];
assert_eq!(decode_u8(&buf).unwrap(), 0u8);
}
#[test]
fn decode_string_empty_string() {
let mut buf = vec![0u8; 96];
buf[31] = 32; buf[63] = 0; assert_eq!(decode_string(&buf).unwrap(), "");
}
#[test]
fn onchain_reader_clone() {
let reader = OnchainReader::new("https://example.com");
let cloned = reader.clone();
assert_eq!(cloned.rpc_url, reader.rpc_url);
}
#[test]
fn onchain_reader_debug() {
let reader = OnchainReader::new("https://example.com");
let s = format!("{reader:?}");
assert!(s.contains("OnchainReader"));
}
}