feat: ✨ Added main mvp for fetching balances for multiple chains d7690aa0
Steve Simkins · 2025-07-03 10:20 2 file(s) · +108 −59
Cargo.toml +1 −1
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"
src/main.rs +107 −58
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
}