Skip to main content

hypercall_api/directives/
onchain.rs

1use crate::error::ApiError;
2use alloy::{
3    primitives::Address,
4    providers::{DynProvider, Provider, ProviderBuilder},
5    sol,
6};
7use async_trait::async_trait;
8use std::collections::{HashMap, HashSet};
9use std::str::FromStr;
10use std::sync::{
11    atomic::{AtomicBool, Ordering},
12    Arc,
13};
14use tokio::sync::RwLock;
15
16sol! {
17    #[sol(rpc)]
18    contract Exchange {
19        function managers(address account) external view returns (address);
20    }
21}
22
23sol! {
24    #[sol(rpc)]
25    contract Account {
26        function isApiWalletActive(address wallet) external view returns (bool);
27    }
28}
29
30#[async_trait]
31pub trait ChainAuthReader: Send + Sync {
32    async fn get_manager(&self, account: Address) -> Result<Address, ApiError>;
33    async fn is_api_wallet_active(
34        &self,
35        account: Address,
36        signer: Address,
37    ) -> Result<bool, ApiError>;
38}
39
40pub struct AlloyChainAuthReader {
41    provider: DynProvider,
42    exchange_address: Address,
43}
44
45impl AlloyChainAuthReader {
46    pub fn new(rpc_url: &str, exchange_address: &str) -> Result<Self, String> {
47        let exchange_address = Address::from_str(exchange_address)
48            .map_err(|e| format!("Invalid EXCHANGE_CONTRACT_ADDRESS: {}", e))?;
49
50        let provider = ProviderBuilder::new()
51            .connect_http(
52                rpc_url
53                    .parse()
54                    .map_err(|e| format!("Invalid RPC_URL: {}", e))?,
55            )
56            .erased();
57
58        Ok(Self {
59            provider,
60            exchange_address,
61        })
62    }
63}
64
65#[async_trait]
66impl ChainAuthReader for AlloyChainAuthReader {
67    async fn get_manager(&self, account: Address) -> Result<Address, ApiError> {
68        let exchange = Exchange::new(self.exchange_address, &self.provider);
69        let response = exchange.managers(account).call().await.map_err(|e| {
70            ApiError::internal_error(format!("Failed to read Exchange.managers({account}): {e}"))
71        })?;
72        Ok(response)
73    }
74
75    async fn is_api_wallet_active(
76        &self,
77        account: Address,
78        signer: Address,
79    ) -> Result<bool, ApiError> {
80        let account_contract = Account::new(account, &self.provider);
81        let response = account_contract
82            .isApiWalletActive(signer)
83            .call()
84            .await
85            .map_err(|e| {
86                ApiError::internal_error(format!(
87                    "Failed to read Account({account}).isApiWalletActive({signer}): {e}"
88                ))
89            })?;
90        Ok(response)
91    }
92}
93
94#[derive(Default)]
95pub struct MockChainAuth {
96    managers: RwLock<HashMap<Address, Address>>,
97    active_wallets: RwLock<HashSet<(Address, Address)>>,
98    fail_reads: AtomicBool,
99}
100
101impl MockChainAuth {
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    pub async fn set_manager(&self, account: Address, manager: Address) {
107        self.managers.write().await.insert(account, manager);
108    }
109
110    pub async fn set_api_wallet_active(&self, account: Address, signer: Address, active: bool) {
111        let mut wallets = self.active_wallets.write().await;
112        if active {
113            wallets.insert((account, signer));
114        } else {
115            wallets.remove(&(account, signer));
116        }
117    }
118
119    pub fn set_fail_reads(&self, fail_reads: bool) {
120        self.fail_reads.store(fail_reads, Ordering::Relaxed);
121    }
122}
123
124#[async_trait]
125impl ChainAuthReader for MockChainAuth {
126    async fn get_manager(&self, account: Address) -> Result<Address, ApiError> {
127        if self.fail_reads.load(Ordering::Relaxed) {
128            return Err(ApiError::internal_error(
129                "MockChainAuth configured to fail chain reads",
130            ));
131        }
132
133        Ok(*self
134            .managers
135            .read()
136            .await
137            .get(&account)
138            .unwrap_or(&Address::ZERO))
139    }
140
141    async fn is_api_wallet_active(
142        &self,
143        account: Address,
144        signer: Address,
145    ) -> Result<bool, ApiError> {
146        if self.fail_reads.load(Ordering::Relaxed) {
147            return Err(ApiError::internal_error(
148                "MockChainAuth configured to fail chain reads",
149            ));
150        }
151
152        Ok(self
153            .active_wallets
154            .read()
155            .await
156            .contains(&(account, signer)))
157    }
158}
159
160#[derive(Default)]
161pub struct NoopChainAuthReader;
162
163impl NoopChainAuthReader {
164    pub fn new() -> Arc<Self> {
165        Arc::new(Self)
166    }
167}
168
169#[async_trait]
170impl ChainAuthReader for NoopChainAuthReader {
171    async fn get_manager(&self, _account: Address) -> Result<Address, ApiError> {
172        Err(ApiError::internal_error(
173            "NoopChainAuthReader cannot service chain reads",
174        ))
175    }
176
177    async fn is_api_wallet_active(
178        &self,
179        _account: Address,
180        _signer: Address,
181    ) -> Result<bool, ApiError> {
182        Err(ApiError::internal_error(
183            "NoopChainAuthReader cannot service chain reads",
184        ))
185    }
186}