use std::{str::FromStr, sync::Arc};
use ethrex_common::{Address, H256, U256, types::TxType};
use ethrex_l2_rpc::signer::{LocalSigner, Signer};
use ethrex_l2_sdk::{build_generic_tx, send_generic_transaction};
use ethrex_rpc::{EthClient, clients::EthClientError, clients::Overrides};
use rex_sdk::client::eth::call_with_overrides;
use secp256k1::SecretKey;
use crate::error::{Error, Result};
pub use ethrex_rpc::types::receipt::RpcReceipt as TransactionReceipt;
pub use rex_sdk::client::eth::{AccountOverride, BlockOverrideSet, StateOverrideSet};
const RECEIPT_MAX_RETRIES: u64 = 60;
#[derive(Debug, Clone, Default)]
pub struct CallOverrides {
pub state: Option<StateOverrideSet>,
pub block: Option<BlockOverrideSet>,
}
#[derive(Clone)]
pub struct ContractClient {
client: Arc<EthClient>,
signer: Option<Signer>,
}
impl ContractClient {
pub fn connect(rpc_url: &str) -> Result<Self> {
Ok(Self {
client: Self::eth_client(rpc_url)?,
signer: None,
})
}
pub fn connect_with_signer(rpc_url: &str, private_key: &str) -> Result<Self> {
let key = SecretKey::from_str(private_key.trim_start_matches("0x"))
.map_err(|e| Error::InvalidInput(format!("invalid private key: {e}")))?;
Ok(Self {
client: Self::eth_client(rpc_url)?,
signer: Some(LocalSigner::new(key).into()),
})
}
fn eth_client(rpc_url: &str) -> Result<Arc<EthClient>> {
let url = rpc_url
.parse()
.map_err(|e| Error::InvalidInput(format!("invalid RPC url: {e}")))?;
Ok(Arc::new(EthClient::new(url)?))
}
pub fn eth(&self) -> &EthClient {
&self.client
}
pub fn signer_address(&self) -> Option<Address> {
self.signer.as_ref().map(|s| s.address())
}
pub async fn call(
&self,
to: Address,
calldata: Vec<u8>,
overrides: &CallOverrides,
) -> Result<Vec<u8>> {
let tx_overrides = Overrides {
from: self.signer_address(),
..Default::default()
};
let default_state = StateOverrideSet::default();
let default_block = BlockOverrideSet::default();
let state = overrides.state.as_ref().unwrap_or(&default_state);
let block = overrides.block.as_ref().unwrap_or(&default_block);
let raw = call_with_overrides(
&self.client,
to,
calldata.into(),
tx_overrides,
state,
block,
)
.await
.map_err(parse_call_error)?;
hex::decode(raw.trim_start_matches("0x"))
.map_err(|e| Error::Abi(format!("eth_call returned invalid hex: {e}")))
}
pub async fn send(&self, to: Address, calldata: Vec<u8>, value: Option<U256>) -> Result<H256> {
let signer = self.signer.as_ref().ok_or_else(|| {
Error::InvalidInput(
"ContractClient was created without a signer; sends are unavailable".into(),
)
})?;
let overrides = Overrides {
from: Some(signer.address()),
value,
..Default::default()
};
let tx = build_generic_tx(
&self.client,
TxType::EIP1559,
to,
signer.address(),
calldata.into(),
overrides,
)
.await?;
Ok(send_generic_transaction(&self.client, tx, signer).await?)
}
pub async fn wait_for_transaction(&self, hash: H256) -> Result<TransactionReceipt> {
let receipt =
rex_sdk::wait_for_transaction_receipt(hash, &self.client, RECEIPT_MAX_RETRIES, true)
.await?;
Ok(receipt)
}
}
fn parse_call_error(error: EthClientError) -> Error {
let EthClientError::Custom(text) = &error else {
return Error::Client(error);
};
let Some(rest) = text.strip_prefix("eth_call rpc error: ") else {
return Error::Client(error);
};
if let Some((message, suffix)) = rest.rsplit_once(" (data: 0x")
&& let Some(data) = suffix
.strip_suffix(')')
.and_then(|hex_data| hex::decode(hex_data).ok())
{
return Error::Revert {
message: message.to_string(),
data: Some(data),
};
}
Error::Revert {
message: rest.to_string(),
data: None,
}
}