Skip to main content

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, NetworkTransactionBuilder,
8    NetworkWallet, TransactionBuilder, 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 type TempoFillers<N> = JoinFill<N, JoinFill<GasFiller, 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 for TempoTransactionRequest {
44    fn chain_id(&self) -> Option<ChainId> {
45        TransactionBuilder::chain_id(&self.inner)
46    }
47
48    fn set_chain_id(&mut self, chain_id: ChainId) {
49        TransactionBuilder::set_chain_id(&mut self.inner, 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        TransactionBuilder::set_nonce(&mut self.inner, nonce)
58    }
59
60    fn take_nonce(&mut self) -> Option<u64> {
61        TransactionBuilder::take_nonce(&mut self.inner)
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        TransactionBuilder::kind(&self.inner)
82    }
83
84    fn clear_kind(&mut self) {
85        TransactionBuilder::clear_kind(&mut self.inner)
86    }
87
88    fn set_kind(&mut self, kind: TxKind) {
89        TransactionBuilder::set_kind(&mut self.inner, 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        TransactionBuilder::set_value(&mut self.inner, 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
141impl NetworkTransactionBuilder<TempoNetwork> for TempoTransactionRequest {
142    fn complete_type(&self, ty: TempoTxType) -> Result<(), Vec<&'static str>> {
143        match ty {
144            TempoTxType::AA => self.complete_aa(),
145            TempoTxType::Legacy
146            | TempoTxType::Eip2930
147            | TempoTxType::Eip1559
148            | TempoTxType::Eip7702 => NetworkTransactionBuilder::<Ethereum>::complete_type(
149                &self.inner,
150                ty.try_into().expect("tempo tx types checked"),
151            ),
152        }
153    }
154
155    fn can_submit(&self) -> bool {
156        NetworkTransactionBuilder::<Ethereum>::can_submit(&self.inner)
157    }
158
159    fn can_build(&self) -> bool {
160        NetworkTransactionBuilder::<Ethereum>::can_build(&self.inner) || self.can_build_aa()
161    }
162
163    fn output_tx_type(&self) -> TempoTxType {
164        if !self.calls.is_empty()
165            || self.nonce_key.is_some()
166            || self.fee_token.is_some()
167            || !self.tempo_authorization_list.is_empty()
168            || self.key_authorization.is_some()
169            || self.key_id.is_some()
170            || self.key_type.is_some()
171            || self.key_data.is_some()
172            || self.valid_before.is_some()
173            || self.valid_after.is_some()
174            || self.fee_payer_signature.is_some()
175        {
176            TempoTxType::AA
177        } else {
178            match NetworkTransactionBuilder::<Ethereum>::output_tx_type(&self.inner) {
179                TxType::Legacy => TempoTxType::Legacy,
180                TxType::Eip2930 => TempoTxType::Eip2930,
181                TxType::Eip1559 => TempoTxType::Eip1559,
182                // EIP-4844 transactions are not supported on Tempo
183                TxType::Eip4844 => TempoTxType::Legacy,
184                TxType::Eip7702 => TempoTxType::Eip7702,
185            }
186        }
187    }
188
189    fn output_tx_type_checked(&self) -> Option<TempoTxType> {
190        match self.output_tx_type() {
191            TempoTxType::AA => Some(TempoTxType::AA).filter(|_| self.can_build_aa()),
192            TempoTxType::Legacy
193            | TempoTxType::Eip2930
194            | TempoTxType::Eip1559
195            | TempoTxType::Eip7702 => {
196                NetworkTransactionBuilder::<Ethereum>::output_tx_type_checked(&self.inner)?
197                    .try_into()
198                    .ok()
199            }
200        }
201    }
202
203    fn prep_for_submission(&mut self) {
204        self.inner.transaction_type = Some(self.output_tx_type() as u8);
205        self.inner.trim_conflicting_keys();
206        self.inner.populate_blob_hashes();
207    }
208
209    fn build_unsigned(self) -> BuildResult<TempoTypedTransaction, TempoNetwork> {
210        match self.output_tx_type() {
211            TempoTxType::AA => match self.complete_aa() {
212                Ok(..) => Ok(self.build_aa().expect("checked by above condition").into()),
213                Err(missing) => Err(TransactionBuilderError::InvalidTransactionRequest(
214                    TempoTxType::AA,
215                    missing,
216                )
217                .into_unbuilt(self)),
218            },
219            _ => {
220                if let Err((tx_type, missing)) = self.inner.missing_keys() {
221                    return Err(match tx_type.try_into() {
222                        Ok(tx_type) => {
223                            TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
224                        }
225                        Err(err) => TransactionBuilderError::from(err),
226                    }
227                    .into_unbuilt(self));
228                }
229
230                if let Some(TxType::Eip4844) = self.inner.buildable_type() {
231                    return Err(UnbuiltTransactionError {
232                        request: self,
233                        error: TransactionBuilderError::Custom(Box::new(
234                            UnsupportedTransactionType::new(TxType::Eip4844),
235                        )),
236                    });
237                }
238
239                let inner = self
240                    .inner
241                    .build_typed_tx()
242                    .expect("checked by missing_keys");
243
244                Ok(inner.try_into().expect("checked by above condition"))
245            }
246        }
247    }
248
249    async fn build<W: NetworkWallet<TempoNetwork>>(
250        self,
251        wallet: &W,
252    ) -> Result<TempoTxEnvelope, TransactionBuilderError<TempoNetwork>> {
253        Ok(wallet.sign_request(self).await?)
254    }
255}
256
257impl TempoTransactionRequest {
258    fn can_build_aa(&self) -> bool {
259        (!self.calls.is_empty() || self.inner.to.is_some())
260            && self.inner.nonce.is_some()
261            && self.inner.gas.is_some()
262            && self.inner.max_fee_per_gas.is_some()
263            && self.inner.max_priority_fee_per_gas.is_some()
264    }
265
266    fn complete_aa(&self) -> Result<(), Vec<&'static str>> {
267        let mut fields = Vec::new();
268
269        if self.calls.is_empty() && self.inner.to.is_none() {
270            fields.push("calls or to");
271        }
272        if self.inner.nonce.is_none() {
273            fields.push("nonce");
274        }
275        if self.inner.gas.is_none() {
276            fields.push("gas");
277        }
278        if self.inner.max_fee_per_gas.is_none() {
279            fields.push("max_fee_per_gas");
280        }
281        if self.inner.max_priority_fee_per_gas.is_none() {
282            fields.push("max_priority_fee_per_gas");
283        }
284
285        if fields.is_empty() {
286            Ok(())
287        } else {
288            Err(fields)
289        }
290    }
291}
292
293impl RecommendedFillers for TempoNetwork {
294    type RecommendedFillers = TempoFillers<NonceFiller>;
295
296    fn recommended_fillers() -> Self::RecommendedFillers {
297        Default::default()
298    }
299}
300
301impl IntoWallet<TempoNetwork> for PrivateKeySigner {
302    type NetworkWallet = EthereumWallet;
303
304    fn into_wallet(self) -> Self::NetworkWallet {
305        self.into()
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use alloy_consensus::{TxEip1559, TxEip2930, TxEip7702, TxLegacy};
313    use alloy_eips::eip7702::SignedAuthorization;
314    use alloy_primitives::{B256, Signature};
315    use alloy_rpc_types_eth::{AccessListItem, Authorization, TransactionRequest};
316    use tempo_primitives::{
317        SignatureType, TempoSignature,
318        transaction::{KeyAuthorization, PrimitiveSignature, TempoSignedAuthorization},
319    };
320
321    #[test_case::test_case(
322        TempoTransactionRequest {
323            inner: TransactionRequest {
324                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
325                gas_price: Some(1234),
326                nonce: Some(57),
327                gas: Some(123456),
328                ..Default::default()
329            },
330            ..Default::default()
331        },
332        TempoTypedTransaction::Legacy(TxLegacy {
333            to: TxKind::Call(Address::repeat_byte(0xDE)),
334            gas_price: 1234,
335            nonce: 57,
336            gas_limit: 123456,
337            ..Default::default()
338        });
339        "Legacy"
340    )]
341    #[test_case::test_case(
342        TempoTransactionRequest {
343            inner: TransactionRequest {
344                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
345                max_fee_per_gas: Some(1234),
346                max_priority_fee_per_gas: Some(987),
347                nonce: Some(57),
348                gas: Some(123456),
349                ..Default::default()
350            },
351            ..Default::default()
352        },
353        TempoTypedTransaction::Eip1559(TxEip1559 {
354            to: TxKind::Call(Address::repeat_byte(0xDE)),
355            max_fee_per_gas: 1234,
356            max_priority_fee_per_gas: 987,
357            nonce: 57,
358            gas_limit: 123456,
359            chain_id: 1,
360            ..Default::default()
361        });
362        "EIP-1559"
363    )]
364    #[test_case::test_case(
365        TempoTransactionRequest {
366            inner: TransactionRequest {
367                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
368                gas_price: Some(1234),
369                nonce: Some(57),
370                gas: Some(123456),
371                access_list: Some(AccessList(vec![AccessListItem {
372                    address: Address::from([3u8; 20]),
373                    storage_keys: vec![B256::from([4u8; 32])],
374                }])),
375                ..Default::default()
376            },
377            ..Default::default()
378        },
379        TempoTypedTransaction::Eip2930(TxEip2930 {
380            to: TxKind::Call(Address::repeat_byte(0xDE)),
381            gas_price: 1234,
382            nonce: 57,
383            gas_limit: 123456,
384            chain_id: 1,
385            access_list: AccessList(vec![AccessListItem {
386                address: Address::from([3u8; 20]),
387                storage_keys: vec![B256::from([4u8; 32])],
388            }]),
389            ..Default::default()
390        });
391        "EIP-2930"
392    )]
393    #[test_case::test_case(
394        TempoTransactionRequest {
395            inner: TransactionRequest {
396                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
397                max_fee_per_gas: Some(1234),
398                max_priority_fee_per_gas: Some(987),
399                nonce: Some(57),
400                gas: Some(123456),
401                authorization_list: Some(vec![SignedAuthorization::new_unchecked(
402                    Authorization {
403                        chain_id: U256::from(1337),
404                        address: Address::ZERO,
405                        nonce: 0
406                    },
407                    0,
408                    U256::ZERO,
409                    U256::ZERO,
410                )]),
411                ..Default::default()
412            },
413            ..Default::default()
414        },
415        TempoTypedTransaction::Eip7702(TxEip7702 {
416            to: Address::repeat_byte(0xDE),
417            max_fee_per_gas: 1234,
418            max_priority_fee_per_gas: 987,
419            nonce: 57,
420            gas_limit: 123456,
421            chain_id: 1,
422            authorization_list: vec![SignedAuthorization::new_unchecked(
423                Authorization {
424                    chain_id: U256::from(1337),
425                    address: Address::ZERO,
426                    nonce: 0
427                },
428                0,
429                U256::ZERO,
430                U256::ZERO,
431            )],
432            ..Default::default()
433        });
434        "EIP-7702"
435    )]
436    fn test_transaction_builds_successfully(
437        request: TempoTransactionRequest,
438        expected_transaction: TempoTypedTransaction,
439    ) {
440        let actual_transaction = request
441            .build_unsigned()
442            .expect("required fields should be filled out");
443
444        assert_eq!(actual_transaction, expected_transaction);
445    }
446
447    #[test_case::test_case(
448        TempoTransactionRequest {
449            inner: TransactionRequest {
450                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
451                max_priority_fee_per_gas: Some(987),
452                nonce: Some(57),
453                gas: Some(123456),
454                ..Default::default()
455            },
456            ..Default::default()
457        },
458        "Failed to build transaction: EIP-1559 transaction can't be built due to missing keys: [\"max_fee_per_gas\"]";
459        "EIP-1559 missing max fee"
460    )]
461    fn test_transaction_fails_to_build(request: TempoTransactionRequest, expected_error: &str) {
462        let actual_error = request
463            .build_unsigned()
464            .expect_err("some required fields should be missing")
465            .to_string();
466
467        assert_eq!(actual_error, expected_error);
468    }
469
470    #[test]
471    fn output_tx_type_empty_request_is_not_aa() {
472        let req = TempoTransactionRequest::default();
473        assert_ne!(req.output_tx_type(), TempoTxType::AA);
474    }
475
476    #[test]
477    fn output_tx_type_tempo_authorization_list_is_aa() {
478        let req = TempoTransactionRequest {
479            tempo_authorization_list: vec![TempoSignedAuthorization::new_unchecked(
480                Authorization {
481                    chain_id: U256::ZERO,
482                    address: Address::ZERO,
483                    nonce: 0,
484                },
485                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::new(
486                    U256::ZERO,
487                    U256::ZERO,
488                    false,
489                ))),
490            )],
491            ..Default::default()
492        };
493        assert_eq!(req.output_tx_type(), TempoTxType::AA);
494    }
495
496    #[test]
497    fn output_tx_type_key_authorization_is_aa() {
498        let req = TempoTransactionRequest {
499            key_authorization: Some(
500                KeyAuthorization::unrestricted(0, SignatureType::Secp256k1, Address::ZERO)
501                    .into_signed(PrimitiveSignature::Secp256k1(Signature::new(
502                        U256::ZERO,
503                        U256::ZERO,
504                        false,
505                    ))),
506            ),
507            ..Default::default()
508        };
509        assert_eq!(req.output_tx_type(), TempoTxType::AA);
510    }
511
512    #[test]
513    fn output_tx_type_key_id_is_aa() {
514        let req = TempoTransactionRequest {
515            key_id: Some(Address::ZERO),
516            ..Default::default()
517        };
518        assert_eq!(req.output_tx_type(), TempoTxType::AA);
519    }
520
521    #[test]
522    fn output_tx_type_fee_payer_signature_is_aa() {
523        let req = TempoTransactionRequest {
524            fee_payer_signature: Some(Signature::new(U256::ZERO, U256::ZERO, false)),
525            ..Default::default()
526        };
527        assert_eq!(req.output_tx_type(), TempoTxType::AA);
528    }
529
530    #[test]
531    fn output_tx_type_validity_window_is_aa() {
532        let req = TempoTransactionRequest {
533            valid_before: Some(core::num::NonZeroU64::new(1000).unwrap()),
534            ..Default::default()
535        };
536        assert_eq!(req.output_tx_type(), TempoTxType::AA);
537
538        let req = TempoTransactionRequest {
539            valid_after: Some(core::num::NonZeroU64::new(500).unwrap()),
540            ..Default::default()
541        };
542        assert_eq!(req.output_tx_type(), TempoTxType::AA);
543    }
544}