cow-sdk-core 0.1.0-alpha.10

Shared CoW Protocol core types and validation primitives
Documentation
//! Combined transport policy bundle.

use std::time::Duration;

use crate::{HttpClientPolicy, ValidationError};

use crate::transport::policy::{JitterStrategy, RequestRateLimiter, RetryPolicy};

/// Default orderbook user-agent string.
pub const DEFAULT_ORDERBOOK_USER_AGENT: &str =
    concat!("cow-sdk-orderbook", "/", env!("CARGO_PKG_VERSION"));
/// Default subgraph user-agent string.
pub const DEFAULT_SUBGRAPH_USER_AGENT: &str =
    concat!("cow-sdk-subgraph", "/", env!("CARGO_PKG_VERSION"));
/// Default trading user-agent string.
pub const DEFAULT_TRADING_USER_AGENT: &str =
    concat!("cow-sdk-trading", "/", env!("CARGO_PKG_VERSION"));
/// Default IPFS user-agent string.
pub const DEFAULT_IPFS_USER_AGENT: &str = concat!("cow-sdk-ipfs", "/", env!("CARGO_PKG_VERSION"));

/// Maximum response-body size buffered from the subgraph gateway, in bytes.
///
/// The subgraph is untrusted third-party infrastructure, but the SDK's
/// subgraph queries return small aggregate documents, so this generous
/// headroom never rejects a legitimate response while still bounding a
/// hostile or misbehaving gateway.
pub const SUBGRAPH_MAX_RESPONSE_BYTES: usize = 2 * 1024 * 1024;
/// Maximum response-body size buffered from an IPFS gateway, in bytes.
///
/// Sized at twice the protocol app-data document limit so encoding and
/// framing overhead never rejects a valid document, while bounding a hostile
/// gateway to a small read.
pub const IPFS_MAX_RESPONSE_BYTES: usize = 16 * 1024;

/// Combined HTTP client, retry, and rate-limit policy.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransportPolicy {
    client: HttpClientPolicy,
    retry: RetryPolicy,
    rate_limit: RequestRateLimiter,
}

impl Default for TransportPolicy {
    fn default() -> Self {
        Self::default_orderbook()
    }
}

impl TransportPolicy {
    /// Creates a policy from explicit component policies.
    #[must_use]
    pub const fn new(
        client: HttpClientPolicy,
        retry: RetryPolicy,
        rate_limit: RequestRateLimiter,
    ) -> Self {
        Self {
            client,
            retry,
            rate_limit,
        }
    }

    /// Returns the documented default orderbook transport policy.
    ///
    /// # Panics
    ///
    /// Panics only if the crate-owned default orderbook user-agent literal
    /// stops being encodable as an HTTP header value.
    #[must_use]
    pub fn default_orderbook() -> Self {
        Self {
            // SAFETY: this crate-owned user-agent literal is static and
            // validated by the HTTP header parser.
            client: HttpClientPolicy::new(DEFAULT_ORDERBOOK_USER_AGENT)
                .expect("static orderbook user-agent must remain valid"),
            retry: RetryPolicy::builder()
                .jitter(JitterStrategy::decorrelated_process())
                .build(),
            rate_limit: RequestRateLimiter::default_orderbook(),
        }
    }

    /// Returns the documented default subgraph transport policy.
    ///
    /// # Panics
    ///
    /// Panics only if the crate-owned default subgraph user-agent literal
    /// stops being encodable as an HTTP header value.
    #[must_use]
    pub fn default_subgraph() -> Self {
        Self {
            // SAFETY: this crate-owned user-agent literal is static and
            // validated by the HTTP header parser.
            client: HttpClientPolicy::new(DEFAULT_SUBGRAPH_USER_AGENT)
                .expect("static subgraph user-agent must remain valid")
                .with_max_response_bytes(SUBGRAPH_MAX_RESPONSE_BYTES),
            retry: RetryPolicy::builder()
                .jitter(JitterStrategy::decorrelated_process())
                .build(),
            // The subgraph host carries the same per-host request budget as the
            // orderbook; one limiter constructor keeps the two from drifting.
            rate_limit: RequestRateLimiter::default_orderbook(),
        }
    }

    /// Returns the documented default trading transport policy.
    ///
    /// Trading currently routes HTTP through the orderbook client, so this
    /// preserves the same retry and limiter behavior with a trading-specific
    /// client policy label.
    ///
    /// # Panics
    ///
    /// Panics only if the crate-owned default trading user-agent literal stops
    /// being encodable as an HTTP header value.
    #[must_use]
    pub fn default_trading() -> Self {
        Self {
            // SAFETY: this crate-owned user-agent literal is static and
            // validated by the HTTP header parser.
            client: HttpClientPolicy::new(DEFAULT_TRADING_USER_AGENT)
                .expect("static trading user-agent must remain valid"),
            retry: RetryPolicy::builder()
                .jitter(JitterStrategy::decorrelated_process())
                .build(),
            rate_limit: RequestRateLimiter::default_orderbook(),
        }
    }

    /// Returns the documented default IPFS transport policy.
    ///
    /// IPFS reads historically performed one direct fetch with no SDK-owned
    /// retry, rate limiting, or default timeout, so the default policy keeps
    /// those behaviors disabled unless a caller opts in.
    ///
    /// # Panics
    ///
    /// Panics only if the crate-owned default IPFS user-agent literal stops
    /// being encodable as an HTTP header value.
    #[must_use]
    pub fn default_ipfs() -> Self {
        Self {
            // SAFETY: this crate-owned user-agent literal is static and
            // validated by the HTTP header parser.
            client: HttpClientPolicy::new(DEFAULT_IPFS_USER_AGENT)
                .expect("static IPFS user-agent must remain valid")
                .without_timeout()
                .with_max_response_bytes(IPFS_MAX_RESPONSE_BYTES),
            retry: RetryPolicy::no_retry(),
            rate_limit: RequestRateLimiter::unlimited(),
        }
    }

    /// Returns a builder seeded with orderbook defaults.
    #[must_use]
    pub fn builder() -> TransportPolicyBuilder {
        TransportPolicyBuilder::default()
    }

    /// Returns the shared HTTP client policy.
    #[must_use]
    pub const fn client_policy(&self) -> &HttpClientPolicy {
        &self.client
    }

    /// Returns the retry policy.
    #[must_use]
    pub const fn retry(&self) -> &RetryPolicy {
        &self.retry
    }

    /// Returns the request rate limiter.
    #[must_use]
    pub const fn rate_limit(&self) -> &RequestRateLimiter {
        &self.rate_limit
    }

    /// Returns the configured request timeout.
    #[must_use]
    pub const fn timeout(&self) -> Option<Duration> {
        self.client.timeout()
    }

    /// Returns the configured user-agent.
    #[must_use]
    pub fn user_agent(&self) -> &str {
        self.client.user_agent()
    }

    /// Returns a copy of this policy with a new HTTP client policy.
    #[must_use]
    pub fn with_client_policy(mut self, client: HttpClientPolicy) -> Self {
        self.client = client;
        self
    }

    /// Returns a copy of this policy with a new retry policy.
    #[must_use]
    pub const fn with_retry(mut self, retry: RetryPolicy) -> Self {
        self.retry = retry;
        self
    }

    /// Returns a copy of this policy with a new rate limiter.
    #[must_use]
    pub fn with_rate_limit(mut self, rate_limit: RequestRateLimiter) -> Self {
        self.rate_limit = rate_limit;
        self
    }
}

/// Builder for [`TransportPolicy`].
#[derive(Debug, Clone)]
pub struct TransportPolicyBuilder {
    client: HttpClientPolicy,
    retry: RetryPolicy,
    rate_limit: RequestRateLimiter,
}

impl TransportPolicyBuilder {
    /// Creates a builder seeded with orderbook defaults.
    ///
    /// # Panics
    ///
    /// Panics only if the crate-owned default orderbook user-agent literal
    /// stops being encodable as an HTTP header value.
    #[must_use]
    pub fn new() -> Self {
        Self {
            // SAFETY: this crate-owned user-agent literal is static and
            // validated by the HTTP header parser.
            client: HttpClientPolicy::new(DEFAULT_ORDERBOOK_USER_AGENT)
                .expect("static orderbook user-agent must remain valid"),
            retry: RetryPolicy::builder()
                .jitter(JitterStrategy::decorrelated_process())
                .build(),
            rate_limit: RequestRateLimiter::default_orderbook(),
        }
    }

    /// Sets the shared HTTP client policy, replacing the seeded default and any
    /// earlier `user_agent` or `timeout` refinement wholesale.
    #[must_use]
    pub fn client_policy(mut self, client: HttpClientPolicy) -> Self {
        self.client = client;
        self
    }

    /// Sets the shared HTTP user-agent in place, preserving every other
    /// client-policy field — including the response-byte cap and a disabled
    /// timeout.
    ///
    /// # Errors
    ///
    /// Returns [`ValidationError`] if the user-agent is empty or not a valid
    /// HTTP header value.
    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Result<Self, ValidationError> {
        self.client = self.client.try_with_user_agent(user_agent)?;
        Ok(self)
    }

    /// Sets the shared HTTP timeout in place, preserving every other
    /// client-policy field — including the response-byte cap.
    #[must_use]
    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.client = self.client.with_timeout(timeout);
        self
    }

    /// Sets the retry policy.
    #[must_use]
    pub const fn retry(mut self, retry: RetryPolicy) -> Self {
        self.retry = retry;
        self
    }

    /// Sets the request rate limiter.
    #[must_use]
    pub fn rate_limit(mut self, rate_limit: RequestRateLimiter) -> Self {
        self.rate_limit = rate_limit;
        self
    }

    /// Builds the transport policy.
    #[must_use]
    pub fn build(self) -> TransportPolicy {
        TransportPolicy {
            client: self.client,
            retry: self.retry,
            rate_limit: self.rate_limit,
        }
    }
}

impl Default for TransportPolicyBuilder {
    fn default() -> Self {
        Self::new()
    }
}