feat: ✨ Added main mvp for fetching balances for multiple chains
d7690aa0
2 file(s) · +108 −59
| 9 | 9 | serde = { version = "1.0", features = ["derive"]} |
|
| 10 | 10 | serde_json = "1.0" |
|
| 11 | 11 | tokio = { version = "1.0", features = ["full"]} |
|
| 12 | - | future = "0.3" |
|
| 12 | + | futures = "0.3" |
| 1 | 1 | use clap::{Arg, Command}; |
|
| 2 | - | use reqwest::blocking::Client; |
|
| 2 | + | use reqwest::Client; |
|
| 3 | 3 | use std::collections::HashMap; |
|
| 4 | 4 | use std::error::Error; |
|
| 5 | 5 | use std::env; |
|
| 6 | 6 | use serde::{Deserialize,Serialize}; |
|
| 7 | 7 | use futures::future::join_all; |
|
| 8 | - | //use serde_json::json; |
|
| 8 | + | use tokio::task::JoinHandle; |
|
| 9 | 9 | ||
| 10 | 10 | #[derive(Serialize)] |
|
| 11 | 11 | struct JsonRpcRequest { |
|
| 20 | 20 | // id: u32, |
|
| 21 | 21 | // jsoonrpc: String, |
|
| 22 | 22 | result: String, |
|
| 23 | - | //#[serde(default)] |
|
| 24 | - | // error: Option<JsonRpcError>, |
|
| 25 | 23 | } |
|
| 26 | 24 | ||
| 27 | - | // #[derive(Debug, Deserialize)] |
|
| 28 | - | // struct JsonRpcError { |
|
| 29 | - | // code: i32, |
|
| 30 | - | // message: String, |
|
| 25 | + | ||
| 26 | + | // #[derive(Deserialize)] |
|
| 27 | + | // struct Config { |
|
| 28 | + | // address: Option<String>, |
|
| 29 | + | // networks: Option<HashMap<String, NetworkConfig>>, |
|
| 30 | + | // } |
|
| 31 | + | ||
| 32 | + | // #[derive(Deserialize)] |
|
| 33 | + | // struct NetworkConfig { |
|
| 34 | + | // name: String, |
|
| 35 | + | // rpc_url: String, |
|
| 31 | 36 | // } |
|
| 32 | 37 | ||
| 33 | 38 | struct Network { |
|
| 50 | 55 | }; |
|
| 51 | 56 | ||
| 52 | 57 | networks.insert(chain_id, Network{ |
|
| 53 | - | chain_id, |
|
| 58 | + | chain_id: chain_id, |
|
| 54 | 59 | name: name.to_string(), |
|
| 55 | 60 | rpc_url: value, |
|
| 56 | - | }) |
|
| 57 | - | } |
|
| 61 | + | }); |
|
| 62 | + | }; |
|
| 63 | + | } |
|
| 64 | + | } |
|
| 65 | + | } |
|
| 66 | + | ||
| 67 | + | networks |
|
| 68 | + | } |
|
| 69 | + | ||
| 70 | + | async fn fetch_balance( |
|
| 71 | + | client: &Client, |
|
| 72 | + | address: &str, |
|
| 73 | + | network: &Network |
|
| 74 | + | ) -> Result<(u64, f64, String), Box<dyn Error + Send + Sync >> { |
|
| 75 | + | let request_data = JsonRpcRequest { |
|
| 76 | + | jsonrpc: "2.0".to_string(), |
|
| 77 | + | method: "eth_getBalance".to_string(), |
|
| 78 | + | params: vec![address.to_string(), "latest".to_string()], |
|
| 79 | + | id: 1, |
|
| 80 | + | }; |
|
| 81 | + | ||
| 82 | + | let response = client.post(&network.rpc_url) |
|
| 83 | + | .json(&request_data) |
|
| 84 | + | .send() |
|
| 85 | + | .await?; |
|
| 86 | + | ||
| 87 | + | let status = response.status(); |
|
| 88 | + | ||
| 89 | + | if !response.status().is_success() { |
|
| 90 | + | let error_text = response.text().await?; |
|
| 91 | + | return Err(format!("HTTP error {}: {}", status, error_text).into()); |
|
| 92 | + | } |
|
| 93 | + | ||
| 94 | + | let response_text = response.text().await?; |
|
| 95 | + | let response_body: JsonRpcResponse = serde_json::from_str(&response_text)?; |
|
| 96 | + | ||
| 97 | + | if let Some(hex_str) = response_body.result.strip_prefix("0x"){ |
|
| 98 | + | if let Ok(balance) = u128::from_str_radix(hex_str, 16){ |
|
| 99 | + | let eth_balance = balance as f64 / 1_000_000_000_000_000_000.0; |
|
| 100 | + | return Ok((network.chain_id, eth_balance, network.name.clone())); |
|
| 101 | + | } |
|
| 102 | + | } |
|
| 103 | + | Err(format!("Failed to parse balance for network {}", network.name).into()) |
|
| 104 | + | } |
|
| 105 | + | ||
| 106 | + | async fn fetch_all_balances( |
|
| 107 | + | address: &str, |
|
| 108 | + | networks: HashMap<u64, Network> |
|
| 109 | + | ) -> Result<Vec<(u64, f64, String)>, Box<dyn Error>> { |
|
| 110 | + | let client = Client::new(); |
|
| 111 | + | ||
| 112 | + | let mut tasks: Vec<JoinHandle<Result<(u64, f64, String), Box<dyn Error + Send + Sync>>>> = Vec::new(); |
|
| 113 | + | ||
| 114 | + | for (_, network) in networks { |
|
| 115 | + | let client_clone = client.clone(); |
|
| 116 | + | let address_clone = address.to_string(); |
|
| 117 | + | let network_clone = network; |
|
| 118 | + | ||
| 119 | + | let task = tokio::spawn(async move { |
|
| 120 | + | fetch_balance(&client_clone, &address_clone, &network_clone).await |
|
| 121 | + | }); |
|
| 122 | + | ||
| 123 | + | tasks.push(task); |
|
| 124 | + | } |
|
| 125 | + | ||
| 126 | + | let results = join_all(tasks).await; |
|
| 127 | + | ||
| 128 | + | let mut balances = Vec::new(); |
|
| 129 | + | for result in results { |
|
| 130 | + | match result { |
|
| 131 | + | Ok(Ok((chain_id, balance, name))) => { |
|
| 132 | + | balances.push((chain_id, balance, name)); |
|
| 133 | + | }, |
|
| 134 | + | Ok(Err(e)) => { |
|
| 135 | + | eprintln!("Error fetching balance: {}", e); |
|
| 136 | + | }, |
|
| 137 | + | Err(e) => { |
|
| 138 | + | eprintln!("Task error: {}", e); |
|
| 58 | 139 | } |
|
| 59 | 140 | } |
|
| 60 | 141 | } |
|
| 142 | + | Ok(balances) |
|
| 61 | 143 | } |
|
| 62 | 144 | ||
| 63 | - | fn main() -> Result<(), Box<dyn Error>> { |
|
| 145 | + | #[tokio::main] |
|
| 146 | + | async fn main() -> Result<(), Box<dyn Error>> { |
|
| 64 | 147 | let matches = Command::new("wallet-fetch") |
|
| 65 | 148 | .version("0.0.1") |
|
| 66 | 149 | .author("Steve Simkins") |
|
| 86 | 169 | } |
|
| 87 | 170 | }; |
|
| 88 | 171 | ||
| 89 | - | let rpc_url = match env::var("RPC_URL"){ |
|
| 90 | - | Ok(url) => url, |
|
| 91 | - | Err(_) => { |
|
| 92 | - | eprintln!("RPC URL not defined"); |
|
| 93 | - | return Err("RPC_URL environment variable not set".into()); |
|
| 94 | - | } |
|
| 95 | - | }; |
|
| 96 | - | ||
| 97 | - | let client = Client::new(); |
|
| 98 | - | ||
| 99 | - | let request_data = JsonRpcRequest { |
|
| 100 | - | jsonrpc: "2.0".to_string(), |
|
| 101 | - | method:"eth_getBalance".to_string(), |
|
| 102 | - | params: vec![address.to_string(), "latest".to_string()], |
|
| 103 | - | id: 1, |
|
| 104 | - | }; |
|
| 105 | - | ||
| 172 | + | let networks = collect_rpc_urls(); |
|
| 106 | 173 | ||
| 107 | - | let response = match client.post(&rpc_url) |
|
| 108 | - | .json(&request_data) |
|
| 109 | - | .send() { |
|
| 110 | - | Ok(resp) => resp, |
|
| 111 | - | Err(e) => { |
|
| 112 | - | eprintln!("Error sending request: {}", e); |
|
| 113 | - | return Err(e.into()); |
|
| 114 | - | } |
|
| 115 | - | }; |
|
| 116 | - | ||
| 117 | - | if !response.status().is_success() { |
|
| 118 | - | eprintln!("Error: HTTP status {}", response.status()); |
|
| 119 | - | eprintln!("Error: {}", response.text()?); |
|
| 120 | - | return Err(format!("Http error").into()); |
|
| 174 | + | if networks.is_empty(){ |
|
| 175 | + | eprintln!("RPC URLs are not defined"); |
|
| 121 | 176 | } |
|
| 122 | 177 | ||
| 123 | - | let response_text = response.text()?; |
|
| 178 | + | let balances = fetch_all_balances(&address, networks).await?; |
|
| 124 | 179 | ||
| 125 | - | let response_body: JsonRpcResponse = match |
|
| 126 | - | serde_json::from_str(&response_text){ |
|
| 127 | - | Ok(body) => body, |
|
| 128 | - | Err(e) => { |
|
| 129 | - | eprintln!("Error parsing response: {}", e); |
|
| 130 | - | return Err(e.into()); |
|
| 180 | + | if balances.is_empty(){ |
|
| 181 | + | println!("No balances found for address {}", address); |
|
| 182 | + | } else { |
|
| 183 | + | println!("Balances for {}", address); |
|
| 184 | + | println!("------------------------"); |
|
| 185 | + | for (_, balance, name) in balances { |
|
| 186 | + | println!("{}: {:.4} ETH", name, balance); |
|
| 131 | 187 | } |
|
| 132 | - | }; |
|
| 133 | - | ||
| 134 | - | if let Some(hex_str) = response_body.result.strip_prefix("0x"){ |
|
| 135 | - | if let Ok(balance) = u128::from_str_radix(hex_str, 16){ |
|
| 136 | - | let eth_balance = balance as f64 / 1_000_000_000_000_000_000.0; |
|
| 137 | - | println!("Balance in ETH: {:.4}", eth_balance); |
|
| 138 | - | } |
|
| 139 | - | } |
|
| 188 | + | } |
|
| 140 | 189 | ||
| 141 | 190 | Ok(()) |
|
| 142 | 191 | } |
|