From cf8968168f7395d27fbb6976054dfb8006a0734e Mon Sep 17 00:00:00 2001 From: PablitoAmaral Date: Sat, 14 Jun 2025 20:23:20 +0100 Subject: [PATCH 1/2] refactor(scripts): refactoring the emv in the validate scripts --- scripts/validate-tokens/Cargo.lock | 123 ++++++++- scripts/validate-tokens/Cargo.toml | 1 + scripts/validate-tokens/src/main.rs | 250 ++++++++++-------- scripts/validate-tokens/tokenlist_config.json | 40 ++- 4 files changed, 294 insertions(+), 120 deletions(-) diff --git a/scripts/validate-tokens/Cargo.lock b/scripts/validate-tokens/Cargo.lock index 893a690..0221e12 100644 --- a/scripts/validate-tokens/Cargo.lock +++ b/scripts/validate-tokens/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -493,6 +493,56 @@ dependencies = [ "url", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -823,6 +873,52 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "const-hex" version = "1.14.1" @@ -1628,6 +1724,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -1883,6 +1985,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "openssl" version = "0.10.72" @@ -2667,6 +2775,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.27.1" @@ -3040,12 +3154,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "validate-tokens" version = "0.1.0" dependencies = [ "alloy", "base64 0.21.7", + "clap", "serde", "serde_json", "tokio", diff --git a/scripts/validate-tokens/Cargo.toml b/scripts/validate-tokens/Cargo.toml index 46484f7..97c1b62 100644 --- a/scripts/validate-tokens/Cargo.toml +++ b/scripts/validate-tokens/Cargo.toml @@ -9,3 +9,4 @@ tokio = { version = "1", features = ["full"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" base64 = "0.21" +clap = { version = "4.5.39", features = ["derive"] } diff --git a/scripts/validate-tokens/src/main.rs b/scripts/validate-tokens/src/main.rs index ecaa726..eb2ce6b 100644 --- a/scripts/validate-tokens/src/main.rs +++ b/scripts/validate-tokens/src/main.rs @@ -4,12 +4,20 @@ use alloy::sol; use alloy::transports::http::reqwest::Url; use alloy::transports::http::{Client, Http}; use base64::{engine::general_purpose, Engine as _}; +use clap::Parser; use serde::Deserialize; use std::collections::HashMap; use std::env; use std::fs; use std::str::FromStr; +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg(long)] + config: String, +} + sol! { #[sol(rpc)] interface IERC20 { @@ -21,7 +29,8 @@ sol! { #[derive(Debug, Deserialize)] struct Config { - files: HashMap, + path: String, + rpc_type: String, } #[derive(Debug, Deserialize)] @@ -33,62 +42,49 @@ struct Token { decimals: u8, } -#[derive(Deserialize)] -struct TokenInfoResponse { - data: TokenInfo, -} +// #[derive(Deserialize)] +// struct TokenInfoResponse { +// data: TokenInfo, +// } -#[derive(Deserialize)] -struct TokenInfo { - name: String, - symbol: String, - decimals: u8, -} +// #[derive(Deserialize)] +// struct TokenInfo { +// name: String, +// symbol: String, +// decimals: u8, +// } #[tokio::main] async fn main() -> Result<(), Box> { - let config_path = env::args() - .skip_while(|arg| arg != "--config") - .nth(1) - .unwrap_or("tokenlist_config.json".to_string()); - - let config_content = fs::read_to_string(config_path)?; - let config: Config = serde_json::from_str(&config_content)?; - + let args = Args::parse(); + let config_content = fs::read_to_string(args.config)?; + let config: HashMap = serde_json::from_str(&config_content)?; let mut error_count = 0; - for (network, path) in config.files { - println!("\nšŸ” Checking file: {path} for network: {network}"); + for (chain_id, cfg) in config { + println!("\nšŸ” Checking file: {} for network: {}", cfg.path, chain_id); - let content = fs::read_to_string(path)?; + let content = fs::read_to_string(cfg.path)?; let json: serde_json::Value = serde_json::from_str(&content)?; let token_array = json.get("tokens").ok_or("Missing 'tokens' field")?; let tokens: Vec = serde_json::from_value(token_array.clone())?; - if let Some(provider) = get_ethereum_provider(&network) { - for token in tokens { - match verify_on_ethereum(&token, &provider).await { - Ok(errors) => error_count += errors, - Err(e) => { - eprintln!("āŒ Failed to verify {}: {}", token.symbol, e); - error_count += 1; - } - } - } - } else if network == "babylon" { - for token in tokens { - match verify_on_babylon(&token).await { - Ok(errors) => error_count += errors, - Err(e) => { - eprintln!("āŒ Failed to verify {}: {}", token.symbol, e); - error_count += 1; - } - } - } - } else { - println!("āš ļø Unknown network: {}", network); + match cfg.rpc_type.as_str() { + "evm" => error_count += evm(&chain_id, tokens).await, + "cosmos" => error_count += cosmos().await, + _ => panic!("invalid rpc_type"), } + + // } else if network == "babylon" { + // for token in tokens { + // match verify_on_cosmos(&token).await { + // Ok(errors) => error_count += errors, + // } + // } + // } else { + // println!("; + // } } if error_count > 0 { @@ -100,7 +96,40 @@ async fn main() -> Result<(), Box> { Ok(()) } -async fn verify_on_ethereum( +async fn evm(chain_id: &String, tokens: Vec) -> u32 { + let mut error_count = 0; + if let Some(provider) = get_evm_provider(chain_id) { + for token in tokens { + match verify_on_evm(&token, &provider).await { + Ok(erros) => error_count += erros, + Err(e) => { + eprintln!("āŒ Failed to verify {}: {}", token.symbol, e); + error_count += 1; + } + } + } + } else { + println!( + "āš ļø Unable to initialize provider for network: {}", + &chain_id + ); + } + error_count +} + +fn get_evm_provider(network: &str) -> Option>> { + let provider_suffix = env::var("RPC_PROVIDER").ok()?; + let subdomain = match network.split_once('.') { + Some((left, right)) => format!("{}.{}", right, left), + None => network.to_owned(), + }; + let full_url = format!("https://rpc.{subdomain}{}", provider_suffix); + let provider = ProviderBuilder::new().on_http(Url::parse(&full_url).ok()?); + + Some(provider) +} + +async fn verify_on_evm( token: &Token, provider: &RootProvider>, ) -> Result> { @@ -149,75 +178,68 @@ async fn verify_on_ethereum( Ok(local_error_count) } -fn get_ethereum_provider(network: &str) -> Option>> { - let provider_suffix = env::var("RPC_PROVIDER").ok()?; - - let subdomain = match network { - "ethereum" => "1.ethereum", - "bob" => "60808.bob", - "corn" => "21000000.corn", - _ => return None, - }; - - let full_url = format!("https://rpc.{subdomain}{}", provider_suffix); - let provider = ProviderBuilder::new().on_http(Url::parse(&full_url).ok()?); +async fn cosmos() -> u32 { + let mut error_count = 0; + println!("Im in the cosmos function"); - Some(provider) + error_count } -async fn verify_on_babylon(token: &Token) -> Result> { - let client = Client::new(); - - if token.address == "ubbn" { - println!("ā„¹ļø Skipping non-contract token: {}", token.symbol); - return Ok(0); - } - - if token.address.starts_with("ibc/") { - println!("ā„¹ļø Skipping IBC token: {}", token.symbol); - return Ok(0); - } - - let msg = serde_json::json!({ "token_info": {} }); - let query = general_purpose::STANDARD.encode(msg.to_string()); - - let rpc_url = "https://babylon.nodes.guru"; - let url = format!( - "{}/api/cosmwasm/wasm/v1/contract/{}/smart/{}", - rpc_url, token.address, query - ); - - let res: TokenInfoResponse = client.get(&url).send().await?.json().await?; - - let mut local_error_count = 0; - - if token.symbol != res.data.symbol { - println!( - "[{}] āŒ Symbol mismatch: JSON = {}, On-chain = {}", - token.symbol, token.symbol, res.data.symbol - ); - local_error_count += 1; - } - - if token.name != res.data.name { - println!( - "[{}] āŒ Name mismatch: JSON = {}, On-chain = {}", - token.symbol, token.name, res.data.name - ); - local_error_count += 1; - } - - if token.decimals != res.data.decimals { - println!( - "[{}] āŒ Decimals mismatch: JSON = {}, On-chain = {}", - token.symbol, token.decimals, res.data.decimals - ); - local_error_count += 1; - } - - if local_error_count == 0 { - println!("[{}] āœ… Token is valid ", token.symbol); - } - - Ok(local_error_count) -} +fn get_rpc(network: &str) {} + +// async fn verify_on_cosmos(token: &Token) -> Result> { +// let client = Client::new(); + +// if token.address == "ubbn" { +// println!("ā„¹ļø Skipping non-contract token: {}", token.symbol); +// return Ok(0); +// } + +// if token.address.starts_with("ibc/") { +// println!("ā„¹ļø Skipping IBC token: {}", token.symbol); +// return Ok(0); +// } + +// let msg = serde_json::json!({ "token_info": {} }); +// let query = general_purpose::STANDARD.encode(msg.to_string()); + +// let rpc_url = "https://babylon.nodes.guru"; +// let url = format!( +// "{}/api/cosmwasm/wasm/v1/contract/{}/smart/{}", +// rpc_url, token.address, query +// ); + +// let res: TokenInfoResponse = client.get(&url).send().await?.json().await?; + +// let mut local_error_count = 0; + +// if token.symbol != res.data.symbol { +// println!( +// "[{}] āŒ Symbol mismatch: JSON = {}, On-chain = {}", +// token.symbol, token.symbol, res.data.symbol +// ); +// local_error_count += 1; +// } + +// if token.name != res.data.name { +// println!( +// "[{}] āŒ Name mismatch: JSON = {}, On-chain = {}", +// token.symbol, token.name, res.data.name +// ); +// local_error_count += 1; +// } + +// if token.decimals != res.data.decimals { +// println!( +// "[{}] āŒ Decimals mismatch: JSON = {}, On-chain = {}", +// token.symbol, token.decimals, res.data.decimals +// ); +// local_error_count += 1; +// } + +// if local_error_count == 0 { +// println!("[{}] āœ… Token is valid ", token.symbol); +// } + +// Ok(local_error_count) +// } diff --git a/scripts/validate-tokens/tokenlist_config.json b/scripts/validate-tokens/tokenlist_config.json index 462c82f..6d475d7 100644 --- a/scripts/validate-tokens/tokenlist_config.json +++ b/scripts/validate-tokens/tokenlist_config.json @@ -1,8 +1,38 @@ { - "files": { - "ethereum": "../../data/ethereum.1/tokenlist.json", - "bob": "../../data/bob.60808/tokenlist.json", - "corn": "../../data/corn.21000000/tokenlist.json", - "babylon": "../../data/babylon.bbn-1/tokenlist.json" + "corn.21000000": { + "path": "../../data/corn.21000000/tokenlist.json", + "rpc_type": "evm" + }, + "babylon.bbn-1": { + "path": "../../data/babylon.bbn-1/tokenlist.json", + "rpc_type": "cosmos" + }, + "bob.60808": { + "path": "../../data/bob.60808/tokenlist.json", + "rpc_type": "evm" + }, + "ethereum.1": { + "path": "../../data/ethereum.1/tokenlist.json", + "rpc_type": "evm" + }, + "sepolia.11155111": { + "path": "../../data/sepolia.11155111/tokenlist.json", + "rpc_type": "evm" + }, + "corn.21000001": { + "path": "../../data/corn.21000001/tokenlist.json", + "rpc_type": "evm" + }, + "bob.808813": { + "path": "../../data/bob.808813/tokenlist.json", + "rpc_type": "evm" + }, + "holesky.17000": { + "path": "../../data/holesky.17000/tokenlist.json", + "rpc_type": "evm" + }, + "sei.1328": { + "path": "../../data/sei.1328/tokenlist.json", + "rpc_type": "evm" } } From a405d5e5430d9d211ae2389dfbe9ae50ef4d47f4 Mon Sep 17 00:00:00 2001 From: PablitoAmaral Date: Sat, 14 Jun 2025 23:24:56 +0100 Subject: [PATCH 2/2] feat(validate): support generic CW20 token checks on Cosmos chains --- scripts/validate-tokens/Cargo.lock | 10 ++ scripts/validate-tokens/Cargo.toml | 1 + scripts/validate-tokens/src/main.rs | 167 ++++++++++++++-------------- 3 files changed, 95 insertions(+), 83 deletions(-) diff --git a/scripts/validate-tokens/Cargo.lock b/scripts/validate-tokens/Cargo.lock index 0221e12..3f806f2 100644 --- a/scripts/validate-tokens/Cargo.lock +++ b/scripts/validate-tokens/Cargo.lock @@ -2809,6 +2809,15 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + [[package]] name = "syn" version = "1.0.109" @@ -3169,6 +3178,7 @@ dependencies = [ "clap", "serde", "serde_json", + "subtle-encoding", "tokio", ] diff --git a/scripts/validate-tokens/Cargo.toml b/scripts/validate-tokens/Cargo.toml index 97c1b62..70aa3d3 100644 --- a/scripts/validate-tokens/Cargo.toml +++ b/scripts/validate-tokens/Cargo.toml @@ -10,3 +10,4 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" base64 = "0.21" clap = { version = "4.5.39", features = ["derive"] } +subtle-encoding = { version = "0.5", features = ["bech32-preview"] } diff --git a/scripts/validate-tokens/src/main.rs b/scripts/validate-tokens/src/main.rs index eb2ce6b..0223fc2 100644 --- a/scripts/validate-tokens/src/main.rs +++ b/scripts/validate-tokens/src/main.rs @@ -42,17 +42,17 @@ struct Token { decimals: u8, } -// #[derive(Deserialize)] -// struct TokenInfoResponse { -// data: TokenInfo, -// } - -// #[derive(Deserialize)] -// struct TokenInfo { -// name: String, -// symbol: String, -// decimals: u8, -// } +#[derive(Deserialize)] +struct TokenInfoResponse { + data: TokenInfo, +} + +#[derive(Deserialize)] +struct TokenInfo { + name: String, + symbol: String, + decimals: u8, +} #[tokio::main] async fn main() -> Result<(), Box> { @@ -72,19 +72,9 @@ async fn main() -> Result<(), Box> { match cfg.rpc_type.as_str() { "evm" => error_count += evm(&chain_id, tokens).await, - "cosmos" => error_count += cosmos().await, + "cosmos" => error_count += cosmos(&chain_id, tokens).await, _ => panic!("invalid rpc_type"), } - - // } else if network == "babylon" { - // for token in tokens { - // match verify_on_cosmos(&token).await { - // Ok(errors) => error_count += errors, - // } - // } - // } else { - // println!("; - // } } if error_count > 0 { @@ -123,7 +113,7 @@ fn get_evm_provider(network: &str) -> Option>> { Some((left, right)) => format!("{}.{}", right, left), None => network.to_owned(), }; - let full_url = format!("https://rpc.{subdomain}{}", provider_suffix); + let full_url = format!("https://rpc.{subdomain}{provider_suffix}"); let provider = ProviderBuilder::new().on_http(Url::parse(&full_url).ok()?); Some(provider) @@ -178,68 +168,79 @@ async fn verify_on_evm( Ok(local_error_count) } -async fn cosmos() -> u32 { +async fn cosmos(network: &str, tokens: Vec) -> u32 { let mut error_count = 0; - println!("Im in the cosmos function"); + let client = Client::new(); + let rpc_url = get_rpc_url(network).unwrap(); + let msg = serde_json::json!({ "token_info": {} }); + let query = general_purpose::STANDARD.encode(msg.to_string()); + + for token in tokens { + if subtle_encoding::bech32::decode(&token.address).is_ok() { + match cw_20(&token, &rpc_url, &query, &client).await { + Ok(errors) => error_count += errors, + Err(e) => { + eprintln!("āŒ Failed to verify {}: {}", token.symbol, e); + error_count += 1; + } + } + } + } error_count } -fn get_rpc(network: &str) {} - -// async fn verify_on_cosmos(token: &Token) -> Result> { -// let client = Client::new(); - -// if token.address == "ubbn" { -// println!("ā„¹ļø Skipping non-contract token: {}", token.symbol); -// return Ok(0); -// } - -// if token.address.starts_with("ibc/") { -// println!("ā„¹ļø Skipping IBC token: {}", token.symbol); -// return Ok(0); -// } - -// let msg = serde_json::json!({ "token_info": {} }); -// let query = general_purpose::STANDARD.encode(msg.to_string()); - -// let rpc_url = "https://babylon.nodes.guru"; -// let url = format!( -// "{}/api/cosmwasm/wasm/v1/contract/{}/smart/{}", -// rpc_url, token.address, query -// ); - -// let res: TokenInfoResponse = client.get(&url).send().await?.json().await?; - -// let mut local_error_count = 0; - -// if token.symbol != res.data.symbol { -// println!( -// "[{}] āŒ Symbol mismatch: JSON = {}, On-chain = {}", -// token.symbol, token.symbol, res.data.symbol -// ); -// local_error_count += 1; -// } - -// if token.name != res.data.name { -// println!( -// "[{}] āŒ Name mismatch: JSON = {}, On-chain = {}", -// token.symbol, token.name, res.data.name -// ); -// local_error_count += 1; -// } - -// if token.decimals != res.data.decimals { -// println!( -// "[{}] āŒ Decimals mismatch: JSON = {}, On-chain = {}", -// token.symbol, token.decimals, res.data.decimals -// ); -// local_error_count += 1; -// } - -// if local_error_count == 0 { -// println!("[{}] āœ… Token is valid ", token.symbol); -// } - -// Ok(local_error_count) -// } +fn get_rpc_url(network: &str) -> Option { + let provider_suffix = env::var("RPC_PROVIDER").ok()?; + let subdomain = match network.split_once('.') { + Some((right, left)) => format!("{}.{}", left, right), + None => network.to_owned(), + }; + Some(format!("https://rest.{subdomain}{provider_suffix}")) +} + +async fn cw_20( + token: &Token, + rpc_url: &str, + query: &str, + client: &Client, +) -> Result> { + let url = format!( + "{}/cosmwasm/wasm/v1/contract/{}/smart/{}", + rpc_url, token.address, query + ); + + let res: TokenInfoResponse = client.get(&url).send().await?.json().await?; + + let mut local_error_count = 0; + + if token.symbol != res.data.symbol { + println!( + "[{}] āŒ Symbol mismatch: JSON = {}, On-chain = {}", + token.symbol, token.symbol, res.data.symbol + ); + local_error_count += 1; + } + + if token.name != res.data.name { + println!( + "[{}] āŒ Name mismatch: JSON = {}, On-chain = {}", + token.symbol, token.name, res.data.name + ); + local_error_count += 1; + } + + if token.decimals != res.data.decimals { + println!( + "[{}] āŒ Decimals mismatch: JSON = {}, On-chain = {}", + token.symbol, token.decimals, res.data.decimals + ); + local_error_count += 1; + } + + if local_error_count == 0 { + println!("[{}] āœ… Token is valid ", token.symbol); + } + + Ok(local_error_count) +}