Skip to content

Commit 479a8cb

Browse files
Hiksangclaude
andcommitted
Add HYPEREVM_RPC_URL env override and rate limit auto-retry
- ChainConfig::effective_rpc_url() checks HYPEREVM_RPC_URL env var first - All CLI commands use effective_rpc_url() instead of hardcoded chain.rpc_url - provider::with_retry() — exponential backoff on rate limit errors (3 retries, 2s initial) - Lending rates and yield compare use retry wrapper - build_provider_from_url() helper for raw URL strings Usage: HYPEREVM_RPC_URL=http://localhost:8545 defi lending rates --protocol hyperlend --asset USDC Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cc99bf1 commit 479a8cb

9 files changed

Lines changed: 99 additions & 26 deletions

File tree

crates/defi-cli/src/commands/dex.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,10 @@ pub async fn run(
162162
amount,
163163
} => {
164164
let entry = registry.get_protocol(&protocol)?;
165-
let dex = defi_protocols::factory::create_dex_with_rpc(entry, Some(&chain.rpc_url))?;
165+
let dex = defi_protocols::factory::create_dex_with_rpc(
166+
entry,
167+
Some(&chain.effective_rpc_url()),
168+
)?;
166169

167170
let token_in_addr =
168171
resolve_token_address(registry, &chain.name.to_lowercase(), &token_in)?;
@@ -205,7 +208,10 @@ pub async fn run(
205208
let mut quotes: Vec<QuoteResult> = Vec::new();
206209

207210
for entry in &dex_protocols {
208-
match defi_protocols::factory::create_dex_with_rpc(entry, Some(&chain.rpc_url)) {
211+
match defi_protocols::factory::create_dex_with_rpc(
212+
entry,
213+
Some(&chain.effective_rpc_url()),
214+
) {
209215
Ok(dex) => {
210216
let params = QuoteParams {
211217
protocol: entry.slug.clone(),

crates/defi-cli/src/commands/lending.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -264,16 +264,26 @@ pub async fn run(
264264
}
265265
LendingCommand::Rates { protocol, asset } => {
266266
let entry = registry.get_protocol(&protocol)?;
267-
let lending =
268-
defi_protocols::factory::create_lending_with_rpc(entry, Some(&chain.rpc_url))?;
267+
let rpc_url = chain.effective_rpc_url();
269268
let asset_addr = resolve_asset(registry, &chain_key, &asset)?;
270-
let rates = lending.get_rates(asset_addr).await?;
269+
let entry_clone = entry.clone();
270+
let rates = defi_core::provider::with_retry(3, 2000, || {
271+
let lending =
272+
defi_protocols::factory::create_lending_with_rpc(&entry_clone, Some(&rpc_url));
273+
async move {
274+
let lending = lending?;
275+
lending.get_rates(asset_addr).await
276+
}
277+
})
278+
.await?;
271279
output.print(&rates)?;
272280
}
273281
LendingCommand::Position { protocol, address } => {
274282
let entry = registry.get_protocol(&protocol)?;
275-
let lending =
276-
defi_protocols::factory::create_lending_with_rpc(entry, Some(&chain.rpc_url))?;
283+
let lending = defi_protocols::factory::create_lending_with_rpc(
284+
entry,
285+
Some(&chain.effective_rpc_url()),
286+
)?;
277287
let user = address
278288
.parse::<Address>()
279289
.map_err(|e| DefiError::InvalidParam(format!("Invalid address: {e}")))?;

crates/defi-cli/src/commands/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ pub async fn run(cli: Cli) -> Result<(), DefiError> {
7979
let registry = defi_core::registry::Registry::load_embedded()?;
8080
let chain = registry.get_chain(&cli.chain)?;
8181
let output_mode = crate::output::OutputMode::from_cli(&cli);
82-
let executor = crate::executor::Executor::new(cli.broadcast, Some(chain.rpc_url.clone()));
82+
let executor = crate::executor::Executor::new(cli.broadcast, Some(chain.effective_rpc_url()));
8383

8484
match cli.command {
8585
Commands::Status(args) => status::run(args, &registry, &output_mode).await,

crates/defi-cli/src/commands/staking.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ pub async fn run(
137137
let entry = registry.get_protocol(&p)?;
138138
let staking = defi_protocols::factory::create_liquid_staking_with_rpc(
139139
entry,
140-
Some(&_chain.rpc_url),
140+
Some(&_chain.effective_rpc_url()),
141141
)?;
142142
let info = staking.get_info().await?;
143143
output.print(&info)?;

crates/defi-cli/src/commands/status.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ pub async fn run(args: StatusArgs, registry: &Registry, output: &OutputMode) ->
124124
.map_err(|e| DefiError::RpcError(format!("Failed to get block number: {e}")))?;
125125
eprintln!(
126126
"Connected to {} (block #{}). Verifying {} contracts...",
127-
chain.rpc_url,
127+
chain.effective_rpc_url(),
128128
bn,
129129
all_addresses.len()
130130
);
@@ -213,7 +213,7 @@ pub async fn run(args: StatusArgs, registry: &Registry, output: &OutputMode) ->
213213
let status = StatusOutput {
214214
chain: chain.name.clone(),
215215
chain_id: chain.chain_id,
216-
rpc_url: chain.rpc_url.clone(),
216+
rpc_url: chain.effective_rpc_url(),
217217
block_number,
218218
protocols,
219219
summary,

crates/defi-cli/src/commands/vault.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,10 @@ pub async fn run(
123123
VaultCommand::Info { protocol } => match protocol {
124124
Some(p) => {
125125
let entry = registry.get_protocol(&p)?;
126-
let vault =
127-
defi_protocols::factory::create_vault_with_rpc(entry, Some(&_chain.rpc_url))?;
126+
let vault = defi_protocols::factory::create_vault_with_rpc(
127+
entry,
128+
Some(&_chain.effective_rpc_url()),
129+
)?;
128130
let info = vault.get_vault_info().await?;
129131
output.print(&info)?;
130132
}

crates/defi-cli/src/commands/yield_cmd.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,17 @@ async fn collect_lending_rates(
5959
}
6060
first = false;
6161

62-
match defi_protocols::factory::create_lending_with_rpc(entry, Some(&chain.rpc_url)) {
63-
Ok(lending) => match lending.get_rates(asset_addr).await {
64-
Ok(rates) => results.push(rates),
65-
Err(e) => {
66-
eprintln!("Warning: {} rates unavailable: {}", entry.name, e);
67-
}
68-
},
62+
let rpc = chain.effective_rpc_url();
63+
let entry_c = (*entry).clone();
64+
match defi_core::provider::with_retry(2, 2000, || {
65+
let l = defi_protocols::factory::create_lending_with_rpc(&entry_c, Some(&rpc));
66+
async move { l?.get_rates(asset_addr).await }
67+
})
68+
.await
69+
{
70+
Ok(rates) => results.push(rates),
6971
Err(e) => {
70-
eprintln!(
71-
"Warning: could not create {} lending adapter: {}",
72-
entry.name, e
73-
);
72+
eprintln!("Warning: {} rates unavailable: {}", entry.name, e);
7473
}
7574
}
7675
}

crates/defi-core/src/provider.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,58 @@ use url::Url;
44
use crate::error::{DefiError, Result};
55
use crate::registry::ChainConfig;
66

7+
/// Build a provider using the effective RPC URL (env var override supported).
78
pub fn build_provider(chain: &ChainConfig) -> Result<impl alloy::providers::Provider> {
8-
let url: Url = chain
9-
.rpc_url
9+
let rpc = chain.effective_rpc_url();
10+
let url: Url = rpc
1011
.parse()
1112
.map_err(|e| DefiError::RpcError(format!("Invalid RPC URL: {e}")))?;
1213
Ok(ProviderBuilder::new().connect_http(url))
1314
}
15+
16+
/// Build a provider from a raw URL string.
17+
pub fn build_provider_from_url(rpc_url: &str) -> Result<impl alloy::providers::Provider> {
18+
let url: Url = rpc_url
19+
.parse()
20+
.map_err(|e| DefiError::RpcError(format!("Invalid RPC URL: {e}")))?;
21+
Ok(ProviderBuilder::new().connect_http(url))
22+
}
23+
24+
/// Retry an async RPC operation with exponential backoff on rate limit errors.
25+
/// Retries up to `max_retries` times with initial delay `initial_delay_ms`.
26+
pub async fn with_retry<F, Fut, T>(
27+
max_retries: u32,
28+
initial_delay_ms: u64,
29+
mut operation: F,
30+
) -> Result<T>
31+
where
32+
F: FnMut() -> Fut,
33+
Fut: std::future::Future<Output = Result<T>>,
34+
{
35+
let mut delay = initial_delay_ms;
36+
for attempt in 0..=max_retries {
37+
match operation().await {
38+
Ok(val) => return Ok(val),
39+
Err(e) => {
40+
let err_str = e.to_string();
41+
let is_rate_limit = err_str.contains("rate limit")
42+
|| err_str.contains("-32005")
43+
|| err_str.contains("429");
44+
45+
if !is_rate_limit || attempt == max_retries {
46+
return Err(e);
47+
}
48+
49+
eprintln!(
50+
"Rate limited (attempt {}/{}), retrying in {}ms...",
51+
attempt + 1,
52+
max_retries,
53+
delay
54+
);
55+
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
56+
delay = (delay * 2).min(30_000); // exponential backoff, max 30s
57+
}
58+
}
59+
}
60+
unreachable!()
61+
}

crates/defi-core/src/registry/chain.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ pub struct ChainConfig {
1010
pub wrapped_native: Option<String>,
1111
pub multicall3: Option<String>,
1212
}
13+
14+
impl ChainConfig {
15+
/// Get the effective RPC URL, checking environment variable override first.
16+
/// Priority: HYPEREVM_RPC_URL env var > chains.toml rpc_url
17+
pub fn effective_rpc_url(&self) -> String {
18+
std::env::var("HYPEREVM_RPC_URL").unwrap_or_else(|_| self.rpc_url.clone())
19+
}
20+
}

0 commit comments

Comments
 (0)