Skip to main content

tempo_alloy/
transport.rs

1//! Relay transport for routing sponsored transactions through a fee payer service.
2//!
3//! `RelayTransport` wraps two transports:
4//! - a default Tempo RPC transport for ordinary requests and sign-only broadcasts.
5//! - a sponsor transport for signing or sign-and-relay raw transaction submissions.
6//!
7//! When a single `eth_sendRawTransaction` or `eth_sendRawTransactionSync` request is submitted, the
8//! raw unsigned Tempo AA transaction is locally preflighted. In `SponsorshipMode::SignAndRelay` it is
9//! re-encoded for the fee-payer service and forwarded to the sponsor, which signs, broadcasts, and
10//! returns the transaction hash or sync response. In `SponsorshipMode::SignOnly` the sponsor signs via
11//! `eth_signRawTransaction`, then the signed raw transaction is broadcast through the default
12//! transport using the original submission method. Non-transaction requests are forwarded unchanged
13//! to the default transport. JSON-RPC batches containing raw transaction submissions are rejected;
14//! use Tempo AA native call batching instead.
15//!
16//! Sign-and-relay can forward original request headers to the sponsor; sign-only never forwards
17//! them to sponsor signing and preserves them only for the final default-transport broadcast.
18
19use alloy_consensus::transaction::SignerRecoverable;
20use alloy_eips::Decodable2718;
21use alloy_json_rpc::{
22    Request, RequestPacket, ResponsePacket, ResponsePayload, RpcError, SerializedRequest,
23};
24use alloy_primitives::hex;
25use alloy_rpc_client::BuiltInConnectionString;
26use alloy_transport::{
27    Authorization, BoxTransport, TransportConnect, TransportError, TransportErrorKind, TransportFut,
28};
29use http::HeaderValue;
30use std::str::FromStr;
31use tempo_primitives::{AASigned, TempoTxEnvelope, transaction::FEE_PAYER_SIGNATURE_MARKER};
32
33/// How sponsored raw transactions are handled by [`RelayTransport`].
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
35pub enum SponsorshipMode {
36    /// Forward marked `eth_sendRawTransaction` requests to the sponsor, which signs and broadcasts.
37    #[default]
38    SignAndRelay,
39    /// Ask the sponsor to sign with `eth_signRawTransaction`, then broadcast through default RPC.
40    SignOnly,
41}
42
43/// A Tempo transport that routes sponsored `eth_sendRawTransaction` requests.
44///
45/// Single `eth_sendRawTransaction` and `eth_sendRawTransactionSync` requests are validated as
46/// unsigned Tempo AA transactions. In [`SponsorshipMode::SignAndRelay`] they are forwarded to the
47/// sponsor relay. In [`SponsorshipMode::SignOnly`] the sponsor returns a fee-payer signed raw
48/// transaction which is then broadcast through the default transport using the original submission
49/// method. All other RPC methods go directly to the default transport. Batched requests containing
50/// raw transaction submissions are rejected; use Tempo AA native batching instead.
51///
52/// Derived requests preserve the original JSON-RPC id. Sign-and-relay can forward original headers
53/// to the sponsor; sign-only keeps sponsor signing isolated and preserves original headers only for
54/// the final default-RPC broadcast. Advanced users can pass customized transports or connectors for
55/// auth, middleware, retry policy, proxies, or dynamic headers.
56///
57/// The relay transport MUST point to a sponsor service.
58#[derive(Debug, Clone)]
59pub struct RelayTransport<D, R> {
60    default: D,
61    relay: R,
62    mode: SponsorshipMode,
63    forward_sponsor_request_headers: bool,
64}
65
66/// Transport connector that combines default and sponsor relay connectors into a [`RelayTransport`].
67///
68/// Use this with explicit connector instances when the default RPC or sponsor endpoint needs custom
69/// configuration such as auth headers, middleware, retry policy, or proxies.
70#[derive(Debug, Clone)]
71pub struct RelayConnector<D, R> {
72    default: D,
73    relay: R,
74    mode: SponsorshipMode,
75    forward_sponsor_request_headers: bool,
76}
77
78impl<D, R> RelayConnector<D, R> {
79    /// Create a connector from default RPC and sponsor relay connectors.
80    pub fn new(default: D, relay: R) -> Self {
81        Self::with_config(default, relay, SponsorshipMode::default(), true)
82    }
83
84    /// Create a connector with explicit mode and sponsor header forwarding.
85    pub fn with_config(
86        default: D,
87        relay: R,
88        mode: SponsorshipMode,
89        forward_sponsor_request_headers: bool,
90    ) -> Self {
91        Self {
92            default,
93            relay,
94            mode,
95            forward_sponsor_request_headers,
96        }
97    }
98}
99
100impl RelayConnector<BuiltInConnectionString, BuiltInConnectionString> {
101    /// Create a relay connector from Alloy built-in connection strings.
102    pub fn builtin(default: &str, relay: &str) -> Result<Self, TransportError> {
103        let default =
104            BuiltInConnectionString::from_str(default).map_err(TransportErrorKind::custom)?;
105        let relay = BuiltInConnectionString::from_str(relay).map_err(TransportErrorKind::custom)?;
106        Ok(Self::new(default, relay))
107    }
108
109    /// Alias for [`Self::builtin`] for the common HTTP URL case.
110    pub fn http(default: &str, relay: &str) -> Result<Self, TransportError> {
111        Self::builtin(default, relay)
112    }
113}
114
115impl<D, R> TransportConnect for RelayConnector<D, R>
116where
117    D: TransportConnect,
118    R: TransportConnect,
119{
120    fn is_local(&self) -> bool {
121        self.default.is_local()
122    }
123
124    async fn get_transport(&self) -> Result<BoxTransport, TransportError> {
125        let default = self.default.get_transport().await?;
126        let relay = self.relay.get_transport().await?;
127        Ok(BoxTransport::new(RelayTransport::with_config(
128            default,
129            relay,
130            self.mode,
131            self.forward_sponsor_request_headers,
132        )))
133    }
134}
135
136impl<D, R> RelayTransport<D, R> {
137    /// Create a new Tempo relay transport.
138    ///
139    /// `default` and `relay` may be customized transports, including auth headers or middleware.
140    pub fn new(default: D, relay: R) -> Self {
141        Self::with_config(default, relay, SponsorshipMode::default(), true)
142    }
143
144    /// Create a new Tempo relay transport with explicit mode and sponsor header forwarding.
145    pub fn with_config(
146        default: D,
147        relay: R,
148        mode: SponsorshipMode,
149        forward_sponsor_request_headers: bool,
150    ) -> Self {
151        Self {
152            default,
153            relay,
154            mode,
155            forward_sponsor_request_headers,
156        }
157    }
158}
159
160const SEND_METHODS: &[&str] = &["eth_sendRawTransaction", "eth_sendRawTransactionSync"];
161const SIGN_METHOD: &str = "eth_signRawTransaction";
162const SPONSOR_SIGNED_TX_HEX_LEN_SLACK: usize = 1024;
163
164#[rustfmt::skip]
165trait RpcService: tower::Service<RequestPacket, Response = ResponsePacket, Error = TransportError, Future = TransportFut<'static>>
166    + Send + 'static {}
167
168#[rustfmt::skip]
169impl<T: Send + 'static> RpcService for T where
170    T: tower::Service<RequestPacket, Response = ResponsePacket, Error = TransportError, Future = TransportFut<'static>> {}
171
172/// Transport wrapper that applies a configured authorization header to every request.
173#[derive(Clone, Debug)]
174pub(crate) struct AuthHeaderTransport<T> {
175    inner: T,
176    auth: HeaderValue,
177}
178
179impl<T> AuthHeaderTransport<T> {
180    pub(crate) fn new(inner: T, auth: Authorization) -> Result<Self, TransportError> {
181        let auth = auth
182            .to_string()
183            .parse()
184            .map_err(TransportErrorKind::non_retryable)?;
185        Ok(Self { inner, auth })
186    }
187
188    fn insert_auth_header(&self, request: &mut SerializedRequest) {
189        request
190            .headers_mut()
191            .insert("authorization", self.auth.clone());
192    }
193}
194
195impl<T> tower::Service<RequestPacket> for AuthHeaderTransport<T>
196where
197    T: RpcService,
198{
199    type Response = ResponsePacket;
200    type Error = TransportError;
201    type Future = TransportFut<'static>;
202
203    fn poll_ready(
204        &mut self,
205        cx: &mut std::task::Context<'_>,
206    ) -> std::task::Poll<Result<(), Self::Error>> {
207        self.inner.poll_ready(cx)
208    }
209
210    fn call(&mut self, mut request: RequestPacket) -> Self::Future {
211        match &mut request {
212            RequestPacket::Single(request) => self.insert_auth_header(request),
213            RequestPacket::Batch(requests) => {
214                requests
215                    .iter_mut()
216                    .for_each(|request| self.insert_auth_header(request));
217            }
218        }
219        self.inner.call(request)
220    }
221}
222
223impl<D, R> tower::Service<RequestPacket> for RelayTransport<D, R>
224where
225    D: RpcService + Clone + Sync,
226    R: RpcService + Clone + Sync,
227{
228    type Response = ResponsePacket;
229    type Error = TransportError;
230    type Future = TransportFut<'static>;
231
232    fn poll_ready(
233        &mut self,
234        cx: &mut std::task::Context<'_>,
235    ) -> std::task::Poll<Result<(), Self::Error>> {
236        futures::ready!(self.default.poll_ready(cx))?;
237        futures::ready!(self.relay.poll_ready(cx))?;
238        std::task::Poll::Ready(Ok(()))
239    }
240
241    fn call(&mut self, request: RequestPacket) -> Self::Future {
242        match request {
243            RequestPacket::Single(req) if SEND_METHODS.contains(&req.method()) => {
244                let mut transport = self.clone();
245                Box::pin(async move { transport.handle_sponsored_send_raw_transaction(req).await })
246            }
247            RequestPacket::Batch(reqs)
248                if reqs.iter().any(|req| SEND_METHODS.contains(&req.method())) =>
249            {
250                Box::pin(async move {
251                    Err(TransportErrorKind::custom_str(
252                        "RelayTransport does not support JSON-RPC batches containing raw transaction submissions; use a single Tempo AA transaction with multiple calls",
253                    ))
254                })
255            }
256            other => self.default.call(other),
257        }
258    }
259}
260
261fn validate_send_raw_request(request: &SerializedRequest) -> Result<&str, TransportError> {
262    let raw_tx = extract_raw_transaction(request.serialized().get())?;
263    decode_unsigned_tempo_aa(raw_tx)?;
264    Ok(raw_tx)
265}
266
267impl<D, R> RelayTransport<D, R> {
268    async fn handle_sponsored_send_raw_transaction(
269        &mut self,
270        request: SerializedRequest,
271    ) -> Result<ResponsePacket, TransportError>
272    where
273        D: RpcService,
274        R: RpcService,
275    {
276        let method = request.method();
277        debug_assert!(SEND_METHODS.contains(&method));
278        let raw_tx = validate_send_raw_request(&request)?;
279        let unsigned_tx = decode_unsigned_tempo_aa(raw_tx)?;
280        let sponsor_raw_tx = encode_for_fee_payer_service(&unsigned_tx);
281
282        match self.mode {
283            SponsorshipMode::SignAndRelay => {
284                let relay_request = tx_request(
285                    method,
286                    &sponsor_raw_tx,
287                    &request,
288                    self.forward_sponsor_request_headers,
289                )?;
290                self.relay.call(RequestPacket::Single(relay_request)).await
291            }
292            SponsorshipMode::SignOnly => {
293                let sign_request = tx_request(SIGN_METHOD, &sponsor_raw_tx, &request, false)?;
294                let signed_tx: String =
295                    match self.relay.call(RequestPacket::Single(sign_request)).await? {
296                        ResponsePacket::Single(response) => match response.payload {
297                            ResponsePayload::Success(payload) => {
298                                serde_json::from_str(payload.get())
299                                    .map_err(TransportErrorKind::non_retryable)?
300                            }
301                            ResponsePayload::Failure(err) => {
302                                return Err(RpcError::ErrorResp(err));
303                            }
304                        },
305                        ResponsePacket::Batch(_) => {
306                            return Err(TransportErrorKind::custom_str(
307                                "sponsor returned a batch response to eth_signRawTransaction",
308                            ));
309                        }
310                    };
311
312                validate_sponsor_signed_tx_len(&signed_tx, raw_tx.len())?;
313                let signed_tx_envelope = validate_signed_tempo_aa(&signed_tx)?;
314                validate_sponsor_signed_same_payload(&unsigned_tx, &signed_tx_envelope)?;
315                let send_request = tx_request(method, &signed_tx, &request, true)?;
316                self.default.call(RequestPacket::Single(send_request)).await
317            }
318        }
319    }
320}
321
322fn encode_for_fee_payer_service(tx: &AASigned) -> String {
323    let mut buf = Vec::new();
324    tx.encode_for_fee_payer_service(&mut buf);
325    hex::encode_prefixed(buf)
326}
327
328fn tx_request(
329    method: &str,
330    tx: &str,
331    original: &SerializedRequest,
332    forward_headers: bool,
333) -> Result<SerializedRequest, TransportError> {
334    let mut request: SerializedRequest =
335        Request::new(method.to_owned(), original.id().clone(), Some([tx]))
336            .try_into()
337            .map_err(TransportErrorKind::non_retryable)?;
338
339    if forward_headers && let Some(headers) = original.headers() {
340        request.headers_mut().extend(headers.clone());
341    }
342
343    Ok(request)
344}
345
346fn extract_raw_transaction(serialized_request: &str) -> Result<&str, TransportError> {
347    #[derive(serde::Deserialize)]
348    struct SendRawRequest<'a> {
349        #[serde(borrow)]
350        params: [&'a str; 1],
351    }
352
353    let request: SendRawRequest<'_> =
354        serde_json::from_str(serialized_request).map_err(TransportErrorKind::non_retryable)?;
355    Ok(request.params[0])
356}
357
358fn decode_tempo_envelope(raw_tx: &str) -> Result<TempoTxEnvelope, TransportError> {
359    let raw_tx = raw_tx
360        .strip_prefix("0x")
361        .ok_or_else(|| TransportErrorKind::custom_str("raw transaction must be 0x-prefixed"))?;
362    let bytes = hex::decode(raw_tx).map_err(TransportErrorKind::non_retryable)?;
363    TempoTxEnvelope::decode_2718(&mut bytes.as_slice()).map_err(TransportErrorKind::non_retryable)
364}
365
366fn validate_sponsor_signed_tx_len(
367    signed_raw_tx: &str,
368    unsigned_raw_tx_len: usize,
369) -> Result<(), TransportError> {
370    let max_len = unsigned_raw_tx_len.saturating_add(SPONSOR_SIGNED_TX_HEX_LEN_SLACK);
371    if signed_raw_tx.len() > max_len {
372        return Err(TransportErrorKind::custom_str(
373            "sponsor returned raw transaction exceeding expected size",
374        ));
375    }
376    Ok(())
377}
378
379fn validate_signed_tempo_aa(raw_tx: &str) -> Result<AASigned, TransportError> {
380    let tx = decode_tempo_aa(raw_tx, "sponsor returned non-Tempo AA transaction")?;
381    match tx.tx().fee_payer_signature.as_ref() {
382        Some(sig) if *sig == FEE_PAYER_SIGNATURE_MARKER => Err(TransportErrorKind::custom_str(
383            "sponsor returned transaction with fee-payer signature placeholder",
384        )),
385        Some(_) => recover_tempo_aa_signer(tx),
386        None => Err(TransportErrorKind::custom_str(
387            "sponsor returned transaction without fee-payer signature",
388        )),
389    }
390}
391
392fn decode_unsigned_tempo_aa(raw_tx: &str) -> Result<AASigned, TransportError> {
393    let tx = decode_tempo_aa(raw_tx, "raw transaction is not a Tempo AA transaction")?;
394    match tx.tx().fee_payer_signature.as_ref() {
395        Some(sig) if *sig == FEE_PAYER_SIGNATURE_MARKER => recover_tempo_aa_signer(tx),
396        Some(_) => Err(TransportErrorKind::custom_str(
397            "raw transaction is already fee-payer signed",
398        )),
399        None => Err(TransportErrorKind::custom_str(
400            "raw transaction is missing fee-payer signature placeholder",
401        )),
402    }
403}
404
405/// Validate that the sponsor only replaced the fee-payer signature.
406fn validate_sponsor_signed_same_payload(
407    unsigned: &AASigned,
408    signed: &AASigned,
409) -> Result<(), TransportError> {
410    if unsigned.signature() != signed.signature() {
411        return Err(TransportErrorKind::custom_str(
412            "sponsor returned transaction with different user signature",
413        ));
414    }
415
416    // Normalize the sponsor response to validate all tx fields.
417    let mut signed_tx = signed.tx().clone();
418    signed_tx.fee_payer_signature = Some(FEE_PAYER_SIGNATURE_MARKER);
419    if unsigned.tx() != &signed_tx {
420        return Err(TransportErrorKind::custom_str(
421            "sponsor returned transaction with different payload",
422        ));
423    }
424
425    Ok(())
426}
427
428fn decode_tempo_aa(raw_tx: &str, non_aa_error: &'static str) -> Result<AASigned, TransportError> {
429    match decode_tempo_envelope(raw_tx)? {
430        TempoTxEnvelope::AA(tx) => Ok(tx),
431        _ => Err(TransportErrorKind::custom_str(non_aa_error)),
432    }
433}
434
435fn recover_tempo_aa_signer(tx: AASigned) -> Result<AASigned, TransportError> {
436    tx.recover_signer()
437        .map_err(TransportErrorKind::non_retryable)?;
438    Ok(tx)
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use alloy_eips::Encodable2718;
445    use alloy_json_rpc::{Id, Request, Response, ResponsePayload, SerializedRequest};
446    use alloy_primitives::{Address, Bytes, TxKind, U256, hex};
447    use alloy_signer::SignerSync;
448    use serde_json::value::RawValue;
449    use std::{
450        collections::VecDeque,
451        sync::{Arc, Mutex},
452    };
453    use tempo_primitives::{
454        TempoSignature, TempoTransaction, TempoTxEnvelope,
455        transaction::{Call, FEE_PAYER_SIGNATURE_MARKER, PrimitiveSignature},
456    };
457
458    #[derive(Clone, Debug, Default)]
459    struct RecordingTransport {
460        requests: Arc<Mutex<Vec<SerializedRequest>>>,
461        responses: Arc<Mutex<VecDeque<ResponsePayload>>>,
462    }
463
464    impl RecordingTransport {
465        fn push_success<T: serde::Serialize>(&self, value: &T) {
466            let raw = RawValue::from_string(serde_json::to_string(value).unwrap()).unwrap();
467            self.responses
468                .lock()
469                .unwrap()
470                .push_back(ResponsePayload::Success(raw));
471        }
472        fn push_failure(&self, message: &'static str) {
473            self.responses
474                .lock()
475                .unwrap()
476                .push_back(ResponsePayload::Failure(
477                    alloy_json_rpc::ErrorPayload::internal_error_message(message.into()),
478                ));
479        }
480        fn methods(&self) -> Vec<String> {
481            self.requests
482                .lock()
483                .unwrap()
484                .iter()
485                .map(|req| req.method().to_string())
486                .collect()
487        }
488        fn params(&self, index: usize) -> serde_json::Value {
489            let requests = self.requests.lock().unwrap();
490            let request: serde_json::Value =
491                serde_json::from_str(requests[index].serialized().get()).unwrap();
492            request.get("params").cloned().unwrap_or_default()
493        }
494        fn ids(&self) -> Vec<Id> {
495            self.requests
496                .lock()
497                .unwrap()
498                .iter()
499                .map(|req| req.id().clone())
500                .collect()
501        }
502        fn header_value(&self, index: usize, name: &str) -> Option<String> {
503            self.requests.lock().unwrap()[index]
504                .headers()
505                .and_then(|headers| headers.get(name))
506                .and_then(|value| value.to_str().ok())
507                .map(ToOwned::to_owned)
508        }
509        fn record(&self, req: SerializedRequest) -> Result<Response, TransportError> {
510            let payload = self
511                .responses
512                .lock()
513                .unwrap()
514                .pop_front()
515                .ok_or_else(|| TransportErrorKind::custom_str("missing mock response"))?;
516            self.requests.lock().unwrap().push(req.clone());
517            if let ResponsePayload::Failure(err) = payload {
518                return Err(TransportErrorKind::custom_str(&err.message));
519            }
520            Ok(Response {
521                id: req.id().clone(),
522                payload,
523            })
524        }
525    }
526
527    impl tower::Service<RequestPacket> for RecordingTransport {
528        type Response = ResponsePacket;
529        type Error = TransportError;
530        type Future = TransportFut<'static>;
531        fn poll_ready(
532            &mut self,
533            _cx: &mut std::task::Context<'_>,
534        ) -> std::task::Poll<Result<(), Self::Error>> {
535            std::task::Poll::Ready(Ok(()))
536        }
537        fn call(&mut self, req: RequestPacket) -> Self::Future {
538            let this = self.clone();
539            Box::pin(async move {
540                Ok(match req {
541                    RequestPacket::Single(req) => ResponsePacket::Single(this.record(req)?),
542                    RequestPacket::Batch(reqs) => ResponsePacket::Batch(
543                        reqs.into_iter()
544                            .map(|req| this.record(req))
545                            .collect::<Result<_, _>>()?,
546                    ),
547                })
548            })
549        }
550    }
551
552    #[derive(Clone, Debug)]
553    struct ConnectForTest(RecordingTransport);
554    impl TransportConnect for ConnectForTest {
555        fn is_local(&self) -> bool {
556            true
557        }
558        async fn get_transport(&self) -> Result<BoxTransport, TransportError> {
559            Ok(BoxTransport::new(self.0.clone()))
560        }
561    }
562
563    fn make_request(method: &'static str) -> RequestPacket {
564        RequestPacket::Single(
565            Request::new(method, Id::Number(1), None::<&RawValue>)
566                .try_into()
567                .unwrap(),
568        )
569    }
570    fn send_req_with_id(raw_tx: &str, id: Id, sync: bool) -> SerializedRequest {
571        Request::new(SEND_METHODS[usize::from(sync)], id, Some([raw_tx]))
572            .try_into()
573            .unwrap()
574    }
575    fn make_send_raw_tx_request(raw_tx: &str, sync: bool) -> RequestPacket {
576        RequestPacket::Single(send_req_with_id(raw_tx, Id::Number(1), sync))
577    }
578    fn make_batch_no_send() -> RequestPacket {
579        RequestPacket::Batch(vec![
580            Request::new("eth_chainId", Id::Number(1), None::<&RawValue>)
581                .try_into()
582                .unwrap(),
583            Request::new("eth_blockNumber", Id::Number(2), None::<&RawValue>)
584                .try_into()
585                .unwrap(),
586        ])
587    }
588    fn make_batch_with_send_raw_tx(raw_tx: &str) -> RequestPacket {
589        RequestPacket::Batch(vec![
590            Request::new("eth_chainId", Id::Number(1), None::<&RawValue>)
591                .try_into()
592                .unwrap(),
593            Request::new(SEND_METHODS[0], Id::Number(2), Some([raw_tx]))
594                .try_into()
595                .unwrap(),
596        ])
597    }
598
599    const USER_PK: &str = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
600    const FEE_PAYER_PK: &str = "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a";
601
602    fn signed_tempo_aa_raw_tx_with_nonce(fee_payer_signed: bool, nonce: u64) -> String {
603        let user: alloy_signer_local::PrivateKeySigner = USER_PK.parse().unwrap();
604        let fee_payer: alloy_signer_local::PrivateKeySigner = FEE_PAYER_PK.parse().unwrap();
605        let mut tx = TempoTransaction {
606            chain_id: 42431,
607            max_priority_fee_per_gas: 1,
608            max_fee_per_gas: 1,
609            gas_limit: 21_000,
610            calls: vec![Call {
611                to: TxKind::Call(Address::repeat_byte(0x11)),
612                value: U256::ZERO,
613                input: Bytes::new(),
614            }],
615            nonce,
616            fee_payer_signature: Some(FEE_PAYER_SIGNATURE_MARKER),
617            ..Default::default()
618        };
619        let user_sig = user.sign_hash_sync(&tx.signature_hash()).unwrap();
620        if fee_payer_signed {
621            tx.fee_payer_signature = Some(
622                fee_payer
623                    .sign_hash_sync(&tx.fee_payer_signature_hash(user.address()))
624                    .unwrap(),
625            );
626        }
627        let envelope = TempoTxEnvelope::AA(tx.into_signed(TempoSignature::Primitive(
628            PrimitiveSignature::Secp256k1(user_sig),
629        )));
630        let mut encoded = Vec::new();
631        envelope.encode_2718(&mut encoded);
632        format!("0x{}", hex::encode(encoded))
633    }
634
635    #[test]
636    fn decodes_unsigned_tempo_aa_raw_transaction_for_sdk_users() {
637        let raw_tx = signed_tempo_aa_raw_tx_with_nonce(false, 1);
638        let user: alloy_signer_local::PrivateKeySigner = USER_PK.parse().unwrap();
639
640        let tx = decode_unsigned_tempo_aa(&raw_tx).unwrap();
641        let signer = tx.recover_signer().unwrap();
642
643        assert_eq!(signer, user.address());
644        assert_eq!(
645            tx.tx().fee_payer_signature,
646            Some(FEE_PAYER_SIGNATURE_MARKER)
647        );
648    }
649
650    #[test]
651    fn decode_unsigned_tempo_aa_raw_transaction_rejects_fee_payer_signed_tx() {
652        let err = decode_unsigned_tempo_aa(&signed_tempo_aa_raw_tx_with_nonce(true, 1))
653            .expect_err("fee-payer signed tx should be rejected");
654
655        assert!(err.to_string().contains("already fee-payer signed"));
656    }
657
658    #[tokio::test]
659    async fn routes_non_send_to_default_only() {
660        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
661        default.push_success(&alloy_primitives::U64::from(42));
662        let mut rpc = RelayTransport::new(default, relay);
663        assert!(
664            tower::Service::call(&mut rpc, make_request("eth_getTransactionCount"))
665                .await
666                .is_ok()
667        );
668        assert_eq!(rpc.default.methods(), vec!["eth_getTransactionCount"]);
669        assert!(rpc.relay.methods().is_empty());
670    }
671
672    #[tokio::test]
673    async fn batch_without_send_forwards_to_default_unchanged() {
674        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
675        default.push_success(&alloy_primitives::U64::from(42431));
676        default.push_success(&alloy_primitives::U64::from(7));
677        let mut rpc = RelayTransport::new(default, relay);
678        assert!(
679            tower::Service::call(&mut rpc, make_batch_no_send())
680                .await
681                .is_ok()
682        );
683        assert_eq!(
684            rpc.default.methods(),
685            vec!["eth_chainId", "eth_blockNumber"]
686        );
687        assert!(rpc.relay.methods().is_empty());
688    }
689
690    #[tokio::test]
691    async fn send_raw_tx_forwards_to_relay_with_fee_payer_service_encoding() {
692        let raw_tx = signed_tempo_aa_raw_tx_with_nonce(false, 1);
693        let sponsor_raw_tx =
694            encode_for_fee_payer_service(&decode_unsigned_tempo_aa(&raw_tx).unwrap());
695        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
696        relay.push_success(&alloy_primitives::B256::ZERO);
697        let mut rpc = RelayTransport::new(default, relay);
698        assert!(
699            tower::Service::call(&mut rpc, make_send_raw_tx_request(&raw_tx, false))
700                .await
701                .is_ok()
702        );
703        assert_eq!(rpc.relay.methods(), vec![SEND_METHODS[0]]);
704        assert_eq!(rpc.relay.params(0), serde_json::json!([sponsor_raw_tx]));
705        assert!(rpc.default.methods().is_empty());
706    }
707
708    #[tokio::test]
709    async fn send_raw_tx_sync_forwards_to_relay_with_original_method() {
710        let raw_tx = signed_tempo_aa_raw_tx_with_nonce(false, 1);
711        let sponsor_raw_tx =
712            encode_for_fee_payer_service(&decode_unsigned_tempo_aa(&raw_tx).unwrap());
713        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
714        relay.push_success(&alloy_primitives::B256::ZERO);
715        let mut rpc = RelayTransport::new(default, relay);
716
717        assert!(
718            tower::Service::call(&mut rpc, make_send_raw_tx_request(&raw_tx, true))
719                .await
720                .is_ok()
721        );
722
723        assert_eq!(rpc.relay.methods(), vec![SEND_METHODS[1]]);
724        assert_eq!(rpc.relay.params(0), serde_json::json!([sponsor_raw_tx]));
725        assert!(rpc.default.methods().is_empty());
726    }
727
728    #[tokio::test]
729    async fn rejects_non_tempo_aa_before_relay() {
730        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
731        let mut rpc = RelayTransport::new(default, relay);
732        assert!(
733            tower::Service::call(&mut rpc, make_send_raw_tx_request("0x01", false))
734                .await
735                .is_err()
736        );
737        assert!(rpc.default.methods().is_empty());
738        assert!(rpc.relay.methods().is_empty());
739    }
740
741    #[tokio::test]
742    async fn rejects_already_fee_payer_signed_tx_before_relay() {
743        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
744        let mut rpc = RelayTransport::new(default, relay);
745        assert!(
746            tower::Service::call(
747                &mut rpc,
748                make_send_raw_tx_request(&signed_tempo_aa_raw_tx_with_nonce(true, 1), false)
749            )
750            .await
751            .is_err()
752        );
753        assert!(rpc.default.methods().is_empty());
754        assert!(rpc.relay.methods().is_empty());
755    }
756
757    #[tokio::test]
758    async fn sign_only_gets_sponsor_signature_then_broadcasts_to_default() {
759        let unsigned_raw_tx = signed_tempo_aa_raw_tx_with_nonce(false, 1);
760        let signed_raw_tx = signed_tempo_aa_raw_tx_with_nonce(true, 1);
761        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
762        relay.push_success(&signed_raw_tx);
763        default.push_success(&alloy_primitives::B256::ZERO);
764        let mut rpc = RelayTransport::with_config(default, relay, SponsorshipMode::SignOnly, true);
765
766        let mut req = send_req_with_id(&unsigned_raw_tx, Id::Number(1), true);
767        req.headers_mut()
768            .insert("authorization", "Bearer default-rpc-token".parse().unwrap());
769        assert!(
770            tower::Service::call(&mut rpc, RequestPacket::Single(req))
771                .await
772                .is_ok()
773        );
774
775        let sponsor_raw_tx =
776            encode_for_fee_payer_service(&decode_unsigned_tempo_aa(&unsigned_raw_tx).unwrap());
777        assert_eq!(rpc.relay.methods(), vec![SIGN_METHOD]);
778        assert_eq!(rpc.relay.params(0), serde_json::json!([sponsor_raw_tx]));
779        assert_eq!(rpc.relay.header_value(0, "authorization"), None);
780        assert_eq!(rpc.default.methods(), vec![SEND_METHODS[1]]);
781        assert_eq!(rpc.default.params(0), serde_json::json!([signed_raw_tx]));
782        assert_eq!(
783            rpc.default.header_value(0, "authorization"),
784            Some("Bearer default-rpc-token".to_string())
785        );
786    }
787
788    #[tokio::test]
789    async fn auth_header_transport_sets_configured_authorization() {
790        let inner = RecordingTransport::default();
791        inner.push_success(&alloy_primitives::B256::ZERO);
792        let mut rpc = AuthHeaderTransport::new(
793            BoxTransport::new(inner.clone()),
794            Authorization::bearer("sponsor-token"),
795        )
796        .unwrap();
797        let mut request = send_req_with_id("0x01", Id::Number(1), false);
798        request
799            .headers_mut()
800            .insert("authorization", "Bearer original-token".parse().unwrap());
801
802        tower::Service::call(&mut rpc, RequestPacket::Single(request))
803            .await
804            .unwrap();
805
806        assert_eq!(
807            inner.header_value(0, "authorization"),
808            Some("Bearer sponsor-token".to_string())
809        );
810    }
811
812    #[tokio::test]
813    async fn sign_only_rejects_oversized_sponsor_response_before_decoding() {
814        let unsigned_raw_tx = signed_tempo_aa_raw_tx_with_nonce(false, 1);
815        let oversized_signed_raw_tx = format!(
816            "0x{}",
817            "00".repeat(unsigned_raw_tx.len() + SPONSOR_SIGNED_TX_HEX_LEN_SLACK)
818        );
819        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
820        relay.push_success(&oversized_signed_raw_tx);
821        let mut rpc = RelayTransport::with_config(default, relay, SponsorshipMode::SignOnly, true);
822
823        let err = tower::Service::call(&mut rpc, make_send_raw_tx_request(&unsigned_raw_tx, false))
824            .await
825            .unwrap_err();
826
827        assert!(err.to_string().contains("exceeding expected size"));
828        assert_eq!(rpc.relay.methods(), vec![SIGN_METHOD]);
829        assert!(rpc.default.methods().is_empty());
830    }
831
832    #[tokio::test]
833    async fn sign_only_rejects_sponsor_response_without_fee_payer_signature() {
834        let unsigned_raw_tx = signed_tempo_aa_raw_tx_with_nonce(false, 1);
835        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
836        relay.push_success(&unsigned_raw_tx);
837        let mut rpc = RelayTransport::with_config(default, relay, SponsorshipMode::SignOnly, true);
838
839        let err = tower::Service::call(&mut rpc, make_send_raw_tx_request(&unsigned_raw_tx, false))
840            .await
841            .unwrap_err();
842
843        assert!(err.to_string().contains("fee-payer signature placeholder"));
844        assert_eq!(rpc.relay.methods(), vec![SIGN_METHOD]);
845        assert!(rpc.default.methods().is_empty());
846    }
847
848    #[tokio::test]
849    async fn sign_only_rejects_sponsor_response_with_different_payload() {
850        let unsigned_raw_tx = signed_tempo_aa_raw_tx_with_nonce(false, 1);
851        let different_signed_raw_tx = signed_tempo_aa_raw_tx_with_nonce(true, 2);
852        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
853        relay.push_success(&different_signed_raw_tx);
854        let mut rpc = RelayTransport::with_config(default, relay, SponsorshipMode::SignOnly, true);
855
856        let err = tower::Service::call(&mut rpc, make_send_raw_tx_request(&unsigned_raw_tx, false))
857            .await
858            .unwrap_err();
859
860        assert!(err.to_string().contains("different"));
861        assert_eq!(rpc.relay.methods(), vec![SIGN_METHOD]);
862        assert!(rpc.default.methods().is_empty());
863    }
864
865    #[tokio::test]
866    async fn relay_error_propagates_without_default_call() {
867        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
868        relay.push_failure("sponsor account broke");
869        let mut rpc = RelayTransport::new(default, relay);
870        assert!(
871            tower::Service::call(
872                &mut rpc,
873                make_send_raw_tx_request(&signed_tempo_aa_raw_tx_with_nonce(false, 1), false)
874            )
875            .await
876            .is_err()
877        );
878        assert_eq!(rpc.relay.methods(), vec![SEND_METHODS[0]]);
879        assert!(rpc.default.methods().is_empty());
880    }
881
882    #[tokio::test]
883    async fn preserves_request_id_when_forwarding_to_relay() {
884        let raw_tx = signed_tempo_aa_raw_tx_with_nonce(false, 1);
885        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
886        relay.push_success(&alloy_primitives::B256::ZERO);
887        let mut rpc = RelayTransport::new(default, relay);
888        let req = RequestPacket::Single(send_req_with_id(&raw_tx, Id::String("abc".into()), false));
889        assert!(tower::Service::call(&mut rpc, req).await.is_ok());
890        assert_eq!(rpc.relay.ids(), vec![Id::String("abc".into())]);
891    }
892
893    #[tokio::test]
894    async fn sign_and_relay_forwards_original_request_headers_to_relay() {
895        let raw_tx = signed_tempo_aa_raw_tx_with_nonce(false, 1);
896        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
897        relay.push_success(&alloy_primitives::B256::ZERO);
898        let mut rpc = RelayTransport::new(default, relay);
899        let mut req = send_req_with_id(&raw_tx, Id::Number(1), false);
900        req.headers_mut()
901            .insert("authorization", "Bearer sponsor-token".parse().unwrap());
902        assert!(
903            tower::Service::call(&mut rpc, RequestPacket::Single(req))
904                .await
905                .is_ok()
906        );
907        assert_eq!(
908            rpc.relay.header_value(0, "authorization"),
909            Some("Bearer sponsor-token".to_string())
910        );
911    }
912
913    #[tokio::test]
914    async fn sign_and_relay_can_skip_original_request_headers_to_relay() {
915        let raw_tx = signed_tempo_aa_raw_tx_with_nonce(false, 1);
916        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
917        relay.push_success(&alloy_primitives::B256::ZERO);
918        let mut rpc =
919            RelayTransport::with_config(default, relay, SponsorshipMode::SignAndRelay, false);
920        let mut req = send_req_with_id(&raw_tx, Id::Number(1), false);
921        req.headers_mut()
922            .insert("authorization", "Bearer default-token".parse().unwrap());
923
924        assert!(
925            tower::Service::call(&mut rpc, RequestPacket::Single(req))
926                .await
927                .is_ok()
928        );
929
930        assert_eq!(rpc.relay.header_value(0, "authorization"), None);
931    }
932
933    #[tokio::test]
934    async fn rejects_batch_containing_send_before_any_transport_call() {
935        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
936        let mut rpc = RelayTransport::new(default, relay);
937        let err = tower::Service::call(
938            &mut rpc,
939            make_batch_with_send_raw_tx(&signed_tempo_aa_raw_tx_with_nonce(false, 1)),
940        )
941        .await
942        .unwrap_err();
943        assert!(
944            err.to_string().contains(
945                "does not support JSON-RPC batches containing raw transaction submissions"
946            )
947        );
948        assert!(rpc.default.methods().is_empty());
949        assert!(rpc.relay.methods().is_empty());
950    }
951
952    #[tokio::test]
953    async fn relay_connector_builds_boxed_relay_transport() {
954        let (default, relay) = (RecordingTransport::default(), RecordingTransport::default());
955        default.push_success(&alloy_primitives::U64::from(42));
956        let connect = RelayConnector::new(ConnectForTest(default), ConnectForTest(relay));
957        assert!(connect.is_local());
958        let mut rpc = connect.get_transport().await.unwrap();
959        assert!(
960            tower::Service::call(&mut rpc, make_request("eth_chainId"))
961                .await
962                .is_ok()
963        );
964        assert_eq!(connect.default.0.methods(), vec!["eth_chainId"]);
965        assert!(connect.relay.0.methods().is_empty());
966    }
967
968    #[test]
969    fn relay_connector_parses_builtin_urls() {
970        let connect =
971            RelayConnector::http("http://localhost:8545", "https://sponsor.testnet.tempo.xyz")
972                .unwrap();
973        assert!(connect.is_local());
974    }
975}