diff --git a/.github/README.md b/.github/README.md index 8a9da7d..b3f7b2b 100644 --- a/.github/README.md +++ b/.github/README.md @@ -73,8 +73,8 @@ cargo uninstall book-of-profits | `✅` | Show global balance | | `✅` | Export raw configuration in plaintext | | `✅` | Display spinner when loading | +| `✅` | Fallback RPCs | | `⚠️` Not supported in EVM | Scan for token holdings in account and automatically add them | -| `❌` | Fallback RPCs | | `❌` | Show balance by chain | | `❌` | Show balance by account | | `❌` | Automatically prune low liquidity tokens | diff --git a/src/chain/chain_properties.rs b/src/chain/chain_properties.rs index e84a109..f9aa5c6 100644 --- a/src/chain/chain_properties.rs +++ b/src/chain/chain_properties.rs @@ -4,7 +4,7 @@ use std::fmt::Display; #[derive(Debug, Clone)] pub struct ChainProperties { - pub rpc_url: Url, + pub rpc_urls: Vec, pub rpc_headers: HeaderMap, pub name: String, pub native_token: Token, diff --git a/src/chain/evm_chain.rs b/src/chain/evm_chain.rs index 9d8af90..917783a 100644 --- a/src/chain/evm_chain.rs +++ b/src/chain/evm_chain.rs @@ -27,7 +27,12 @@ impl From<&Chain> for EvmChain { } impl EvmChain { - async fn rpc_call(&self, method: &str, params: Value) -> (Option, Option) { + async fn rpc_call( + &self, + method: &str, + params: Value, + rpc_index: usize, + ) -> (Option, Option) { let payload = json!({ "jsonrpc": "2.0", "id": "1", @@ -36,7 +41,7 @@ impl EvmChain { }); let response = match self .http_client - .post(self.properties.rpc_url.clone()) + .post(self.properties.rpc_urls[rpc_index % self.properties.rpc_urls.len()].clone()) .json(&payload) .send() .await @@ -58,9 +63,13 @@ impl EvmChain { } impl ChainOps for EvmChain { - async fn get_native_token_balance(&self, address: &str) -> (Option, Option) { + async fn get_native_token_balance( + &self, + address: &str, + rpc_index: usize, + ) -> (Option, Option) { let (balance_hex, wait_time) = self - .rpc_call("eth_getBalance", json!([address, "latest"])) + .rpc_call("eth_getBalance", json!([address, "latest"]), rpc_index) .await; ( balance_hex.and_then(|b| BigUint::parse_bytes(&b.as_bytes()[2..], 16)), @@ -71,6 +80,7 @@ impl ChainOps for EvmChain { &self, token: &Token, address: &str, + rpc_index: usize, ) -> (Option, Option) { let params = json!([ { @@ -79,16 +89,20 @@ impl ChainOps for EvmChain { }, "latest" ]); - let (balance_hex, wait_time) = self.rpc_call("eth_call", params).await; + let (balance_hex, wait_time) = self.rpc_call("eth_call", params, rpc_index).await; ( balance_hex.and_then(|b| BigUint::parse_bytes(&b.as_bytes()[2..], 16)), wait_time, ) } - async fn get_holdings_balance(&self, _address: &str) -> SupportOption> { + async fn get_holdings_balance( + &self, + _address: &str, + _rpc_index: usize, + ) -> SupportOption> { SupportOption::Unsupported } - async fn get_token_decimals(&self, token_address: &str) -> Option { + async fn get_token_decimals(&self, token_address: &str, rpc_index: usize) -> Option { let params = json!([ { "to": token_address, @@ -96,12 +110,16 @@ impl ChainOps for EvmChain { }, "latest" ]); - let decimals_hex = self.rpc_call("eth_call", params).await.0?; + let decimals_hex = self.rpc_call("eth_call", params, rpc_index).await.0?; BigUint::parse_bytes(&decimals_hex.as_bytes()[2..], 16)? .to_usize() .into() } - async fn scan_for_tokens(&self, _address: &str) -> SupportOption> { + async fn scan_for_tokens( + &self, + _address: &str, + _rpc_index: usize, + ) -> SupportOption> { SupportOption::Unsupported } fn parse_wallet_address(&self, address: &str) -> Option { diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 0bd6ebb..745e498 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -28,14 +28,14 @@ pub struct Chain { impl Chain { pub fn new( chain_type: ChainType, - rpc_url: &str, + rpc_urls: Vec<&str>, name: &str, native_token_symbol: &str, native_token_address: &str, native_token_decimals: usize, ) -> Self { let properties = ChainProperties { - rpc_url: Url::from_str(rpc_url).unwrap(), + rpc_urls: rpc_urls.iter().map(|u| Url::from_str(u).unwrap()).collect(), rpc_headers: HeaderMap::new(), name: name.to_string(), native_token: Token::hardcode( @@ -53,19 +53,28 @@ impl Chain { } pub trait ChainOps { - async fn get_native_token_balance(&self, address: &str) -> (Option, Option); + async fn get_native_token_balance( + &self, + address: &str, + rpc_index: usize, + ) -> (Option, Option); async fn get_token_balance( &self, token: &Token, address: &str, + rpc_index: usize, ) -> (Option, Option); - async fn get_token_decimals(&self, token_address: &str) -> Option; - async fn get_token_symbol(&self, token_address: &str) -> Option { + async fn get_token_decimals(&self, token_address: &str, rpc_index: usize) -> Option; + async fn get_token_symbol(&self, token_address: &str, _rpc_index: usize) -> Option { let pairs = dexscreener::pairs::get_pairs(vec![token_address]).await?; (pairs.len() != 0).then(|| pairs[0].base_token.symbol.clone()) } - async fn get_holdings_balance(&self, address: &str) -> SupportOption>; - async fn scan_for_tokens(&self, address: &str) -> SupportOption>; + async fn get_holdings_balance( + &self, + address: &str, + rpc_index: usize, + ) -> SupportOption>; + async fn scan_for_tokens(&self, address: &str, rpc_index: usize) -> SupportOption>; fn parse_wallet_address(&self, address: &str) -> Option; fn parse_token_address(&self, address: &str) -> Option { self.parse_wallet_address(address) @@ -90,27 +99,36 @@ macro_rules! chain_ops_method { } impl ChainOps for Chain { - async fn get_native_token_balance(&self, address: &str) -> (Option, Option) { - chain_ops_method!(self, get_native_token_balance, address; await) + async fn get_native_token_balance( + &self, + address: &str, + rpc_index: usize, + ) -> (Option, Option) { + chain_ops_method!(self, get_native_token_balance, address, rpc_index; await) } async fn get_token_balance( &self, token: &Token, address: &str, + rpc_index: usize, ) -> (Option, Option) { - chain_ops_method!(self, get_token_balance, token, address; await) + chain_ops_method!(self, get_token_balance, token, address, rpc_index; await) } - async fn get_holdings_balance(&self, address: &str) -> SupportOption> { - chain_ops_method!(self, get_holdings_balance, address; await) + async fn get_holdings_balance( + &self, + address: &str, + rpc_index: usize, + ) -> SupportOption> { + chain_ops_method!(self, get_holdings_balance, address, rpc_index; await) } - async fn get_token_decimals(&self, token_address: &str) -> Option { - chain_ops_method!(self, get_token_decimals, token_address; await) + async fn get_token_decimals(&self, token_address: &str, rpc_index: usize) -> Option { + chain_ops_method!(self, get_token_decimals, token_address, rpc_index; await) } - async fn get_token_symbol(&self, token_address: &str) -> Option { - chain_ops_method!(self, get_token_symbol, token_address; await) + async fn get_token_symbol(&self, token_address: &str, rpc_index: usize) -> Option { + chain_ops_method!(self, get_token_symbol, token_address, rpc_index; await) } - async fn scan_for_tokens(&self, address: &str) -> SupportOption> { - chain_ops_method!(self, scan_for_tokens, address; await) + async fn scan_for_tokens(&self, address: &str, rpc_index: usize) -> SupportOption> { + chain_ops_method!(self, scan_for_tokens, address, rpc_index; await) } fn parse_wallet_address(&self, address: &str) -> Option { chain_ops_method!(self, parse_wallet_address, address) diff --git a/src/chain/sol_chain.rs b/src/chain/sol_chain.rs index e087640..b1eb97a 100644 --- a/src/chain/sol_chain.rs +++ b/src/chain/sol_chain.rs @@ -35,6 +35,7 @@ impl SolChain { &self, method: &str, params: Value, + rpc_index: usize, ) -> (Option, Option) { let payload = json!({ "jsonrpc": "2.0", @@ -44,7 +45,7 @@ impl SolChain { }); let response = match self .http_client - .post(self.properties.rpc_url.clone()) + .post(self.properties.rpc_urls[rpc_index % self.properties.rpc_urls.len()].clone()) .json(&payload) .send() .await @@ -98,9 +99,13 @@ struct SolGetTokenAccountsResponse { } impl ChainOps for SolChain { - async fn get_native_token_balance(&self, address: &str) -> (Option, Option) { + async fn get_native_token_balance( + &self, + address: &str, + rpc_index: usize, + ) -> (Option, Option) { let (balance, wait_time) = self - .rpc_call::("getBalance", json!([address])) + .rpc_call::("getBalance", json!([address]), rpc_index) .await; (balance.and_then(|b| BigUint::from_u64(b.value)), wait_time) } @@ -108,6 +113,7 @@ impl ChainOps for SolChain { &self, token: &Token, address: &str, + rpc_index: usize, ) -> (Option, Option) { let params = json!([ address, @@ -115,7 +121,7 @@ impl ChainOps for SolChain { { "encoding": "jsonParsed" }, ]); let (balances, wait_time) = self - .rpc_call::("getTokenAccountsByOwner", params) + .rpc_call::("getTokenAccountsByOwner", params, rpc_index) .await; if balances.is_some() && balances.clone().unwrap().token_amounts.len() == 0 { return (Some(BigUint::ZERO), wait_time); @@ -125,29 +131,33 @@ impl ChainOps for SolChain { wait_time, ) } - async fn get_holdings_balance(&self, _address: &str) -> SupportOption> { + async fn get_holdings_balance( + &self, + _address: &str, + _rpc_index: usize, + ) -> SupportOption> { SupportOption::Unsupported } - async fn get_token_decimals(&self, token_address: &str) -> Option { + async fn get_token_decimals(&self, token_address: &str, rpc_index: usize) -> Option { let params = json!([ token_address, { "encoding": "jsonParsed" }, ]); Some( - self.rpc_call::("getAccountInfo", params) + self.rpc_call::("getAccountInfo", params, rpc_index) .await .0? .decimals, ) } - async fn scan_for_tokens(&self, address: &str) -> SupportOption> { + async fn scan_for_tokens(&self, address: &str, rpc_index: usize) -> SupportOption> { let params = json!([ address, { "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, { "encoding": "jsonParsed" }, ]); let tokens_data = self - .rpc_call::("getTokenAccountsByOwner", params) + .rpc_call::("getTokenAccountsByOwner", params, rpc_index) .await .0 .to_supported()? diff --git a/src/chain/token.rs b/src/chain/token.rs index 006c5c2..f0f5c72 100644 --- a/src/chain/token.rs +++ b/src/chain/token.rs @@ -19,8 +19,8 @@ impl Token { } } pub async fn new(address: &str, chain: &Chain) -> Option { - let decimals = chain.get_token_decimals(address).await?; - let symbol = chain.get_token_symbol(address).await?; + let decimals = chain.get_token_decimals(address, 0).await?; + let symbol = chain.get_token_symbol(address, 0).await?; Some(Self { symbol, address: chain.parse_token_address(&address)?, diff --git a/src/chain/ton_chain.rs b/src/chain/ton_chain.rs index 70807ac..fb0b471 100644 --- a/src/chain/ton_chain.rs +++ b/src/chain/ton_chain.rs @@ -75,7 +75,7 @@ impl TonChain { ) -> (Option, Option) { let mut url = Url::parse(&format!( "{}/{}", - self.properties.rpc_url.to_string(), + self.properties.rpc_urls[0].to_string(), route )) .unwrap(); @@ -97,7 +97,11 @@ impl TonChain { } impl ChainOps for TonChain { - async fn get_native_token_balance(&self, address: &str) -> (Option, Option) { + async fn get_native_token_balance( + &self, + address: &str, + _rpc_index: usize, + ) -> (Option, Option) { let (balance, wait_time) = self .api_call::(format!("accounts/{address}"), vec![]) .await; @@ -110,6 +114,7 @@ impl ChainOps for TonChain { &self, token: &Token, address: &str, + _rpc_index: usize, ) -> (Option, Option) { let (balance, wait_time) = self .api_call::( @@ -122,7 +127,11 @@ impl ChainOps for TonChain { wait_time, ) } - async fn get_holdings_balance(&self, address: &str) -> SupportOption> { + async fn get_holdings_balance( + &self, + address: &str, + _rpc_index: usize, + ) -> SupportOption> { let address = self.parse_wallet_address(&address).to_supported()?; self.api_call::( format!("accounts/{}/jettons", address), @@ -142,7 +151,7 @@ impl ChainOps for TonChain { .collect::>() .into() } - async fn get_token_decimals(&self, token_address: &str) -> Option { + async fn get_token_decimals(&self, token_address: &str, _rpc_index: usize) -> Option { usize::from_str( &self .api_call::(format!("jettons/{token_address}"), vec![]) @@ -154,7 +163,7 @@ impl ChainOps for TonChain { .ok() .into() } - async fn scan_for_tokens(&self, address: &str) -> SupportOption> { + async fn scan_for_tokens(&self, address: &str, _rpc_index: usize) -> SupportOption> { let address = self.parse_wallet_address(&address).to_supported()?; self.api_call::( format!("accounts/{}/jettons", address), diff --git a/src/dexscreener/pairs.rs b/src/dexscreener/pairs.rs index e15da17..e5d9629 100644 --- a/src/dexscreener/pairs.rs +++ b/src/dexscreener/pairs.rs @@ -63,7 +63,7 @@ where format!("https://api.dexscreener.com/latest/dex/tokens/{}", t).as_str(), ) .unwrap(); - let task = async || (get_pairs_request(url.clone()).await, None); + let task = async |_rpc_index| (get_pairs_request(url.clone()).await, None); let result = handle_retry(task).await; if let Some(handler) = progress_handler.as_ref() { handler(); diff --git a/src/repl/default.rs b/src/repl/default.rs index 74bab52..1217ffc 100644 --- a/src/repl/default.rs +++ b/src/repl/default.rs @@ -9,7 +9,7 @@ impl Default for Repl { fn default() -> Self { let sol = Vec::from([Chain::new( ChainType::Solana, - "https://api.mainnet-beta.solana.com", + Vec::from(["https://api.mainnet-beta.solana.com"]), "Solana", "SOL", "So11111111111111111111111111111111111111112", @@ -17,7 +17,7 @@ impl Default for Repl { )]); let ton = Vec::from([Chain::new( ChainType::Ton, - "https://tonapi.io/v2", + Vec::from(["https://tonapi.io/v2"]), "Ton", "TON", "0x582d872A1B094FC48F5DE31D3B73F2D9bE47def1", @@ -26,7 +26,12 @@ impl Default for Repl { let evm = Vec::from([ Chain::new( ChainType::Evm, - "https://eth.llamarpc.com", + Vec::from([ + "https://eth.llamarpc.com", + "https://eth.drpc.org", + "https://ethereum-rpc.publicnode.com", + "https://mainnet.gateway.tenderly.co", + ]), "Ethereum", "ETH", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", @@ -34,7 +39,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://base.llamarpc.com", + Vec::from([ + "https://base.llamarpc.com", + "https://base-rpc.publicnode.com", + "https://base.drpc.org", + "https://base.gateway.tenderly.co", + ]), "Base", "ETH", "0x4200000000000000000000000000000000000006", @@ -42,7 +52,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://binance.llamarpc.com", + Vec::from([ + "https://binance.llamarpc.com", + "https://bsc.drpc.org", + "https://bsc.meowrpc.com", + "https://bsc.blockrazor.xyz", + ]), "BSC", "BNB", "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", @@ -50,7 +65,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://arbitrum.llamarpc.com", + Vec::from([ + "https://arbitrum.llamarpc.com", + "https://arbitrum.drpc.org", + "https://arbitrum.meowrpc.com", + "https://rpc.therpc.io/arbitrum", + ]), "Arbitrum", "ETH", "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", @@ -58,7 +78,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://avalanche.drpc.org", + Vec::from([ + "https://avalanche.drpc.org", + "https://avalanche.drpc.org", + "https://avax.meowrpc.com", + "https://avalanche-c-chain-rpc.publicnode.com", + ]), "Avalanche", "AVAX", "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", @@ -66,7 +91,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://polygon.llamarpc.com", + Vec::from([ + "https://polygon.llamarpc.com", + "https://polygon.meowrpc.com", + "https://polygon.drpc.org", + "https://polygon-rpc.com", + ]), "Polygon", "POL", "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", @@ -74,7 +104,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://mainnet.era.zksync.io", + Vec::from([ + "https://mainnet.era.zksync.io", + "https://1rpc.io/zksync2-era", + "https://zksync.drpc.org", + "https://zksync.meowrpc.com", + ]), "zkSync", "ETH", "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91", @@ -82,7 +117,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://cronos-evm-rpc.publicnode.com", + Vec::from([ + "https://cronos-evm-rpc.publicnode.com", + "https://cronos.drpc.org", + "https://1rpc.io/cro", + "https://evm.cronos.org", + ]), "Cronos", "CRO", "0x5C7F8A570d578ED84E63fdFA7b1eE72dEae1AE23", @@ -90,7 +130,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://fantom.drpc.org", + Vec::from([ + "https://fantom.drpc.org", + "https://1rpc.io/ftm", + "https://rpc.fantom.network", + "https://fantom-rpc.publicnode.com", + ]), "Fantom", "FTM", "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83", @@ -98,7 +143,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://mainnet.optimism.io", + Vec::from([ + "https://mainnet.optimism.io", + "https://1rpc.io/op", + "https://optimism.drpc.org", + "https://optimism.rpc.subquery.network/public", + ]), "Optimism", "ETH", "0x4200000000000000000000000000000000000006", @@ -106,7 +156,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://linea.drpc.org", + Vec::from([ + "https://linea.drpc.org", + "https://rpc.linea.build", + "https://linea-rpc.publicnode.com", + "https://1rpc.io/linea", + ]), "Linea", "ETH", "0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f", @@ -114,7 +169,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://rpc.mantle.xyz", + Vec::from([ + "https://rpc.mantle.xyz", + "https://1rpc.io/mantle", + "https://mantle-rpc.publicnode.com", + "https://mantle-mainnet.public.blastapi.io", + ]), "Mantle", "MNT", "0x201EBa5CC46D216Ce6DC03F6a759e8E766e956aE", @@ -122,7 +182,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://metis.drpc.org", + Vec::from([ + "https://metis.drpc.org", + "https://metis-andromeda.rpc.thirdweb.com", + "https://metis-pokt.nodies.app", + "https://metis-andromeda.gateway.tenderly.co", + ]), "Metis", "METIS", "0x75cb093E4D61d2A2e65D8e0BBb01DE8d89b53481", @@ -130,7 +195,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://core.drpc.org", + Vec::from([ + "https://core.drpc.org", + "https://1rpc.io/core", + "https://rpc.coredao.org", + "https://rpc.ankr.com/core", + ]), "Core", "CORE", "0x40375C92d9FAf44d2f9db9Bd9ba41a3317a2404f", @@ -138,7 +208,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://rpc.scroll.io", + Vec::from([ + "https://rpc.scroll.io", + "https://rpc.ankr.com/scroll", + "https://scroll-rpc.publicnode.com", + "https://1rpc.io/scroll", + ]), "Scroll", "ETH", "0x5300000000000000000000000000000000000004", @@ -146,7 +221,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://rpc.ankr.com/iotex", + Vec::from([ + "https://rpc.ankr.com/iotex", + "https://iotex-network.rpc.thirdweb.com", + "https://iotex.api.onfinality.io/public", + "https://babel-api.mainnet.iotex.io", + ]), "IoTeX", "IOTX", "0xA00744882684C3e4747faEFD68D283eA44099D03", @@ -154,7 +234,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://forno.celo.org", + Vec::from([ + "https://forno.celo.org", + "https://rpc.ankr.com/celo", + "https://celo.drpc.org", + "https://celo-json-rpc.stakely.io", + ]), "Celo", "CELO", "0x471EcE3750Da237f93B8E339c536989b8978a438", @@ -162,7 +247,11 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://rpc.pulsechain.com", + Vec::from([ + "https://rpc.pulsechain.com", + "https://pulsechain-rpc.publicnode.com", + "https://rpc-pulsechain.g4mm4.io", + ]), "PulseChain", "PLS", "0xA1077a294dDE1B09bB078844df40758a5D0f9a27", @@ -170,7 +259,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://polygon-zkevm.drpc.org", + Vec::from([ + "https://polygon-zkevm.drpc.org", + "https://zkevm-rpc.com", + "https://1rpc.io/polygon/zkevm", + "https://polygon-zkevm-mainnet.public.blastapi.io", + ]), "Polygon zkEVM", "ETH", "0x4F9A0e7FD2Bf6067db6994CF12E4495Df938E6e9", @@ -178,7 +272,12 @@ impl Default for Repl { ), Chain::new( ChainType::Evm, - "https://rpc.telos.net", + Vec::from([ + "https://rpc.telos.net", + "https://telos.drpc.org", + "https://1rpc.io/telos/evm", + "https://rpc.ankr.com/telos", + ]), "Telos", "TLOS", "0xB4B01216a5Bc8F1C8A33CD990A1239030E60C905", diff --git a/src/repl/mod.rs b/src/repl/mod.rs index fe0a481..b9fb334 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -271,7 +271,18 @@ can use the same command to set an authentication token for the API. "DISABLED".to_string() } ); - println!("{}", chain.properties.rpc_url); + println!("Main RPC: {}", chain.properties.rpc_urls[0]); + println!( + "Fallback RPCs: [{}]", + chain + .properties + .rpc_urls + .iter() + .skip(1) + .map(|u| u.to_string()) + .collect::>() + .join(", ") + ); Ok(()) } 2 => { @@ -520,11 +531,14 @@ alias, if set. chain.chain_type.label(), )); } - let tokens_found = - match chain.scan_for_tokens(account_address).await.to_result()? { - Some(x) => x, - None => return Err("Could not fetch account holdings".to_string()), - }; + let tokens_found = match chain + .scan_for_tokens(account_address, 0) + .await + .to_result()? + { + Some(x) => x, + None => return Err("Could not fetch account holdings".to_string()), + }; let new_tokens = tokens_found .into_iter() .filter_map(|t| { @@ -595,7 +609,7 @@ alias, if set. let results_natives = stream::iter(accounts_natives.iter().enumerate()) .map(async |(i, (chain, address, _))| { - let task = || chain.get_native_token_balance(address); + let task = |rpc_index| chain.get_native_token_balance(address, rpc_index); let result = handle_retry_indexed(i, task).await; self.spinner.inc_progress(); self.spinner @@ -608,7 +622,7 @@ alias, if set. let results_not_supported = stream::iter(accounts_not_supported.iter().enumerate()) .map(async |(i, (chain, token, address, _))| { - let task = || chain.get_token_balance(token, address); + let task = |rpc_index| chain.get_token_balance(token, address, rpc_index); let result = handle_retry_indexed(i, task).await; self.spinner.inc_progress(); self.spinner @@ -621,10 +635,10 @@ alias, if set. let results_supported = stream::iter(accounts_supported.iter().enumerate()) .map(async |(i, (chain, address, _))| { - let task = async || { + let task = async |rpc_index| { ( chain - .get_holdings_balance(address) + .get_holdings_balance(address, rpc_index) .await .to_result() .unwrap(), @@ -861,7 +875,7 @@ alias, if set. headers.insert("Authorization", format!("Bearer {rpc}").parse().unwrap()); c.properties.rpc_headers = headers; } else { - c.properties.rpc_url = Url::from_str(rpc).unwrap(); + c.properties.rpc_urls.insert(0, Url::from_str(rpc).unwrap()); } return Some(c); } @@ -871,8 +885,12 @@ alias, if set. .unwrap() .properties; if c.chain_type != ChainType::Ton { - if default_properties.rpc_url.to_string() != c.properties.rpc_url.to_string() { - c.properties.rpc_url = default_properties.rpc_url.clone(); + if default_properties.rpc_urls[0].to_string() + != c.properties.rpc_urls[0].to_string() + { + c.properties + .rpc_urls + .insert(0, default_properties.rpc_urls[0].clone()); return Some(c); } return None; diff --git a/src/utils/retry.rs b/src/utils/retry.rs index 5008a82..77967df 100644 --- a/src/utils/retry.rs +++ b/src/utils/retry.rs @@ -15,17 +15,27 @@ pub fn get_retry_time(response: &Response) -> Option { pub async fn handle_retry(mut task: F) -> T where - F: FnMut() -> Fut, + F: FnMut(usize) -> Fut, Fut: Future, Option)>, { let mut retries = 0; + let mut rpc_index = 0; + let maximum_retry_time_secs = 1.0; loop { - let (result, retry_time) = task().await; + let (result, retry_time) = task(rpc_index).await; match result { - Some(x) => return x, + Some(x) => { + return x; + } None => { - if retries >= 3 { - sleep(Duration::from_secs_f32(retry_time.unwrap_or(2.0))).await; + if retries >= 2 { + if let Some(retry_time) = retry_time { + sleep(Duration::from_secs_f32( + retry_time.min(maximum_retry_time_secs), + )) + .await; + } + rpc_index += 1; } retries += 1; } @@ -35,7 +45,7 @@ where pub async fn handle_retry_indexed(index: usize, task: F) -> (usize, T) where - F: FnMut() -> Fut, + F: FnMut(usize) -> Fut, Fut: Future, Option)>, { (index, handle_retry(task).await)