rust_x402/
blockchain.rs

1//! Real blockchain integration for x402 payments
2//!
3//! This module provides real blockchain interactions for:
4//! - Transaction monitoring
5//! - Balance checking
6//! - Network status verification
7//! - Gas estimation
8
9use crate::{Result, X402Error};
10use serde::{Deserialize, Serialize};
11
12/// Blockchain client for real network interactions
13pub struct BlockchainClient {
14    /// RPC endpoint URL
15    rpc_url: String,
16    /// Network name
17    pub network: String,
18    /// HTTP client for RPC calls
19    client: reqwest::Client,
20}
21
22/// Blockchain transaction status
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub enum TransactionStatus {
25    Pending,
26    Confirmed,
27    Failed,
28    Unknown,
29}
30
31/// Blockchain transaction information
32#[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/// Balance information for an address
45#[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/// Network information
54#[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    /// Create a new blockchain client
64    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    /// Get transaction status by hash
73    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            // Get transaction receipt for gas information
111            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    /// Get transaction receipt
157    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    /// Get balance for an address
182    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    /// Get USDC balance for an address
215    pub async fn get_usdc_balance(&self, address: &str) -> Result<BalanceInfo> {
216        let usdc_contract = self.get_usdc_contract_address()?;
217
218        // Call balanceOf function on USDC contract
219        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(), // We're only getting token balance
248            token_balance: Some(token_balance),
249            token_address: Some(usdc_contract),
250        })
251    }
252
253    /// Get network information
254    pub async fn get_network_info(&self) -> Result<NetworkInfo> {
255        // Get chain ID
256        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        // Get latest block number
280        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        // Get gas price
304        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    /// Estimate gas for a transaction
336    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    /// Get USDC contract address for current network
364    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/// Transaction request for gas estimation
379#[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
389/// Blockchain client factory
390pub struct BlockchainClientFactory;
391
392impl BlockchainClientFactory {
393    /// Create client for Base Sepolia testnet
394    pub fn base_sepolia() -> BlockchainClient {
395        BlockchainClient::new(
396            "https://sepolia.base.org".to_string(),
397            "base-sepolia".to_string(),
398        )
399    }
400
401    /// Create client for Base mainnet
402    pub fn base() -> BlockchainClient {
403        BlockchainClient::new("https://mainnet.base.org".to_string(), "base".to_string())
404    }
405
406    /// Create client for Avalanche Fuji testnet
407    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    /// Create client for Avalanche mainnet
415    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    /// Create client with custom RPC URL
423    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}