alloy_provider/ext/mev/
with_auth.rs

1use crate::{ext::FLASHBOTS_SIGNATURE_HEADER, ProviderCall};
2use alloy_json_rpc::{RpcRecv, RpcSend};
3use alloy_primitives::{hex, keccak256};
4use alloy_rpc_client::RpcCall;
5use alloy_signer::Signer;
6use alloy_transport::{TransportErrorKind, TransportResult};
7use http::{HeaderMap, HeaderName, HeaderValue};
8use std::future::IntoFuture;
9
10/// A builder for MEV RPC calls that allow optional Flashbots authentication.
11pub struct MevBuilder<Params, Resp, Output = Resp, Map = fn(Resp) -> Output>
12where
13    Params: RpcSend,
14    Resp: RpcRecv,
15    Map: Fn(Resp) -> Output,
16{
17    inner: RpcCall<Params, Resp, Output, Map>,
18    signer: Option<Box<dyn Signer + Send + Sync>>,
19}
20
21impl<Params, Resp, Output, Map> std::fmt::Debug for MevBuilder<Params, Resp, Output, Map>
22where
23    Params: RpcSend + std::fmt::Debug,
24    Resp: RpcRecv + std::fmt::Debug,
25    Output: std::fmt::Debug,
26    Map: Fn(Resp) -> Output + Clone + std::fmt::Debug,
27{
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        f.debug_struct("MevBuilder").field("inner", &self.inner).finish()
30    }
31}
32
33impl<Params, Resp, Output, Map> MevBuilder<Params, Resp, Output, Map>
34where
35    Params: RpcSend,
36    Resp: RpcRecv,
37    Map: Fn(Resp) -> Output + Clone,
38{
39    /// Create a new [`MevBuilder`] from a [`RpcCall`].
40    pub const fn new_rpc(inner: RpcCall<Params, Resp, Output, Map>) -> Self {
41        Self { inner, signer: None }
42    }
43}
44
45impl<Params, Resp, Output, Map> From<RpcCall<Params, Resp, Output, Map>>
46    for MevBuilder<Params, Resp, Output, Map>
47where
48    Params: RpcSend,
49    Resp: RpcRecv,
50    Map: Fn(Resp) -> Output + Clone,
51{
52    fn from(inner: RpcCall<Params, Resp, Output, Map>) -> Self {
53        Self::new_rpc(inner)
54    }
55}
56
57impl<Params, Resp, Output, Map> MevBuilder<Params, Resp, Output, Map>
58where
59    Params: RpcSend,
60    Resp: RpcRecv,
61    Map: Fn(Resp) -> Output,
62{
63    /// Enables Flashbots authentication using the provided signer.
64    ///
65    /// The signer is used to generate the `X-Flashbots-Signature` header, which will be included
66    /// in the request if the transport supports HTTP headers.
67    pub fn with_auth<S: Signer + Send + Sync + 'static>(mut self, signer: S) -> Self {
68        self.signer = Some(Box::new(signer));
69        self
70    }
71}
72
73impl<Params, Resp, Output, Map> IntoFuture for MevBuilder<Params, Resp, Output, Map>
74where
75    Params: RpcSend + 'static,
76    Resp: RpcRecv,
77    Output: 'static,
78    Map: Fn(Resp) -> Output + Send + 'static,
79{
80    type Output = TransportResult<Output>;
81    type IntoFuture = ProviderCall<Params, Resp, Output, Map>;
82
83    fn into_future(self) -> Self::IntoFuture {
84        if let Some(signer) = self.signer {
85            let fut = async move {
86                // Generate the Flashbots signature for the request body
87                let body = serde_json::to_string(&self.inner.request())
88                    .map_err(TransportErrorKind::custom)?;
89                let signature = sign_flashbots_payload(body, &signer)
90                    .await
91                    .map_err(TransportErrorKind::custom)?;
92
93                // Add the Flashbots signature to the request headers
94                let headers = HeaderMap::from_iter([(
95                    HeaderName::from_static(FLASHBOTS_SIGNATURE_HEADER),
96                    HeaderValue::from_str(signature.as_str())
97                        .map_err(TransportErrorKind::custom)?,
98                )]);
99
100                // Patch the existing RPC call with the new headers
101                let rpc_call = self.inner.map_meta(|meta| {
102                    let mut meta = meta;
103                    meta.extensions_mut().insert(headers);
104                    meta
105                });
106
107                rpc_call.await
108            };
109            return ProviderCall::BoxedFuture(Box::pin(fut));
110        }
111        ProviderCall::RpcCall(self.inner)
112    }
113}
114
115/// Uses the provided signer to generate a signature for Flashbots authentication.
116/// Returns the value for the `X-Flashbots-Signature` header.
117///
118/// See [here](https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint#authentication) for more information.
119pub async fn sign_flashbots_payload<S: Signer + Send + Sync>(
120    body: String,
121    signer: &S,
122) -> Result<String, alloy_signer::Error> {
123    let message_hash = keccak256(body.as_bytes()).to_string();
124    let signature = signer.sign_message(message_hash.as_bytes()).await?;
125
126    // Normalized recovery byte (0/1) following the canonical signature encoding
127    let mut sig_bytes = [0u8; 65];
128    sig_bytes[..32].copy_from_slice(&signature.r().to_be_bytes::<32>());
129    sig_bytes[32..64].copy_from_slice(&signature.s().to_be_bytes::<32>());
130    sig_bytes[64] = signature.v() as u8;
131    Ok(format!("{}:{}", signer.address(), hex::encode_prefixed(sig_bytes)))
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use alloy_primitives::b256;
138    use alloy_signer_local::PrivateKeySigner;
139
140    #[tokio::test]
141    async fn test_sign_flashbots_payload() {
142        let signer = PrivateKeySigner::from_bytes(&b256!(
143            "0x0000000000000000000000000000000000000000000000000000000000123456"
144        ))
145        .unwrap();
146        let body = "sign this message".to_string();
147        let signature = sign_flashbots_payload(body.clone(), &signer).await.unwrap();
148        assert_eq!(signature, "0xd5F5175D014F28c85F7D67A111C2c9335D7CD771:0x983dc7c520db0d287faff3cd0aef81d5a7f4ffd3473440d3f705da16299724271f660b6fe367f455b205bc014eff3e20defd011f92000f94d39365ca0bc7867200");
149    }
150}