use alloy::network::{EthereumWallet, AnyNetwork};
use alloy::primitives::{Address, U256};
use alloy::providers::{ProviderBuilder, Provider as _};
use std::sync::Arc;
use tokio::sync::OnceCell;
use crate::onchain::{
contracts::{get_addresses, Addresses, IERC20},
NetworkConfig, OnchainError, Result, WalletType,
};
use super::provider::OnchainProvider;
use super::signer::OnchainSigner;
#[derive(Clone)]
pub struct OnchainClient {
provider: Arc<OnchainProvider>,
signer: Arc<OnchainSigner>,
addresses: Addresses,
wallet_provider: Arc<OnceCell<alloy::providers::DynProvider<AnyNetwork>>>,
}
impl OnchainClient {
pub fn new(provider: OnchainProvider, signer: OnchainSigner) -> Self {
let addresses = get_addresses(provider.chain_id())
.expect("Provider should have valid chain ID");
Self {
provider: Arc::new(provider),
signer: Arc::new(signer),
addresses,
wallet_provider: Arc::new(OnceCell::new()),
}
}
pub(crate) async fn wallet_provider(&self) -> Result<&alloy::providers::DynProvider<AnyNetwork>> {
self.wallet_provider
.get_or_try_init(|| async {
let wallet = self.ethereum_wallet();
let url = self.network().rpc_url.parse().map_err(|e| {
OnchainError::NetworkError(format!("Invalid RPC URL: {}", e))
})?;
let provider = ProviderBuilder::new()
.network::<AnyNetwork>()
.with_cached_nonce_management() .wallet(wallet)
.connect_http(url);
Ok::<_, OnchainError>(provider.erased())
})
.await
}
pub fn provider(&self) -> &OnchainProvider {
&self.provider
}
pub fn signer(&self) -> &OnchainSigner {
&self.signer
}
pub fn address(&self) -> Address {
self.signer.address()
}
pub fn signer_address(&self) -> Address {
self.signer.signer_address()
}
pub fn wallet_type(&self) -> WalletType {
self.signer.wallet_type()
}
pub fn network(&self) -> &NetworkConfig {
self.provider.network()
}
pub fn chain_id(&self) -> u64 {
self.provider.chain_id()
}
pub fn addresses(&self) -> &Addresses {
&self.addresses
}
pub fn usdc_address(&self) -> Address {
self.addresses.usdc
}
pub fn ctf_address(&self) -> Address {
self.addresses.ctf
}
pub fn proxy_factory_address(&self) -> Address {
self.addresses.proxy_factory
}
pub fn safe_factory_address(&self) -> Address {
self.addresses.safe_factory
}
pub async fn get_gas_price(&self) -> Result<U256> {
self.provider.get_gas_price().await
}
pub async fn get_block_number(&self) -> Result<u64> {
self.provider.get_block_number().await
}
pub async fn is_connected(&self) -> bool {
self.provider.is_connected().await
}
pub async fn get_usdc_balance(&self) -> Result<U256> {
self.get_usdc_balance_of(self.address()).await
}
pub async fn get_usdc_balance_of(&self, address: Address) -> Result<U256> {
let usdc_address = self.addresses.usdc;
let usdc = IERC20::new(usdc_address, self.provider.provider());
let balance_result = usdc
.balanceOf(address)
.call()
.await
.map_err(|e| OnchainError::ContractError(format!("Failed to get USDC balance: {}", e)))?;
Ok(balance_result)
}
pub async fn sign_message(&self, message: &[u8]) -> Result<Vec<u8>> {
self.signer.sign_message(message).await
}
pub(crate) fn ethereum_wallet(&self) -> EthereumWallet {
self.signer.to_ethereum_wallet(self.chain_id())
}
pub fn is_smart_wallet(&self) -> bool {
self.signer.is_smart_wallet()
}
pub fn proxy_address(&self) -> Option<Address> {
self.signer.proxy_address()
}
pub fn safe_address(&self) -> Option<Address> {
self.signer.safe_address()
}
}
impl std::fmt::Debug for OnchainClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OnchainClient")
.field("chain_id", &self.chain_id())
.field("wallet_type", &self.wallet_type())
.field("address", &self.address())
.field("signer_address", &self.signer_address())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy::network::AnyNetwork;
use alloy::providers::{ProviderBuilder, Provider};
const TEST_PRIVATE_KEY: &str =
"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
#[test]
fn test_client_new() {
let network = NetworkConfig::polygon_mainnet();
let provider = OnchainProvider {
provider: {
let url = network.rpc_url.parse().unwrap();
ProviderBuilder::new()
.network::<AnyNetwork>()
.connect_http(url)
.erased()
},
network: network.clone(),
};
let signer = OnchainSigner::from_private_key(TEST_PRIVATE_KEY).unwrap();
let client = OnchainClient::new(provider, signer);
assert_eq!(client.chain_id(), 137);
assert_eq!(client.wallet_type(), WalletType::PrivateKey);
assert!(!client.is_smart_wallet());
}
#[test]
fn test_client_addresses() {
let network = NetworkConfig::polygon_mainnet();
let provider = OnchainProvider {
provider: {
let url = network.rpc_url.parse().unwrap();
ProviderBuilder::new()
.network::<AnyNetwork>()
.connect_http(url)
.erased()
},
network: network.clone(),
};
let signer = OnchainSigner::from_private_key(TEST_PRIVATE_KEY).unwrap();
let client = OnchainClient::new(provider, signer);
assert_ne!(client.usdc_address(), Address::ZERO);
assert_ne!(client.ctf_address(), Address::ZERO);
assert_ne!(client.proxy_factory_address(), Address::ZERO);
assert_ne!(client.safe_factory_address(), Address::ZERO);
}
#[test]
fn test_client_proxy_wallet() {
let network = NetworkConfig::polygon_mainnet();
let provider = OnchainProvider {
provider: {
let url = network.rpc_url.parse().unwrap();
ProviderBuilder::new()
.network::<AnyNetwork>()
.connect_http(url)
.erased()
},
network: network.clone(),
};
let proxy_address = Address::repeat_byte(0x42);
let signer = OnchainSigner::from_proxy(TEST_PRIVATE_KEY, proxy_address).unwrap();
let client = OnchainClient::new(provider, signer);
assert_eq!(client.wallet_type(), WalletType::Proxy);
assert!(client.is_smart_wallet());
assert_eq!(client.address(), proxy_address);
assert_eq!(client.proxy_address(), Some(proxy_address));
assert_ne!(client.signer_address(), proxy_address);
}
#[test]
fn test_client_safe_wallet() {
let network = NetworkConfig::polygon_mainnet();
let provider = OnchainProvider {
provider: {
let url = network.rpc_url.parse().unwrap();
ProviderBuilder::new()
.network::<AnyNetwork>()
.connect_http(url)
.erased()
},
network: network.clone(),
};
let safe_address = Address::repeat_byte(0x99);
let signer = OnchainSigner::from_safe(TEST_PRIVATE_KEY, safe_address, Some(2)).unwrap();
let client = OnchainClient::new(provider, signer);
assert_eq!(client.wallet_type(), WalletType::Safe);
assert!(client.is_smart_wallet());
assert_eq!(client.address(), safe_address);
assert_eq!(client.safe_address(), Some(safe_address));
assert_ne!(client.signer_address(), safe_address);
}
#[test]
fn test_client_debug() {
let network = NetworkConfig::polygon_mainnet();
let provider = OnchainProvider {
provider: {
let url = network.rpc_url.parse().unwrap();
ProviderBuilder::new()
.network::<AnyNetwork>()
.connect_http(url)
.erased()
},
network: network.clone(),
};
let signer = OnchainSigner::from_private_key(TEST_PRIVATE_KEY).unwrap();
let client = OnchainClient::new(provider, signer);
let debug_str = format!("{:?}", client);
assert!(debug_str.contains("OnchainClient"));
assert!(debug_str.contains("chain_id"));
assert!(debug_str.contains("137"));
}
}