hypercall_api/directives/
onchain.rs1use 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}