tower-defense 0.2.0

Middleware utilities to authenticate RPC calls using libp2p node keys
Documentation
use jsonrpsee::{
    MethodResponse,
    core::{
        ClientError,
        middleware::{Batch, Notification, RpcServiceT},
        traits::ToRpcParams as _,
    },
    types::{ErrorCode, Id, Request},
};

use crate::{
    auth::{AuthenticatedParams, VerifyTimestamp},
    crypto::Keypair,
};

pub struct Sign<S> {
    keypair: Keypair,
    inner: S,
}

impl<S> Sign<S> {
    pub fn new(keypair: Keypair, inner: S) -> Self {
        Self { keypair, inner }
    }
}

impl<S, MR, NR, BR> RpcServiceT for Sign<S>
where
    S: RpcServiceT<
            MethodResponse = Result<MR, ClientError>,
            NotificationResponse = Result<NR, ClientError>,
            BatchResponse = Result<BR, ClientError>,
        > + Send
        + Sync
        + Clone
        + 'static,
{
    type MethodResponse = S::MethodResponse;
    type NotificationResponse = S::NotificationResponse;
    type BatchResponse = S::BatchResponse;

    fn call<'a>(
        &self,
        request: Request<'a>,
    ) -> impl Future<Output = Self::MethodResponse> + Send + 'a {
        let inner = self.inner.clone();
        let keypair = self.keypair.clone();

        async move {
            let Ok(auth) = AuthenticatedParams::prepare_now(
                &keypair,
                &request.id,
                request.method.as_ref(),
                request.params,
            ) else {
                return Err(ClientError::Custom(
                    "Failed to prepare authenticated params".into(),
                ));
            };

            let Ok(params) = auth.to_rpc_params() else {
                return Err(ClientError::Custom(
                    "Failed to serialize authenticated params".into(),
                ));
            };

            let request = Request::owned(request.method.into_owned(), params, request.id);

            inner.call(request).await
        }
    }

    #[expect(clippy::manual_async_fn)]
    fn batch<'a>(
        &self,
        _requests: Batch<'a>,
    ) -> impl Future<Output = Self::BatchResponse> + Send + 'a {
        async {
            Err(ClientError::Custom(
                "Authenticated batch calls are not supported".into(),
            ))
        }
    }

    #[expect(clippy::manual_async_fn)]
    fn notification<'a>(
        &self,
        _n: Notification<'a>,
    ) -> impl Future<Output = Self::NotificationResponse> + Send + 'a {
        async {
            Err(ClientError::Custom(
                "Authenticated notifications are not supported".into(),
            ))
        }
    }
}

pub struct Verify<S, VT> {
    inner: S,
    verify_timestamp: VT,
}

impl<S, VT> Verify<S, VT> {
    pub fn new(inner: S, verify_timestamp: VT) -> Self {
        Self {
            inner,
            verify_timestamp,
        }
    }
}

impl<S, VT> RpcServiceT for Verify<S, VT>
where
    S: RpcServiceT<MethodResponse = MethodResponse> + Send + Clone + 'static,
    VT: VerifyTimestamp + Clone + Send + 'static,
{
    type MethodResponse = MethodResponse;
    type NotificationResponse = MethodResponse;
    type BatchResponse = MethodResponse;

    fn call<'a>(
        &self,
        mut request: Request<'a>,
    ) -> impl Future<Output = Self::MethodResponse> + Send + 'a {
        let inner = self.inner.clone();
        let verify_timestamp = self.verify_timestamp.clone();

        async move {
            let Ok(auth) = request.params().parse::<AuthenticatedParams>() else {
                return MethodResponse::error(request.id, ErrorCode::InvalidRequest);
            };

            let Ok(verified) = auth.verify(&request.id, request.method_name(), &verify_timestamp)
            else {
                return MethodResponse::error(request.id, ErrorCode::InvalidRequest);
            };

            request.params = verified.inner;
            request.extensions_mut().insert(verified.peer);

            inner.call(request).await
        }
    }

    #[expect(clippy::manual_async_fn)]
    fn batch<'a>(
        &self,
        _requests: Batch<'a>,
    ) -> impl Future<Output = Self::BatchResponse> + Send + 'a {
        async { MethodResponse::error(Id::Null, ErrorCode::InternalError) }
    }

    #[expect(clippy::manual_async_fn)]
    fn notification<'a>(
        &self,
        _n: Notification<'a>,
    ) -> impl Future<Output = Self::NotificationResponse> + Send + 'a {
        async { MethodResponse::error(Id::Null, ErrorCode::InternalError) }
    }
}