1use crate::{Result, X402Error};
10use serde::{Deserialize, Serialize};
11
12pub struct BlockchainClient {
14 rpc_url: String,
16 pub network: String,
18 client: reqwest::Client,
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub enum TransactionStatus {
25 Pending,
26 Confirmed,
27 Failed,
28 Unknown,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct TransactionInfo {
34 pub hash: String,
35 pub status: TransactionStatus,
36 pub block_number: Option<u64>,
37 pub gas_used: Option<u64>,
38 pub effective_gas_price: Option<String>,
39 pub from: String,
40 pub to: String,
41 pub value: String,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct BalanceInfo {
47 pub address: String,
48 pub balance: String,
49 pub token_balance: Option<String>,
50 pub token_address: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct NetworkInfo {
56 pub chain_id: u64,
57 pub network_name: String,
58 pub latest_block: u64,
59 pub gas_price: String,
60}
61
62impl BlockchainClient {
63 pub fn new(rpc_url: String, network: String) -> Self {
65 Self {
66 rpc_url,
67 network,
68 client: reqwest::Client::new(),
69 }
70 }
71
72 pub async fn get_transaction_status(&self, tx_hash: &str) -> Result<TransactionInfo> {
74 let response = self
75 .client
76 .post(&self.rpc_url)
77 .json(&serde_json::json!({
78 "jsonrpc": "2.0",
79 "method": "eth_getTransactionByHash",
80 "params": [tx_hash],
81 "id": 1
82 }))
83 .send()
84 .await
85 .map_err(|e| X402Error::network_error(format!("RPC request failed: {}", e)))?;
86
87 let response_json: serde_json::Value = response.json().await.map_err(|e| {
88 X402Error::network_error(format!("Failed to parse RPC response: {}", e))
89 })?;
90
91 if let Some(result) = response_json.get("result") {
92 if result.is_null() {
93 return Ok(TransactionInfo {
94 hash: tx_hash.to_string(),
95 status: TransactionStatus::Unknown,
96 block_number: None,
97 gas_used: None,
98 effective_gas_price: None,
99 from: "".to_string(),
100 to: "".to_string(),
101 value: "".to_string(),
102 });
103 }
104
105 let block_number = result
106 .get("blockNumber")
107 .and_then(|v| v.as_str())
108 .and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok());
109
110 let gas_info = self.get_transaction_receipt(tx_hash).await.ok();
112
113 Ok(TransactionInfo {
114 hash: tx_hash.to_string(),
115 status: if block_number.is_some() {
116 TransactionStatus::Confirmed
117 } else {
118 TransactionStatus::Pending
119 },
120 block_number,
121 gas_used: gas_info
122 .as_ref()
123 .and_then(|r| r.get("gasUsed"))
124 .and_then(|v| {
125 v.as_str()
126 .and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok())
127 }),
128 effective_gas_price: gas_info
129 .as_ref()
130 .and_then(|r| r.get("effectiveGasPrice"))
131 .and_then(|v| v.as_str())
132 .map(|s| s.to_string()),
133 from: result
134 .get("from")
135 .and_then(|v| v.as_str())
136 .unwrap_or("")
137 .to_string(),
138 to: result
139 .get("to")
140 .and_then(|v| v.as_str())
141 .unwrap_or("")
142 .to_string(),
143 value: result
144 .get("value")
145 .and_then(|v| v.as_str())
146 .unwrap_or("0x0")
147 .to_string(),
148 })
149 } else {
150 Err(X402Error::network_error(
151 "Invalid RPC response format".to_string(),
152 ))
153 }
154 }
155
156 async fn get_transaction_receipt(&self, tx_hash: &str) -> Result<serde_json::Value> {
158 let response = self
159 .client
160 .post(&self.rpc_url)
161 .json(&serde_json::json!({
162 "jsonrpc": "2.0",
163 "method": "eth_getTransactionReceipt",
164 "params": [tx_hash],
165 "id": 1
166 }))
167 .send()
168 .await
169 .map_err(|e| X402Error::network_error(format!("RPC request failed: {}", e)))?;
170
171 let response_json: serde_json::Value = response.json().await.map_err(|e| {
172 X402Error::network_error(format!("Failed to parse RPC response: {}", e))
173 })?;
174
175 response_json
176 .get("result")
177 .ok_or_else(|| X402Error::network_error("No result in RPC response".to_string()))
178 .cloned()
179 }
180
181 pub async fn get_balance(&self, address: &str) -> Result<BalanceInfo> {
183 let response = self
184 .client
185 .post(&self.rpc_url)
186 .json(&serde_json::json!({
187 "jsonrpc": "2.0",
188 "method": "eth_getBalance",
189 "params": [address, "latest"],
190 "id": 1
191 }))
192 .send()
193 .await
194 .map_err(|e| X402Error::network_error(format!("RPC request failed: {}", e)))?;
195
196 let response_json: serde_json::Value = response.json().await.map_err(|e| {
197 X402Error::network_error(format!("Failed to parse RPC response: {}", e))
198 })?;
199
200 let balance = response_json
201 .get("result")
202 .and_then(|v| v.as_str())
203 .unwrap_or("0x0")
204 .to_string();
205
206 Ok(BalanceInfo {
207 address: address.to_string(),
208 balance,
209 token_balance: None,
210 token_address: None,
211 })
212 }
213
214 pub async fn get_usdc_balance(&self, address: &str) -> Result<BalanceInfo> {
216 let usdc_contract = self.get_usdc_contract_address()?;
217
218 let response = self
220 .client
221 .post(&self.rpc_url)
222 .json(&serde_json::json!({
223 "jsonrpc": "2.0",
224 "method": "eth_call",
225 "params": [{
226 "to": usdc_contract,
227 "data": format!("0x70a08231000000000000000000000000{}", address.trim_start_matches("0x"))
228 }, "latest"],
229 "id": 1
230 }))
231 .send()
232 .await
233 .map_err(|e| X402Error::network_error(format!("RPC request failed: {}", e)))?;
234
235 let response_json: serde_json::Value = response.json().await.map_err(|e| {
236 X402Error::network_error(format!("Failed to parse RPC response: {}", e))
237 })?;
238
239 let token_balance = response_json
240 .get("result")
241 .and_then(|v| v.as_str())
242 .unwrap_or("0x0")
243 .to_string();
244
245 Ok(BalanceInfo {
246 address: address.to_string(),
247 balance: "0x0".to_string(), token_balance: Some(token_balance),
249 token_address: Some(usdc_contract),
250 })
251 }
252
253 pub async fn get_network_info(&self) -> Result<NetworkInfo> {
255 let chain_id_response = self
257 .client
258 .post(&self.rpc_url)
259 .json(&serde_json::json!({
260 "jsonrpc": "2.0",
261 "method": "eth_chainId",
262 "params": [],
263 "id": 1
264 }))
265 .send()
266 .await
267 .map_err(|e| X402Error::network_error(format!("RPC request failed: {}", e)))?;
268
269 let chain_id_json: serde_json::Value = chain_id_response.json().await.map_err(|e| {
270 X402Error::network_error(format!("Failed to parse RPC response: {}", e))
271 })?;
272
273 let chain_id = chain_id_json
274 .get("result")
275 .and_then(|v| v.as_str())
276 .and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok())
277 .unwrap_or(0);
278
279 let block_response = self
281 .client
282 .post(&self.rpc_url)
283 .json(&serde_json::json!({
284 "jsonrpc": "2.0",
285 "method": "eth_blockNumber",
286 "params": [],
287 "id": 1
288 }))
289 .send()
290 .await
291 .map_err(|e| X402Error::network_error(format!("RPC request failed: {}", e)))?;
292
293 let block_json: serde_json::Value = block_response.json().await.map_err(|e| {
294 X402Error::network_error(format!("Failed to parse RPC response: {}", e))
295 })?;
296
297 let latest_block = block_json
298 .get("result")
299 .and_then(|v| v.as_str())
300 .and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok())
301 .unwrap_or(0);
302
303 let gas_response = self
305 .client
306 .post(&self.rpc_url)
307 .json(&serde_json::json!({
308 "jsonrpc": "2.0",
309 "method": "eth_gasPrice",
310 "params": [],
311 "id": 1
312 }))
313 .send()
314 .await
315 .map_err(|e| X402Error::network_error(format!("RPC request failed: {}", e)))?;
316
317 let gas_json: serde_json::Value = gas_response.json().await.map_err(|e| {
318 X402Error::network_error(format!("Failed to parse RPC response: {}", e))
319 })?;
320
321 let gas_price = gas_json
322 .get("result")
323 .and_then(|v| v.as_str())
324 .unwrap_or("0x0")
325 .to_string();
326
327 Ok(NetworkInfo {
328 chain_id,
329 network_name: self.network.clone(),
330 latest_block,
331 gas_price,
332 })
333 }
334
335 pub async fn estimate_gas(&self, transaction: &TransactionRequest) -> Result<u64> {
337 let response = self
338 .client
339 .post(&self.rpc_url)
340 .json(&serde_json::json!({
341 "jsonrpc": "2.0",
342 "method": "eth_estimateGas",
343 "params": [transaction],
344 "id": 1
345 }))
346 .send()
347 .await
348 .map_err(|e| X402Error::network_error(format!("RPC request failed: {}", e)))?;
349
350 let response_json: serde_json::Value = response.json().await.map_err(|e| {
351 X402Error::network_error(format!("Failed to parse RPC response: {}", e))
352 })?;
353
354 let gas_hex = response_json
355 .get("result")
356 .and_then(|v| v.as_str())
357 .ok_or_else(|| X402Error::network_error("No gas estimate in response".to_string()))?;
358
359 u64::from_str_radix(gas_hex.trim_start_matches("0x"), 16)
360 .map_err(|_| X402Error::network_error("Invalid gas estimate format".to_string()))
361 }
362
363 pub fn get_usdc_contract_address(&self) -> Result<String> {
365 match self.network.as_str() {
366 "base-sepolia" => Ok("0x036CbD53842c5426634e7929541eC2318f3dCF7e".to_string()),
367 "base" => Ok("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913".to_string()),
368 "avalanche-fuji" => Ok("0x5425890298aed601595a70AB815c96711a31Bc65".to_string()),
369 "avalanche" => Ok("0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E".to_string()),
370 _ => Err(X402Error::invalid_network(format!(
371 "Unsupported network: {}",
372 self.network
373 ))),
374 }
375 }
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct TransactionRequest {
381 pub from: String,
382 pub to: String,
383 pub value: Option<String>,
384 pub data: Option<String>,
385 pub gas: Option<String>,
386 pub gas_price: Option<String>,
387}
388
389pub struct BlockchainClientFactory;
391
392impl BlockchainClientFactory {
393 pub fn base_sepolia() -> BlockchainClient {
395 BlockchainClient::new(
396 "https://sepolia.base.org".to_string(),
397 "base-sepolia".to_string(),
398 )
399 }
400
401 pub fn base() -> BlockchainClient {
403 BlockchainClient::new("https://mainnet.base.org".to_string(), "base".to_string())
404 }
405
406 pub fn avalanche_fuji() -> BlockchainClient {
408 BlockchainClient::new(
409 "https://api.avax-test.network/ext/bc/C/rpc".to_string(),
410 "avalanche-fuji".to_string(),
411 )
412 }
413
414 pub fn avalanche() -> BlockchainClient {
416 BlockchainClient::new(
417 "https://api.avax.network/ext/bc/C/rpc".to_string(),
418 "avalanche".to_string(),
419 )
420 }
421
422 pub fn custom(rpc_url: &str, network: &str) -> BlockchainClient {
424 BlockchainClient::new(rpc_url.to_string(), network.to_string())
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn test_blockchain_client_creation() {
434 let client =
435 BlockchainClient::new("https://example.com".to_string(), "testnet".to_string());
436 assert_eq!(client.network, "testnet");
437 }
438
439 #[test]
440 fn test_usdc_contract_address() {
441 let client = BlockchainClient::new(
442 "https://example.com".to_string(),
443 "base-sepolia".to_string(),
444 );
445 let address = client.get_usdc_contract_address().unwrap();
446 assert_eq!(address, "0x036CbD53842c5426634e7929541eC2318f3dCF7e");
447 }
448
449 #[test]
450 fn test_transaction_request_serialization() {
451 let tx = TransactionRequest {
452 from: "0x123".to_string(),
453 to: "0x456".to_string(),
454 value: Some("0x1000".to_string()),
455 data: None,
456 gas: None,
457 gas_price: None,
458 };
459
460 let json = serde_json::to_string(&tx).unwrap();
461 assert!(json.contains("0x123"));
462 }
463}