agentswap 0.13.0

AgentSwap CLI for requesting quotes, inspecting tokens and pools, managing intents, and registering an API key against the AgentSwap service.
// Bounded retries for intermittent Ethereum archive-access refusals.
// Deps: parent chain configuration and alloy contract errors; no retries on other errors.

use super::{ChainConfig, event_query_error};
use alloy::contract::Error;
use eyre::Result;
use std::future::Future;

pub async fn query<T, F, Fut>(config: ChainConfig, from: u64, to: u64, mut request: F) -> Result<T>
where
    F: FnMut() -> Fut,
    Fut: Future<Output = std::result::Result<T, Error>>,
{
    let mut retries = 0;
    loop {
        match request().await {
            Ok(value) => return Ok(value),
            Err(error) => {
                if config.id != 1 || !archive_refusal(&error) || retries == 3 {
                    return Err(event_query_error(config, from, to, error));
                }
                retries += 1;
                tokio::time::sleep(std::time::Duration::from_millis(100 * retries)).await;
            }
        }
    }
}

fn archive_refusal(error: &Error) -> bool {
    const PHRASE: &str = "archive requests require a personal token";
    let Error::TransportError(transport) = error else {
        return false;
    };
    if let Some(rpc) = transport.as_error_resp() {
        return rpc.code == -32602 && rpc.message.to_ascii_lowercase().contains(PHRASE);
    }
    let Some(http) = transport
        .as_transport_err()
        .and_then(|kind| kind.as_http_error())
    else {
        return false;
    };
    if http.status != 403 {
        return false;
    }
    let Ok(body) = serde_json::from_str::<serde_json::Value>(&http.body) else {
        return false;
    };
    body["error"]["code"].as_i64() == Some(-32602)
        && body["error"]["message"]
            .as_str()
            .is_some_and(|message| message.to_ascii_lowercase().contains(PHRASE))
}