1use serde::{Deserialize, Serialize};
7
8use crate::{Error, Result};
9
10#[derive(Clone)]
12pub struct RelayClient {
13 client: reqwest::Client,
14 relay_url: String,
15 auth_key_hex: String,
16 wallet_address: String,
17 is_test: bool,
18 chain_id: u64,
19}
20
21#[derive(Deserialize)]
23pub struct RegisterResponse {
24 pub success: bool,
25 pub user_id: Option<String>,
26 pub error_code: Option<String>,
27 pub error_message: Option<String>,
28}
29
30#[derive(Deserialize)]
32pub struct ResolveResponse {
33 pub address: Option<String>,
34 pub error: Option<String>,
35}
36
37#[derive(Deserialize)]
39pub struct GraphQLResponse<T> {
40 pub data: Option<T>,
41 pub errors: Option<Vec<serde_json::Value>>,
42}
43
44#[derive(Deserialize)]
46pub struct BillingStatus {
47 pub tier: Option<String>,
48 pub facts_used: Option<u64>,
49 pub facts_limit: Option<u64>,
50 pub features: Option<serde_json::Value>,
51}
52
53#[derive(Clone, Debug)]
55pub struct RelayConfig {
56 pub relay_url: String,
57 pub auth_key_hex: String,
58 pub wallet_address: String,
59 pub is_test: bool,
60 pub chain_id: u64,
61}
62
63impl Default for RelayConfig {
64 fn default() -> Self {
65 Self {
66 relay_url: String::new(),
67 auth_key_hex: String::new(),
68 wallet_address: String::new(),
69 is_test: false,
70 chain_id: 84532, }
72 }
73}
74
75impl RelayClient {
76 pub fn new(config: RelayConfig) -> Self {
78 let client = reqwest::Client::builder()
79 .timeout(std::time::Duration::from_secs(30))
80 .build()
81 .unwrap_or_default();
82
83 Self {
84 client,
85 relay_url: config.relay_url.trim_end_matches('/').to_string(),
86 auth_key_hex: config.auth_key_hex,
87 wallet_address: config.wallet_address,
88 is_test: config.is_test,
89 chain_id: config.chain_id,
90 }
91 }
92
93 fn headers(&self) -> reqwest::header::HeaderMap {
95 let mut headers = reqwest::header::HeaderMap::new();
96 headers.insert(
97 "X-TotalReclaw-Client",
98 "rust-client:zeroclaw".parse().unwrap(),
99 );
100 if !self.auth_key_hex.is_empty() {
101 headers.insert(
102 "Authorization",
103 format!("Bearer {}", self.auth_key_hex).parse().unwrap(),
104 );
105 }
106 if !self.wallet_address.is_empty() {
107 headers.insert(
108 "X-Wallet-Address",
109 self.wallet_address.parse().unwrap(),
110 );
111 }
112 if self.is_test {
113 headers.insert("X-TotalReclaw-Test", "true".parse().unwrap());
114 }
115 headers
116 }
117
118 pub async fn register(&self, auth_key_hash: &str, salt_hex: &str) -> Result<String> {
120 #[derive(Serialize)]
121 struct Body<'a> {
122 auth_key_hash: &'a str,
123 salt: &'a str,
124 }
125
126 let resp = self
127 .client
128 .post(format!("{}/v1/register", self.relay_url))
129 .headers(self.headers())
130 .json(&Body {
131 auth_key_hash,
132 salt: salt_hex,
133 })
134 .send()
135 .await
136 .map_err(|e| Error::Http(e.to_string()))?;
137
138 let body: RegisterResponse = resp
139 .json()
140 .await
141 .map_err(|e| Error::Http(e.to_string()))?;
142
143 if body.success {
144 Ok(body.user_id.unwrap_or_default())
145 } else {
146 Err(Error::Http(
147 body.error_message
148 .unwrap_or_else(|| "Registration failed".into()),
149 ))
150 }
151 }
152
153 pub async fn resolve_address(&self, auth_key_hex: &str) -> Result<String> {
155 #[derive(Serialize)]
156 struct Body<'a> {
157 auth_key: &'a str,
158 }
159
160 let resp = self
161 .client
162 .post(format!("{}/v1/addresses/resolve", self.relay_url))
163 .headers(self.headers())
164 .json(&Body {
165 auth_key: auth_key_hex,
166 })
167 .send()
168 .await
169 .map_err(|e| Error::Http(e.to_string()))?;
170
171 let body: ResolveResponse = resp
172 .json()
173 .await
174 .map_err(|e| Error::Http(e.to_string()))?;
175
176 body.address
177 .ok_or_else(|| Error::Http(body.error.unwrap_or_else(|| "No address returned".into())))
178 }
179
180 pub async fn graphql<T: for<'de> Deserialize<'de>>(
182 &self,
183 query: &str,
184 variables: serde_json::Value,
185 ) -> Result<T> {
186 let resp = self
187 .client
188 .post(format!("{}/v1/subgraph", self.relay_url))
189 .headers(self.headers())
190 .json(&serde_json::json!({
191 "query": query,
192 "variables": variables,
193 }))
194 .send()
195 .await
196 .map_err(|e| Error::Http(e.to_string()))?;
197
198 if resp.status().as_u16() == 403 {
199 crate::billing::invalidate_cache();
201 let text = resp.text().await.unwrap_or_default();
202 return Err(Error::QuotaExceeded(format!(
203 "Quota exceeded (403). Billing cache invalidated. Upgrade to Pro for unlimited storage. Server: {}",
204 text
205 )));
206 }
207
208 if !resp.status().is_success() {
209 let status = resp.status();
210 let text = resp.text().await.unwrap_or_default();
211 return Err(Error::Http(format!("GraphQL HTTP {}: {}", status, text)));
212 }
213
214 let gql: GraphQLResponse<T> = resp
215 .json()
216 .await
217 .map_err(|e| Error::Http(format!("GraphQL parse error: {}", e)))?;
218
219 gql.data
220 .ok_or_else(|| Error::Http("GraphQL returned no data".into()))
221 }
222
223 pub async fn submit_fact_native(
225 &self,
226 protobuf_payload: &[u8],
227 private_key: &[u8; 32],
228 ) -> Result<crate::userop::SubmitResult> {
229 let calldata = crate::userop::encode_single_call(protobuf_payload);
230 crate::userop::submit_userop(
231 &calldata,
232 &self.wallet_address,
233 private_key,
234 &self.relay_url,
235 &self.auth_key_hex,
236 self.chain_id,
237 self.is_test,
238 )
239 .await
240 }
241
242 pub async fn submit_fact_batch_native(
244 &self,
245 protobuf_payloads: &[Vec<u8>],
246 private_key: &[u8; 32],
247 ) -> Result<crate::userop::SubmitResult> {
248 let calldata = crate::userop::encode_batch_call(protobuf_payloads)?;
249 crate::userop::submit_userop(
250 &calldata,
251 &self.wallet_address,
252 private_key,
253 &self.relay_url,
254 &self.auth_key_hex,
255 self.chain_id,
256 self.is_test,
257 )
258 .await
259 }
260
261 pub async fn submit_protobuf(&self, payload: &[u8]) -> Result<SubmitResult> {
263 let payload_hex = hex::encode(payload);
264
265 let resp = self
266 .client
267 .post(format!("{}/v1/bundler", self.relay_url))
268 .headers(self.headers())
269 .json(&serde_json::json!({
270 "jsonrpc": "2.0",
271 "method": "eth_sendUserOperation",
272 "params": [{
273 "callData": format!("0x{}", payload_hex),
274 }],
275 "id": 1,
276 }))
277 .send()
278 .await
279 .map_err(|e| Error::Http(e.to_string()))?;
280
281 let body: serde_json::Value = resp
282 .json()
283 .await
284 .map_err(|e| Error::Http(e.to_string()))?;
285
286 Ok(SubmitResult {
287 tx_hash: body["result"]["txHash"]
288 .as_str()
289 .unwrap_or("")
290 .to_string(),
291 user_op_hash: body["result"]["userOpHash"]
292 .as_str()
293 .unwrap_or("")
294 .to_string(),
295 success: body["result"]["success"].as_bool().unwrap_or(false),
296 })
297 }
298
299 pub async fn health_check(&self) -> Result<bool> {
301 let resp = self
302 .client
303 .get(format!("{}/health", self.relay_url))
304 .send()
305 .await
306 .map_err(|e| Error::Http(e.to_string()))?;
307
308 Ok(resp.status().is_success())
309 }
310
311 pub async fn billing_status(&self) -> Result<BillingStatus> {
313 let resp = self
314 .client
315 .get(format!(
316 "{}/v1/billing/status?wallet_address={}",
317 self.relay_url, self.wallet_address
318 ))
319 .headers(self.headers())
320 .send()
321 .await
322 .map_err(|e| Error::Http(e.to_string()))?;
323
324 resp.json()
325 .await
326 .map_err(|e| Error::Http(e.to_string()))
327 }
328
329 pub async fn create_checkout(&self) -> Result<String> {
331 let resp = self
332 .client
333 .post(format!("{}/v1/billing/checkout", self.relay_url))
334 .headers(self.headers())
335 .json(&serde_json::json!({
336 "wallet_address": self.wallet_address,
337 "tier": "pro",
338 }))
339 .send()
340 .await
341 .map_err(|e| Error::Http(e.to_string()))?;
342
343 let body: serde_json::Value = resp
344 .json()
345 .await
346 .map_err(|e| Error::Http(e.to_string()))?;
347
348 body["checkout_url"]
349 .as_str()
350 .map(|s| s.to_string())
351 .ok_or_else(|| Error::Http("No checkout_url in response".into()))
352 }
353
354 pub fn relay_url(&self) -> &str {
356 &self.relay_url
357 }
358
359 pub fn wallet_address(&self) -> &str {
361 &self.wallet_address
362 }
363
364 pub fn auth_key_hex(&self) -> &str {
366 &self.auth_key_hex
367 }
368
369 pub fn is_test(&self) -> bool {
371 self.is_test
372 }
373
374 pub fn chain_id(&self) -> u64 {
376 self.chain_id
377 }
378
379 pub fn set_chain_id(&mut self, chain_id: u64) {
381 self.chain_id = chain_id;
382 }
383}
384
385#[derive(Debug)]
387pub struct SubmitResult {
388 pub tx_hash: String,
389 pub user_op_hash: String,
390 pub success: bool,
391}