tempo_alloy/
network.rs

1use std::fmt::Debug;
2
3use crate::rpc::{TempoHeaderResponse, TempoTransactionReceipt, TempoTransactionRequest};
4use alloy_consensus::{ReceiptWithBloom, TxType, error::UnsupportedTransactionType};
5
6use alloy_network::{
7    BuildResult, Ethereum, EthereumWallet, IntoWallet, Network, NetworkWallet, TransactionBuilder,
8    TransactionBuilderError, UnbuiltTransactionError,
9};
10use alloy_primitives::{Address, Bytes, ChainId, TxKind, U256};
11use alloy_provider::fillers::{
12    ChainIdFiller, GasFiller, JoinFill, NonceFiller, RecommendedFillers,
13};
14use alloy_rpc_types_eth::{AccessList, Block, Transaction};
15use alloy_signer_local::PrivateKeySigner;
16use tempo_primitives::{
17    TempoHeader, TempoReceipt, TempoTxEnvelope, TempoTxType, transaction::TempoTypedTransaction,
18};
19
20/// Set of recommended fillers.
21///
22/// `N` is a nonce filler.
23pub(crate) type TempoFillers<N> = JoinFill<GasFiller, JoinFill<N, ChainIdFiller>>;
24
25/// The Tempo specific configuration of [`Network`] schema and consensus primitives.
26#[derive(Default, Debug, Clone, Copy)]
27#[non_exhaustive]
28pub struct TempoNetwork;
29
30impl Network for TempoNetwork {
31    type TxType = TempoTxType;
32    type TxEnvelope = TempoTxEnvelope;
33    type UnsignedTx = TempoTypedTransaction;
34    type ReceiptEnvelope = ReceiptWithBloom<TempoReceipt>;
35    type Header = TempoHeader;
36    type TransactionRequest = TempoTransactionRequest;
37    type TransactionResponse = Transaction<TempoTxEnvelope>;
38    type ReceiptResponse = TempoTransactionReceipt;
39    type HeaderResponse = TempoHeaderResponse;
40    type BlockResponse = Block<Transaction<TempoTxEnvelope>, Self::HeaderResponse>;
41}
42
43impl TransactionBuilder<TempoNetwork> for TempoTransactionRequest {
44    fn chain_id(&self) -> Option<ChainId> {
45        self.inner.chain_id()
46    }
47
48    fn set_chain_id(&mut self, chain_id: ChainId) {
49        self.inner.set_chain_id(chain_id)
50    }
51
52    fn nonce(&self) -> Option<u64> {
53        TransactionBuilder::nonce(&self.inner)
54    }
55
56    fn set_nonce(&mut self, nonce: u64) {
57        self.inner.set_nonce(nonce)
58    }
59
60    fn take_nonce(&mut self) -> Option<u64> {
61        self.inner.take_nonce()
62    }
63
64    fn input(&self) -> Option<&Bytes> {
65        TransactionBuilder::input(&self.inner)
66    }
67
68    fn set_input<T: Into<Bytes>>(&mut self, input: T) {
69        TransactionBuilder::set_input(&mut self.inner, input)
70    }
71
72    fn from(&self) -> Option<Address> {
73        TransactionBuilder::from(&self.inner)
74    }
75
76    fn set_from(&mut self, from: Address) {
77        TransactionBuilder::set_from(&mut self.inner, from)
78    }
79
80    fn kind(&self) -> Option<TxKind> {
81        self.inner.kind()
82    }
83
84    fn clear_kind(&mut self) {
85        self.inner.clear_kind()
86    }
87
88    fn set_kind(&mut self, kind: TxKind) {
89        self.inner.set_kind(kind)
90    }
91
92    fn value(&self) -> Option<U256> {
93        TransactionBuilder::value(&self.inner)
94    }
95
96    fn set_value(&mut self, value: U256) {
97        self.inner.set_value(value)
98    }
99
100    fn gas_price(&self) -> Option<u128> {
101        TransactionBuilder::gas_price(&self.inner)
102    }
103
104    fn set_gas_price(&mut self, gas_price: u128) {
105        TransactionBuilder::set_gas_price(&mut self.inner, gas_price)
106    }
107
108    fn max_fee_per_gas(&self) -> Option<u128> {
109        TransactionBuilder::max_fee_per_gas(&self.inner)
110    }
111
112    fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
113        TransactionBuilder::set_max_fee_per_gas(&mut self.inner, max_fee_per_gas)
114    }
115
116    fn max_priority_fee_per_gas(&self) -> Option<u128> {
117        TransactionBuilder::max_priority_fee_per_gas(&self.inner)
118    }
119
120    fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
121        TransactionBuilder::set_max_priority_fee_per_gas(&mut self.inner, max_priority_fee_per_gas)
122    }
123
124    fn gas_limit(&self) -> Option<u64> {
125        TransactionBuilder::gas_limit(&self.inner)
126    }
127
128    fn set_gas_limit(&mut self, gas_limit: u64) {
129        TransactionBuilder::set_gas_limit(&mut self.inner, gas_limit)
130    }
131
132    fn access_list(&self) -> Option<&AccessList> {
133        TransactionBuilder::access_list(&self.inner)
134    }
135
136    fn set_access_list(&mut self, access_list: AccessList) {
137        TransactionBuilder::set_access_list(&mut self.inner, access_list)
138    }
139
140    fn complete_type(&self, ty: TempoTxType) -> Result<(), Vec<&'static str>> {
141        match ty {
142            TempoTxType::FeeToken => self.complete_fee_token(),
143            TempoTxType::AA => self.complete_aa(),
144            TempoTxType::Legacy
145            | TempoTxType::Eip2930
146            | TempoTxType::Eip1559
147            | TempoTxType::Eip7702 => self
148                .inner
149                .complete_type(ty.try_into().expect("tempo tx types checked")),
150        }
151    }
152
153    fn can_submit(&self) -> bool {
154        self.inner.can_submit()
155    }
156
157    fn can_build(&self) -> bool {
158        self.inner.can_build()
159    }
160
161    fn output_tx_type(&self) -> TempoTxType {
162        if !self.calls.is_empty() || self.nonce_key.is_some() {
163            TempoTxType::AA
164        } else if self.fee_token.is_some() {
165            TempoTxType::FeeToken
166        } else {
167            match self.inner.output_tx_type() {
168                TxType::Legacy => TempoTxType::Legacy,
169                TxType::Eip2930 => TempoTxType::Eip2930,
170                TxType::Eip1559 => TempoTxType::Eip1559,
171                // EIP-4844 transactions are not supported on Tempo
172                TxType::Eip4844 => TempoTxType::Legacy,
173                TxType::Eip7702 => TempoTxType::Eip7702,
174            }
175        }
176    }
177
178    fn output_tx_type_checked(&self) -> Option<TempoTxType> {
179        match self.output_tx_type() {
180            TempoTxType::AA => Some(TempoTxType::AA).filter(|_| self.can_build_aa()),
181            TempoTxType::FeeToken => {
182                Some(TempoTxType::FeeToken).filter(|_| self.can_build_fee_token())
183            }
184            TempoTxType::Legacy
185            | TempoTxType::Eip2930
186            | TempoTxType::Eip1559
187            | TempoTxType::Eip7702 => self.inner.output_tx_type_checked()?.try_into().ok(),
188        }
189    }
190
191    fn prep_for_submission(&mut self) {
192        self.inner.transaction_type = Some(self.output_tx_type() as u8);
193        self.inner.trim_conflicting_keys();
194        self.inner.populate_blob_hashes();
195    }
196
197    fn build_unsigned(self) -> BuildResult<TempoTypedTransaction, TempoNetwork> {
198        match self.output_tx_type() {
199            TempoTxType::AA => match self.complete_aa() {
200                Ok(..) => Ok(self.build_aa().expect("checked by above condition").into()),
201                Err(missing) => Err(TransactionBuilderError::InvalidTransactionRequest(
202                    TempoTxType::AA,
203                    missing,
204                )
205                .into_unbuilt(self)),
206            },
207            TempoTxType::FeeToken => match self.complete_fee_token() {
208                Ok(..) => Ok(self
209                    .build_fee_token()
210                    .expect("checked by above condition")
211                    .into()),
212                Err(missing) => Err(TransactionBuilderError::InvalidTransactionRequest(
213                    TempoTxType::FeeToken,
214                    missing,
215                )
216                .into_unbuilt(self)),
217            },
218            _ => {
219                if let Err((tx_type, missing)) = self.inner.missing_keys() {
220                    return Err(match tx_type.try_into() {
221                        Ok(tx_type) => {
222                            TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
223                        }
224                        Err(err) => TransactionBuilderError::from(err),
225                    }
226                    .into_unbuilt(self));
227                }
228
229                if let Some(TxType::Eip4844) = self.inner.buildable_type() {
230                    return Err(UnbuiltTransactionError {
231                        request: self,
232                        error: TransactionBuilderError::Custom(Box::new(
233                            UnsupportedTransactionType::new(TxType::Eip4844),
234                        )),
235                    });
236                }
237
238                let inner = self
239                    .inner
240                    .build_typed_tx()
241                    .expect("checked by missing_keys");
242
243                Ok(inner.try_into().expect("checked by above condition"))
244            }
245        }
246    }
247
248    async fn build<W: NetworkWallet<TempoNetwork>>(
249        self,
250        wallet: &W,
251    ) -> Result<TempoTxEnvelope, TransactionBuilderError<TempoNetwork>> {
252        Ok(wallet.sign_request(self).await?)
253    }
254}
255
256impl TempoTransactionRequest {
257    fn can_build_aa(&self) -> bool {
258        (!self.calls.is_empty() || self.inner.to.is_some())
259            && self.inner.nonce.is_some()
260            && self.inner.gas.is_some()
261            && self.inner.max_fee_per_gas.is_some()
262            && self.inner.max_priority_fee_per_gas.is_some()
263    }
264
265    fn can_build_fee_token(&self) -> bool {
266        self.fee_token.is_some()
267            && self.inner.nonce.is_some()
268            && self.inner.gas.is_some()
269            && self.inner.max_fee_per_gas.is_some()
270            && self.inner.max_priority_fee_per_gas.is_some()
271            && (self
272                .inner
273                .authorization_list
274                .as_ref()
275                .map(Vec::is_empty)
276                .unwrap_or(true)
277                || matches!(self.inner.to, Some(TxKind::Call(..))))
278    }
279
280    fn complete_aa(&self) -> Result<(), Vec<&'static str>> {
281        let mut fields = Vec::new();
282
283        if self.calls.is_empty() && self.inner.to.is_none() {
284            fields.push("calls or to");
285        }
286        if self.inner.nonce.is_none() {
287            fields.push("nonce");
288        }
289        if self.inner.gas.is_none() {
290            fields.push("gas");
291        }
292        if self.inner.max_fee_per_gas.is_none() {
293            fields.push("max_fee_per_gas");
294        }
295        if self.inner.max_priority_fee_per_gas.is_none() {
296            fields.push("max_priority_fee_per_gas");
297        }
298
299        if fields.is_empty() {
300            Ok(())
301        } else {
302            Err(fields)
303        }
304    }
305
306    fn complete_fee_token(&self) -> Result<(), Vec<&'static str>> {
307        let mut fields = Vec::new();
308
309        if self.fee_token.is_none() {
310            fields.push("fee_token");
311        }
312        if self.inner.gas.is_none() {
313            fields.push("gas");
314        }
315        if self.inner.max_fee_per_gas.is_none() {
316            fields.push("max_fee_per_gas");
317        }
318        if self.inner.max_priority_fee_per_gas.is_none() {
319            fields.push("max_priority_fee_per_gas");
320        }
321        if !self
322            .inner
323            .authorization_list
324            .as_ref()
325            .map(Vec::is_empty)
326            .unwrap_or(true)
327            && !matches!(self.inner.to, Some(TxKind::Call(..)))
328        {
329            fields.push("to");
330        }
331
332        if fields.is_empty() {
333            Ok(())
334        } else {
335            Err(fields)
336        }
337    }
338}
339
340impl RecommendedFillers for TempoNetwork {
341    type RecommendedFillers = TempoFillers<NonceFiller>;
342
343    fn recommended_fillers() -> Self::RecommendedFillers {
344        Default::default()
345    }
346}
347
348impl NetworkWallet<TempoNetwork> for EthereumWallet {
349    fn default_signer_address(&self) -> Address {
350        NetworkWallet::<Ethereum>::default_signer_address(self)
351    }
352
353    fn has_signer_for(&self, address: &Address) -> bool {
354        NetworkWallet::<Ethereum>::has_signer_for(self, address)
355    }
356
357    fn signer_addresses(&self) -> impl Iterator<Item = Address> {
358        NetworkWallet::<Ethereum>::signer_addresses(self)
359    }
360
361    #[doc(alias = "sign_tx_from")]
362    async fn sign_transaction_from(
363        &self,
364        sender: Address,
365        mut tx: TempoTypedTransaction,
366    ) -> alloy_signer::Result<TempoTxEnvelope> {
367        let signer = self.signer_by_address(sender).ok_or_else(|| {
368            alloy_signer::Error::other(format!("Missing signing credential for {sender}"))
369        })?;
370        let sig = signer.sign_transaction(tx.as_dyn_signable_mut()).await?;
371        Ok(tx.into_envelope(sig))
372    }
373}
374
375impl IntoWallet<TempoNetwork> for PrivateKeySigner {
376    type NetworkWallet = EthereumWallet;
377
378    fn into_wallet(self) -> Self::NetworkWallet {
379        self.into()
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386    use alloy_consensus::{TxEip1559, TxEip2930, TxEip7702, TxLegacy};
387    use alloy_eips::eip7702::SignedAuthorization;
388    use alloy_primitives::B256;
389    use alloy_rpc_types_eth::{AccessListItem, Authorization, TransactionRequest};
390    use tempo_primitives::TxFeeToken;
391
392    #[test_case::test_case(
393        TempoTransactionRequest {
394            inner: TransactionRequest {
395                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
396                gas_price: Some(1234),
397                nonce: Some(57),
398                gas: Some(123456),
399                ..Default::default()
400            },
401            ..Default::default()
402        },
403        TempoTypedTransaction::Legacy(TxLegacy {
404            to: TxKind::Call(Address::repeat_byte(0xDE)),
405            gas_price: 1234,
406            nonce: 57,
407            gas_limit: 123456,
408            ..Default::default()
409        });
410        "Legacy"
411    )]
412    #[test_case::test_case(
413        TempoTransactionRequest {
414            inner: TransactionRequest {
415                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
416                max_fee_per_gas: Some(1234),
417                max_priority_fee_per_gas: Some(987),
418                nonce: Some(57),
419                gas: Some(123456),
420                ..Default::default()
421            },
422            ..Default::default()
423        },
424        TempoTypedTransaction::Eip1559(TxEip1559 {
425            to: TxKind::Call(Address::repeat_byte(0xDE)),
426            max_fee_per_gas: 1234,
427            max_priority_fee_per_gas: 987,
428            nonce: 57,
429            gas_limit: 123456,
430            chain_id: 1,
431            ..Default::default()
432        });
433        "EIP-1559"
434    )]
435    #[test_case::test_case(
436        TempoTransactionRequest {
437            inner: TransactionRequest {
438                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
439                gas_price: Some(1234),
440                nonce: Some(57),
441                gas: Some(123456),
442                access_list: Some(AccessList(vec![AccessListItem {
443                    address: Address::from([3u8; 20]),
444                    storage_keys: vec![B256::from([4u8; 32])],
445                }])),
446                ..Default::default()
447            },
448            ..Default::default()
449        },
450        TempoTypedTransaction::Eip2930(TxEip2930 {
451            to: TxKind::Call(Address::repeat_byte(0xDE)),
452            gas_price: 1234,
453            nonce: 57,
454            gas_limit: 123456,
455            chain_id: 1,
456            access_list: AccessList(vec![AccessListItem {
457                address: Address::from([3u8; 20]),
458                storage_keys: vec![B256::from([4u8; 32])],
459            }]),
460            ..Default::default()
461        });
462        "EIP-2930"
463    )]
464    #[test_case::test_case(
465        TempoTransactionRequest {
466            inner: TransactionRequest {
467                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
468                max_fee_per_gas: Some(1234),
469                max_priority_fee_per_gas: Some(987),
470                nonce: Some(57),
471                gas: Some(123456),
472                authorization_list: Some(vec![SignedAuthorization::new_unchecked(
473                    Authorization {
474                        chain_id: U256::from(1337),
475                        address: Address::ZERO,
476                        nonce: 0
477                    },
478                    0,
479                    U256::ZERO,
480                    U256::ZERO,
481                )]),
482                ..Default::default()
483            },
484            ..Default::default()
485        },
486        TempoTypedTransaction::Eip7702(TxEip7702 {
487            to: Address::repeat_byte(0xDE),
488            max_fee_per_gas: 1234,
489            max_priority_fee_per_gas: 987,
490            nonce: 57,
491            gas_limit: 123456,
492            chain_id: 1,
493            authorization_list: vec![SignedAuthorization::new_unchecked(
494                Authorization {
495                    chain_id: U256::from(1337),
496                    address: Address::ZERO,
497                    nonce: 0
498                },
499                0,
500                U256::ZERO,
501                U256::ZERO,
502            )],
503            ..Default::default()
504        });
505        "EIP-7702"
506    )]
507    #[test_case::test_case(
508        TempoTransactionRequest {
509            inner: TransactionRequest {
510                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
511                max_fee_per_gas: Some(1234),
512                max_priority_fee_per_gas: Some(987),
513                nonce: Some(57),
514                gas: Some(123456),
515                ..Default::default()
516            },
517            fee_token: Some(Address::repeat_byte(0xFA)),
518            ..Default::default()
519        },
520        TempoTypedTransaction::FeeToken(TxFeeToken {
521            to: TxKind::Call(Address::repeat_byte(0xDE)),
522            max_fee_per_gas: 1234,
523            max_priority_fee_per_gas: 987,
524            nonce: 57,
525            gas_limit: 123456,
526            fee_token: Some(Address::repeat_byte(0xFA)),
527            chain_id: 1,
528            ..Default::default()
529        });
530        "Fee token of call kind"
531    )]
532    #[test_case::test_case(
533        TempoTransactionRequest {
534            inner: TransactionRequest {
535                to: Some(TxKind::Create),
536                max_fee_per_gas: Some(987),
537                max_priority_fee_per_gas: Some(987),
538                nonce: Some(57),
539                gas: Some(123456),
540                ..Default::default()
541            },
542            fee_token: Some(Address::repeat_byte(0xFA)),
543            ..Default::default()
544        },
545        TempoTypedTransaction::FeeToken(TxFeeToken {
546            to: TxKind::Create,
547            max_fee_per_gas: 987,
548            max_priority_fee_per_gas: 987,
549            nonce: 57,
550            gas_limit: 123456,
551            chain_id: 1,
552            fee_token: Some(Address::repeat_byte(0xFA)),
553            ..Default::default()
554        });
555        "Fee token of create kind"
556    )]
557    #[test_case::test_case(
558        TempoTransactionRequest {
559            inner: TransactionRequest {
560                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
561                max_fee_per_gas: Some(987),
562                max_priority_fee_per_gas: Some(987),
563                nonce: Some(57),
564                gas: Some(123456),
565                authorization_list: Some(vec![SignedAuthorization::new_unchecked(
566                    Authorization {
567                        chain_id: U256::from(1337),
568                        address: Address::ZERO,
569                        nonce: 0
570                    },
571                    0,
572                    U256::ZERO,
573                    U256::ZERO,
574                )]),
575                ..Default::default()
576            },
577            fee_token: Some(Address::repeat_byte(0xFA)),
578            ..Default::default()
579        },
580        TempoTypedTransaction::FeeToken(TxFeeToken {
581            to: TxKind::Call(Address::repeat_byte(0xDE)),
582            max_fee_per_gas: 987,
583            max_priority_fee_per_gas: 987,
584            nonce: 57,
585            gas_limit: 123456,
586            chain_id: 1,
587            fee_token: Some(Address::repeat_byte(0xFA)),
588            authorization_list: vec![SignedAuthorization::new_unchecked(
589                Authorization {
590                    chain_id: U256::from(1337),
591                    address: Address::ZERO,
592                    nonce: 0
593                },
594                0,
595                U256::ZERO,
596                U256::ZERO,
597            )],
598            ..Default::default()
599        });
600        "Fee token of call kind with authorization list"
601    )]
602    fn test_transaction_builds_successfully(
603        request: TempoTransactionRequest,
604        expected_transaction: TempoTypedTransaction,
605    ) {
606        let actual_transaction = request
607            .build_unsigned()
608            .expect("required fields should be filled out");
609
610        assert_eq!(actual_transaction, expected_transaction);
611    }
612
613    #[test_case::test_case(
614        TempoTransactionRequest {
615            inner: TransactionRequest {
616                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
617                max_priority_fee_per_gas: Some(987),
618                nonce: Some(57),
619                gas: Some(123456),
620                ..Default::default()
621            },
622            ..Default::default()
623        },
624        "Failed to build transaction: EIP-1559 transaction can't be built due to missing keys: [\"max_fee_per_gas\"]";
625        "EIP-1559 missing max fee"
626    )]
627    #[test_case::test_case(
628        TempoTransactionRequest {
629            inner: TransactionRequest {
630                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
631                max_priority_fee_per_gas: Some(987),
632                nonce: Some(57),
633                gas: Some(123456),
634                ..Default::default()
635            },
636            fee_token: Some(Address::repeat_byte(0xFA)),
637            ..Default::default()
638        },
639        "Failed to build transaction: FeeToken transaction can't be built due to missing keys: [\"max_fee_per_gas\"]";
640        "Fee token missing max fee"
641    )]
642    #[test_case::test_case(
643        TempoTransactionRequest {
644            inner: TransactionRequest {
645                to: Some(TxKind::Create),
646                max_fee_per_gas: Some(987),
647                max_priority_fee_per_gas: Some(987),
648                nonce: Some(57),
649                gas: Some(123456),
650                authorization_list: Some(vec![SignedAuthorization::new_unchecked(
651                    Authorization {
652                        chain_id: U256::from(1337),
653                        address: Address::ZERO,
654                        nonce: 0
655                    },
656                    0,
657                    U256::ZERO,
658                    U256::ZERO,
659                )]),
660                ..Default::default()
661            },
662            fee_token: Some(Address::repeat_byte(0xFA)),
663            ..Default::default()
664        },
665        "Failed to build transaction: FeeToken transaction can't be built due to missing keys: [\"to\"]";
666        "Fee token of create kind with authorization list"
667    )]
668    fn test_transaction_fails_to_build(request: TempoTransactionRequest, expected_error: &str) {
669        let actual_error = request
670            .build_unsigned()
671            .expect_err("some required fields should be missing")
672            .to_string();
673
674        assert_eq!(actual_error, expected_error);
675    }
676}