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 => self.can_build_aa().then_some(TempoTxType::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::{
319            FEE_PAYER_SIGNATURE_MARKER, KeyAuthorization, PrimitiveSignature,
320            TempoSignedAuthorization,
321        },
322    };
323
324    #[test_case::test_case(
325        TempoTransactionRequest {
326            inner: TransactionRequest {
327                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
328                gas_price: Some(1234),
329                nonce: Some(57),
330                gas: Some(123456),
331                ..Default::default()
332            },
333            ..Default::default()
334        },
335        TempoTypedTransaction::Legacy(TxLegacy {
336            to: TxKind::Call(Address::repeat_byte(0xDE)),
337            gas_price: 1234,
338            nonce: 57,
339            gas_limit: 123456,
340            ..Default::default()
341        });
342        "Legacy"
343    )]
344    #[test_case::test_case(
345        TempoTransactionRequest {
346            inner: TransactionRequest {
347                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
348                max_fee_per_gas: Some(1234),
349                max_priority_fee_per_gas: Some(987),
350                nonce: Some(57),
351                gas: Some(123456),
352                ..Default::default()
353            },
354            ..Default::default()
355        },
356        TempoTypedTransaction::Eip1559(TxEip1559 {
357            to: TxKind::Call(Address::repeat_byte(0xDE)),
358            max_fee_per_gas: 1234,
359            max_priority_fee_per_gas: 987,
360            nonce: 57,
361            gas_limit: 123456,
362            chain_id: 1,
363            ..Default::default()
364        });
365        "EIP-1559"
366    )]
367    #[test_case::test_case(
368        TempoTransactionRequest {
369            inner: TransactionRequest {
370                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
371                gas_price: Some(1234),
372                nonce: Some(57),
373                gas: Some(123456),
374                access_list: Some(AccessList(vec![AccessListItem {
375                    address: Address::from([3u8; 20]),
376                    storage_keys: vec![B256::from([4u8; 32])],
377                }])),
378                ..Default::default()
379            },
380            ..Default::default()
381        },
382        TempoTypedTransaction::Eip2930(TxEip2930 {
383            to: TxKind::Call(Address::repeat_byte(0xDE)),
384            gas_price: 1234,
385            nonce: 57,
386            gas_limit: 123456,
387            chain_id: 1,
388            access_list: AccessList(vec![AccessListItem {
389                address: Address::from([3u8; 20]),
390                storage_keys: vec![B256::from([4u8; 32])],
391            }]),
392            ..Default::default()
393        });
394        "EIP-2930"
395    )]
396    #[test_case::test_case(
397        TempoTransactionRequest {
398            inner: TransactionRequest {
399                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
400                max_fee_per_gas: Some(1234),
401                max_priority_fee_per_gas: Some(987),
402                nonce: Some(57),
403                gas: Some(123456),
404                authorization_list: Some(vec![SignedAuthorization::new_unchecked(
405                    Authorization {
406                        chain_id: U256::from(1337),
407                        address: Address::ZERO,
408                        nonce: 0
409                    },
410                    0,
411                    U256::ZERO,
412                    U256::ZERO,
413                )]),
414                ..Default::default()
415            },
416            ..Default::default()
417        },
418        TempoTypedTransaction::Eip7702(TxEip7702 {
419            to: Address::repeat_byte(0xDE),
420            max_fee_per_gas: 1234,
421            max_priority_fee_per_gas: 987,
422            nonce: 57,
423            gas_limit: 123456,
424            chain_id: 1,
425            authorization_list: vec![SignedAuthorization::new_unchecked(
426                Authorization {
427                    chain_id: U256::from(1337),
428                    address: Address::ZERO,
429                    nonce: 0
430                },
431                0,
432                U256::ZERO,
433                U256::ZERO,
434            )],
435            ..Default::default()
436        });
437        "EIP-7702"
438    )]
439    fn test_transaction_builds_successfully(
440        request: TempoTransactionRequest,
441        expected_transaction: TempoTypedTransaction,
442    ) {
443        let actual_transaction = request
444            .build_unsigned()
445            .expect("required fields should be filled out");
446
447        assert_eq!(actual_transaction, expected_transaction);
448    }
449
450    #[test_case::test_case(
451        TempoTransactionRequest {
452            inner: TransactionRequest {
453                to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
454                max_priority_fee_per_gas: Some(987),
455                nonce: Some(57),
456                gas: Some(123456),
457                ..Default::default()
458            },
459            ..Default::default()
460        },
461        "Failed to build transaction: EIP-1559 transaction can't be built due to missing keys: [\"max_fee_per_gas\"]";
462        "EIP-1559 missing max fee"
463    )]
464    fn test_transaction_fails_to_build(request: TempoTransactionRequest, expected_error: &str) {
465        let actual_error = request
466            .build_unsigned()
467            .expect_err("some required fields should be missing")
468            .to_string();
469
470        assert_eq!(actual_error, expected_error);
471    }
472
473    #[test]
474    fn output_tx_type_empty_request_is_not_aa() {
475        let req = TempoTransactionRequest::default();
476        assert_ne!(req.output_tx_type(), TempoTxType::AA);
477    }
478
479    #[test]
480    fn output_tx_type_tempo_authorization_list_is_aa() {
481        let req = TempoTransactionRequest {
482            tempo_authorization_list: vec![TempoSignedAuthorization::new_unchecked(
483                Authorization {
484                    chain_id: U256::ZERO,
485                    address: Address::ZERO,
486                    nonce: 0,
487                },
488                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::new(
489                    U256::ZERO,
490                    U256::ZERO,
491                    false,
492                ))),
493            )],
494            ..Default::default()
495        };
496        assert_eq!(req.output_tx_type(), TempoTxType::AA);
497    }
498
499    #[test]
500    fn output_tx_type_key_authorization_is_aa() {
501        let req = TempoTransactionRequest {
502            key_authorization: Some(
503                KeyAuthorization::unrestricted(0, SignatureType::Secp256k1, Address::ZERO)
504                    .into_signed(PrimitiveSignature::Secp256k1(Signature::new(
505                        U256::ZERO,
506                        U256::ZERO,
507                        false,
508                    ))),
509            ),
510            ..Default::default()
511        };
512        assert_eq!(req.output_tx_type(), TempoTxType::AA);
513    }
514
515    #[test]
516    fn output_tx_type_key_id_is_aa() {
517        let req = TempoTransactionRequest {
518            key_id: Some(Address::ZERO),
519            ..Default::default()
520        };
521        assert_eq!(req.output_tx_type(), TempoTxType::AA);
522    }
523
524    #[test]
525    fn output_tx_type_fee_payer_signature_is_aa() {
526        let req = TempoTransactionRequest {
527            fee_payer_signature: Some(FEE_PAYER_SIGNATURE_MARKER),
528            ..Default::default()
529        };
530        assert_eq!(req.output_tx_type(), TempoTxType::AA);
531    }
532
533    #[test]
534    fn output_tx_type_validity_window_is_aa() {
535        let req = TempoTransactionRequest {
536            valid_before: Some(core::num::NonZeroU64::new(1000).unwrap()),
537            ..Default::default()
538        };
539        assert_eq!(req.output_tx_type(), TempoTxType::AA);
540
541        let req = TempoTransactionRequest {
542            valid_after: Some(core::num::NonZeroU64::new(500).unwrap()),
543            ..Default::default()
544        };
545        assert_eq!(req.output_tx_type(), TempoTxType::AA);
546    }
547}