feat: Added ENS support
cfdcefc9
3 file(s) · +222 −1
| 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]] |
|
| 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" |
| 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 | ||