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