1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
35pub enum SponsorshipMode {
36 #[default]
38 SignAndRelay,
39 SignOnly,
41}
42
43#[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#[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 pub fn new(default: D, relay: R) -> Self {
81 Self::with_config(default, relay, SponsorshipMode::default(), true)
82 }
83
84 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 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 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 pub fn new(default: D, relay: R) -> Self {
141 Self::with_config(default, relay, SponsorshipMode::default(), true)
142 }
143
144 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#[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
405fn 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 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}