barter-integration 0.5.1

Low-level framework for composing flexible web integrations, especially with financial exchanges
Documentation
use self::encoder::Encoder;
use super::{rest::RestRequest, BuildStrategy};
use crate::error::SocketError;
use bytes::Bytes;
use hmac::Mac;

/// Implementations for encoding signatures generated by a [`RequestSigner`].
pub mod encoder;

/// API specific signing logic used by a [`RequestSigner`].
#[allow(clippy::needless_lifetimes)]
pub trait Signer {
    /// Configuration required to sign the [`RestRequest`]s for this API server.
    type Config<'a>
    where
        Self: 'a;

    /// Generates a [`Self::Config`] for this [`RestRequest`] and
    /// [`RequestBuilder`](reqwest::RequestBuilder).
    ///
    /// # Examples
    ///
    /// ## Private REST Request: FTX
    /// ```rust,ignore
    /// fn config<Request>(&self, _: Request, _: &RequestBuilder) -> Self::Config
    /// where
    ///     Request: RestRequest
    /// {
    ///     FtxSignConfig {
    ///         api_key: self.api_key.as_str(),
    ///         time: Utc::now(),
    ///         method: Request::method(),
    ///         path: Request::path()
    ///     }
    /// }
    /// ```
    fn config<'a, Request>(
        &'a self,
        request: Request,
        builder: &reqwest::RequestBuilder,
    ) -> Result<Self::Config<'a>, SocketError>
    where
        Request: RestRequest;

    /// Generate the bytes to sign from the provided [`Self::Config`].
    ///
    /// # Examples
    ///
    /// ## Private REST Request: FTX
    /// ```rust,ignore
    /// fn bytes_to_sign(config: &Self::Config) -> Result<&[u8], SocketError> {
    ///     Ok(format!("{}{}{}", config.time, config.method, config.path).as_bytes())
    /// }
    /// ```
    fn bytes_to_sign<'a>(config: &Self::Config<'a>) -> Bytes;

    /// Build a signed [`reqwest::Request`] from the provided [`Self::Config`],
    /// [`RequestBuilder`](reqwest::RequestBuilder), and generated cryptographic signature `String`.
    ///
    /// # Examples
    ///
    /// ## Private REST Request: FTX
    /// ```rust,ignore
    /// fn build_signed_request(config: Self::Config, builder: RequestBuilder, signature: String) -> Result<reqwest::Request, SocketError> {
    ///     // Add Ftx required Headers & build reqwest::Request
    ///     builder
    ///         .header(HEADER_FTX_KEY, &self.api_key)
    ///         .header(HEADER_FTX_SIGN, &signature)
    ///         .header(HEADER_FTX_TS, &time)
    ///         .build()
    ///         .map_err(SocketError::from)
    /// }
    /// ```
    fn build_signed_request<'a>(
        config: Self::Config<'a>,
        builder: reqwest::RequestBuilder,
        signature: String,
    ) -> Result<reqwest::Request, SocketError>;
}

/// Generically signs Http [`RestRequest`]s utilising API specific [`Signer`] logic, a hashable
/// [`Mac`], and a signature [`Encoder`].
#[derive(Debug, Copy, Clone)]
pub struct RequestSigner<Sig, Hmac, SigEncoder> {
    signer: Sig,
    mac: Hmac,
    encoder: SigEncoder,
}

impl<Sig, Hmac, SigEncoder> BuildStrategy for RequestSigner<Sig, Hmac, SigEncoder>
where
    Sig: Signer,
    Hmac: Mac + Clone,
    SigEncoder: Encoder,
{
    fn build<Request>(
        &self,
        request: Request,
        builder: reqwest::RequestBuilder,
    ) -> Result<reqwest::Request, SocketError>
    where
        Request: RestRequest,
    {
        // Build configuration required for generating signed requests
        let config = self.signer.config(request, &builder)?;

        // Generate bytes data used to update Mac state
        let bytes_to_sign = Sig::bytes_to_sign(&config);

        // Update Mac state & finalise bytes
        let mut mac = self.mac.clone();
        mac.update(&bytes_to_sign);
        let bytes_to_encode = mac.finalize().into_bytes();

        // Encode signature from Mac bytes
        let signature = self.encoder.encode(bytes_to_encode);

        Sig::build_signed_request(config, builder, signature)
    }
}

impl<Sig, Hmac, SigEncoder> RequestSigner<Sig, Hmac, SigEncoder> {
    /// Construct a new [`Self`] using the provided API specific configuration.
    pub fn new(signer: Sig, mac: Hmac, encoder: SigEncoder) -> Self {
        Self {
            signer,
            mac,
            encoder,
        }
    }
}