feat: Added ENS support cfdcefc9
stevedylandev · 2025-07-08 18:59 3 file(s) · +222 −1
Cargo.lock +48 −0
219 219
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
220 220
221 221
[[package]]
222 +
name = "crunchy"
223 +
version = "0.2.4"
224 +
source = "registry+https://github.com/rust-lang/crates.io-index"
225 +
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
226 +
227 +
[[package]]
222 228
name = "dirs"
223 229
version = "5.0.1"
224 230
source = "registry+https://github.com/rust-lang/crates.io-index"
465 471
version = "0.5.0"
466 472
source = "registry+https://github.com/rust-lang/crates.io-index"
467 473
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
474 +
475 +
[[package]]
476 +
name = "hex"
477 +
version = "0.4.3"
478 +
source = "registry+https://github.com/rust-lang/crates.io-index"
479 +
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
468 480
469 481
[[package]]
470 482
name = "http"
1291 1303
]
1292 1304
1293 1305
[[package]]
1306 +
name = "tiny-keccak"
1307 +
version = "2.0.2"
1308 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1309 +
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
1310 +
dependencies = [
1311 +
 "crunchy",
1312 +
]
1313 +
1314 +
[[package]]
1294 1315
name = "tinystr"
1295 1316
version = "0.8.1"
1296 1317
source = "registry+https://github.com/rust-lang/crates.io-index"
1299 1320
 "displaydoc",
1300 1321
 "zerovec",
1301 1322
]
1323 +
1324 +
[[package]]
1325 +
name = "tinyvec"
1326 +
version = "1.9.0"
1327 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1328 +
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
1329 +
dependencies = [
1330 +
 "tinyvec_macros",
1331 +
]
1332 +
1333 +
[[package]]
1334 +
name = "tinyvec_macros"
1335 +
version = "0.1.1"
1336 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1337 +
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
1302 1338
1303 1339
[[package]]
1304 1340
name = "tokio"
1433 1469
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
1434 1470
1435 1471
[[package]]
1472 +
name = "unicode-normalization"
1473 +
version = "0.1.24"
1474 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1475 +
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
1476 +
dependencies = [
1477 +
 "tinyvec",
1478 +
]
1479 +
1480 +
[[package]]
1436 1481
name = "unicode-width"
1437 1482
version = "0.2.1"
1438 1483
source = "registry+https://github.com/rust-lang/crates.io-index"
1475 1520
 "colored",
1476 1521
 "dirs",
1477 1522
 "futures",
1523 +
 "hex",
1478 1524
 "indicatif",
1479 1525
 "reqwest",
1480 1526
 "serde",
1481 1527
 "serde_json",
1528 +
 "tiny-keccak",
1482 1529
 "tokio",
1483 1530
 "toml",
1531 +
 "unicode-normalization",
1484 1532
]
1485 1533
1486 1534
[[package]]
Cargo.toml +3 −0
14 14
dirs = "5.0"
15 15
indicatif = "0.17"
16 16
colored = "2.0"
17 +
tiny-keccak = { version = "2.0.2", features = ["keccak"]}
18 +
unicode-normalization = "0.1.24"
19 +
hex = "0.4.3"
src/main.rs +171 −1
9 9
use dirs;
10 10
use indicatif::{ProgressBar, ProgressStyle};
11 11
use colored::*;
12 +
use tiny_keccak::{Hasher, Keccak};
13 +
use unicode_normalization::UnicodeNormalization;
14 +
use hex;
12 15
13 16
#[derive(Serialize)]
14 17
struct JsonRpcRequest {
68 71
enum BalanceResult {
69 72
  Native(f64, String),
70 73
  Token(TokenBalance),
74 +
}
75 +
76 +
fn keccak256(data: &[u8]) -> [u8; 32] {
77 +
  let mut hasher = Keccak::v256();
78 +
  let mut output = [0u8; 32];
79 +
  hasher.update(data);
80 +
  hasher.finalize(&mut output);
81 +
  output
82 +
}
83 +
84 +
fn normalize_name(name: &str) -> String {
85 +
  name.nfkc().collect::<String>().to_lowercase()
86 +
}
87 +
88 +
fn namehash(name: &str) -> [u8; 32] {
89 +
  let normalized = normalize_name(name);
90 +
91 +
  let mut node = [0u8; 32];
92 +
93 +
  if normalized.is_empty(){
94 +
    return node;
95 +
  }
96 +
97 +
  let labels: Vec<&str> = normalized.split('.').collect();
98 +
99 +
  for label in labels.iter().rev() {
100 +
    let label_hash = keccak256(label.as_bytes());
101 +
102 +
    let mut combined = [0u8; 64];
103 +
    combined[0..32].copy_from_slice(&node);
104 +
    combined[32..64].copy_from_slice(&label_hash);
105 +
106 +
    node = keccak256(&combined);
107 +
  }
108 +
  node
71 109
}
72 110
73 111
fn read_config() -> Result<Config, Box<dyn Error>> {
288 326
  Ok(balances)
289 327
}
290 328
329 +
async fn get_resolver_address(
330 +
  client: &Client,
331 +
  rpc_url: &str,
332 +
  namehash: [u8; 32]
333 +
) -> Result<String, Box<dyn Error>>{
334 +
  let registry_address = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e";
335 +
336 +
  let namehash_hex = format!("0x{}", hex::encode(namehash));
337 +
338 +
  let data = format!("0x0178b8bf{}", &namehash_hex[2..]);
339 +
340 +
  let request_data = serde_json::json!({
341 +
    "jsonrpc": "2.0",
342 +
    "method": "eth_call",
343 +
    "params": [
344 +
      {
345 +
        "to": registry_address,
346 +
        "data": data
347 +
      },
348 +
      "latest"
349 +
    ],
350 +
    "id": 1
351 +
  });
352 +
353 +
  let response = client.post(rpc_url)
354 +
    .json(&request_data)
355 +
    .send()
356 +
    .await?;
357 +
358 +
  let status = response.status();
359 +
360 +
  if !status.is_success(){
361 +
    let error_text = response.text().await?;
362 +
    return Err(format!("HTTP error {}: {}", status, error_text).into());
363 +
  }
364 +
365 +
  let response_text = response.text().await?;
366 +
  let response_body: JsonRpcResponse = serde_json::from_str(&response_text)?;
367 +
368 +
  if response_body.result == "0x0000000000000000000000000000000000000000" {
369 +
    return Err("No resolver found for this name".into());
370 +
  }
371 +
372 +
  let address = format!("0x{}", &response_body.result[26..]);
373 +
374 +
  Ok(address)
375 +
}
376 +
377 +
async fn resolve_ens_address(
378 +
  client: &Client,
379 +
  rpc_url: &str,
380 +
  resolver_address: &str,
381 +
  namehash: [u8; 32]
382 +
) -> Result<String, Box<dyn Error>>{
383 +
384 +
  let namehash_hex = format!("0x{}", hex::encode(namehash));
385 +
386 +
  println!("Namehash hex: {}", namehash_hex);
387 +
388 +
  let data = format!("0x3b3b57de{}", &namehash_hex[2..]);
389 +
390 +
  let request_data = serde_json::json!({
391 +
    "jsonrpc": "2.0",
392 +
    "method": "eth_call",
393 +
    "params": [
394 +
      {
395 +
        "to": resolver_address,
396 +
        "data": data
397 +
      },
398 +
      "latest"
399 +
    ],
400 +
    "id": 1
401 +
  });
402 +
403 +
  let response = client.post(rpc_url)
404 +
    .json(&request_data)
405 +
    .send()
406 +
    .await?;
407 +
408 +
  let status = response.status();
409 +
410 +
  if !status.is_success(){
411 +
    let error_text = response.text().await?;
412 +
    return Err(format!("HTTP error {}: {}", status, error_text).into());
413 +
  }
414 +
415 +
  let response_text = response.text().await?;
416 +
  let response_body: JsonRpcResponse = serde_json::from_str(&response_text)?;
417 +
418 +
  if response_body.result == "0x0000000000000000000000000000000000000000" {
419 +
    return Err("No address found for this name".into());
420 +
  }
421 +
422 +
  let address = format!("0x{}", &response_body.result[26..]);
423 +
424 +
  Ok(address)
425 +
}
426 +
427 +
async fn resolve_address_or_ens(
428 +
  client: &Client,
429 +
  input: &str,
430 +
  networks: &HashMap<u64, Network>
431 +
) -> Result<String, Box<dyn Error>> {
432 +
  if input.to_lowercase().ends_with(".eth"){
433 +
    let mainnet = networks.get(&1);
434 +
435 +
    if let Some(network) = mainnet {
436 +
      let namehash = namehash(input);
437 +
438 +
      let resolver_address = get_resolver_address(client, &network.rpc_url, namehash).await?;
439 +
440 +
      println!("Resolver address: {}", resolver_address);
441 +
442 +
      let eth_address = resolve_ens_address(client, &network.rpc_url, &resolver_address, namehash).await?;
443 +
444 +
      return Ok(eth_address);
445 +
    } else {
446 +
      return Err("Ethereum mainnet configuration not found for ENS resolution".into());
447 +
    }
448 +
  } else {
449 +
    if !input.starts_with("0x") || input.len() != 42 {
450 +
      return Err("Invalid Ethereum address format".into());
451 +
    }
452 +
453 +
    Ok(input.to_string())
454 +
  }
455 +
}
456 +
291 457
#[tokio::main]
292 458
async fn main() -> Result<(), Box<dyn Error>> {
293 459
    let matches = Command::new("wallet-fetch")
304 470
305 471
    let config = read_config()?;
306 472
307 -
    let address = match matches.get_one::<String>("address"){
473 +
    let input = match matches.get_one::<String>("address"){
308 474
      Some(addr) if !addr.is_empty() => addr.to_string(),
309 475
      _ => match &config.address {
310 476
        Some(addr) if !addr.is_empty() => addr.clone(),
320 486
    if networks.is_empty(){
321 487
      eprintln!("RPC URLs are not defined");
322 488
    }
489 +
490 +
    let client = Client::new();
491 +
492 +
    let address = resolve_address_or_ens(&client, &input, &networks).await?;
323 493
324 494
    let balances = fetch_all_balances(&address, networks).await?;
325 495