tempo_alloy/rpc/
request.rs

1use alloy_consensus::{Signed, TxEip1559, TxEip2930, TxEip7702, TxLegacy, error::ValueError};
2use alloy_contract::{CallBuilder, CallDecoder};
3use alloy_eips::Typed2718;
4use alloy_primitives::{Address, Bytes, U256};
5use alloy_provider::Provider;
6use alloy_rpc_types_eth::{TransactionRequest, TransactionTrait};
7use serde::{Deserialize, Serialize};
8use tempo_primitives::{
9    AASigned, SignatureType, TempoTransaction, TempoTxEnvelope, TxFeeToken,
10    transaction::{Call, TempoSignedAuthorization, TempoTypedTransaction},
11};
12
13use crate::TempoNetwork;
14
15/// An Ethereum [`TransactionRequest`] with an optional `fee_token`.
16#[derive(
17    Clone,
18    Debug,
19    Default,
20    PartialEq,
21    Eq,
22    Hash,
23    Serialize,
24    Deserialize,
25    derive_more::Deref,
26    derive_more::DerefMut,
27)]
28#[serde(rename_all = "camelCase")]
29pub struct TempoTransactionRequest {
30    /// Inner [`TransactionRequest`]
31    #[serde(flatten)]
32    #[deref]
33    #[deref_mut]
34    pub inner: TransactionRequest,
35
36    /// Optional fee token preference
37    pub fee_token: Option<Address>,
38
39    /// Optional nonce key for a 2D [`TempoTransaction`].
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub nonce_key: Option<U256>,
42
43    /// Optional calls array, for Tempo transactions.
44    #[serde(default)]
45    pub calls: Vec<Call>,
46
47    /// Optional key type for gas estimation of Tempo transactions.
48    /// Specifies the signature verification algorithm to calculate accurate gas costs.
49    pub key_type: Option<SignatureType>,
50
51    /// Optional key-specific data for gas estimation (e.g., webauthn authenticator data).
52    /// Required when key_type is WebAuthn to calculate calldata gas costs.
53    pub key_data: Option<Bytes>,
54
55    /// Optional authorization list for Tempo transactions (supports multiple signature types)
56    #[serde(
57        default,
58        skip_serializing_if = "Vec::is_empty",
59        rename = "aaAuthorizationList"
60    )]
61    pub tempo_authorization_list: Vec<TempoSignedAuthorization>,
62}
63
64impl TempoTransactionRequest {
65    /// Builder-pattern method for setting the fee token.
66    pub fn with_fee_token(mut self, fee_token: Address) -> Self {
67        self.fee_token = Some(fee_token);
68        self
69    }
70
71    /// Set the 2D nonce key for the [`TempoTransaction`] transaction.
72    pub fn set_nonce_key(&mut self, nonce_key: U256) {
73        self.nonce_key = Some(nonce_key);
74    }
75
76    /// Builder-pattern method for setting a 2D nonce key for a [`TempoTransaction`].
77    pub fn with_nonce_key(mut self, nonce_key: U256) -> Self {
78        self.nonce_key = Some(nonce_key);
79        self
80    }
81
82    pub fn build_fee_token(self) -> Result<TxFeeToken, ValueError<Self>> {
83        let Some(to) = self.inner.to else {
84            return Err(ValueError::new(
85                self,
86                "Missing 'to' field for FeeToken transaction.",
87            ));
88        };
89        let Some(nonce) = self.inner.nonce else {
90            return Err(ValueError::new(
91                self,
92                "Missing 'nonce' field for FeeToken transaction.",
93            ));
94        };
95        let Some(gas_limit) = self.inner.gas else {
96            return Err(ValueError::new(
97                self,
98                "Missing 'gas_limit' field for FeeToken transaction.",
99            ));
100        };
101        let Some(max_fee_per_gas) = self.inner.max_fee_per_gas else {
102            return Err(ValueError::new(
103                self,
104                "Missing 'max_fee_per_gas' field for FeeToken transaction.",
105            ));
106        };
107        let Some(max_priority_fee_per_gas) = self.inner.max_priority_fee_per_gas else {
108            return Err(ValueError::new(
109                self,
110                "Missing 'max_priority_fee_per_gas' field for FeeToken transaction.",
111            ));
112        };
113
114        Ok(TxFeeToken {
115            chain_id: self.inner.chain_id.unwrap_or(1),
116            nonce,
117            gas_limit,
118            max_fee_per_gas,
119            max_priority_fee_per_gas,
120            to,
121            fee_token: self.fee_token,
122            value: self.inner.value.unwrap_or_default(),
123            input: self.inner.input.into_input().unwrap_or_default(),
124            access_list: self.inner.access_list.unwrap_or_default(),
125            authorization_list: self.inner.authorization_list.unwrap_or_default(),
126            fee_payer_signature: None,
127        })
128    }
129
130    /// Attempts to build a [`TempoTransaction`] with the configured fields.
131    pub fn build_aa(self) -> Result<TempoTransaction, ValueError<Self>> {
132        if self.calls.is_empty() && self.inner.to.is_none() {
133            return Err(ValueError::new(
134                self,
135                "Missing 'calls' or 'to' field for Tempo transaction.",
136            ));
137        }
138
139        let Some(nonce) = self.inner.nonce else {
140            return Err(ValueError::new(
141                self,
142                "Missing 'nonce' field for Tempo transaction.",
143            ));
144        };
145        let Some(gas_limit) = self.inner.gas else {
146            return Err(ValueError::new(
147                self,
148                "Missing 'gas_limit' field for Tempo transaction.",
149            ));
150        };
151        let Some(max_fee_per_gas) = self.inner.max_fee_per_gas else {
152            return Err(ValueError::new(
153                self,
154                "Missing 'max_fee_per_gas' field for Tempo transaction.",
155            ));
156        };
157        let Some(max_priority_fee_per_gas) = self.inner.max_priority_fee_per_gas else {
158            return Err(ValueError::new(
159                self,
160                "Missing 'max_priority_fee_per_gas' field for Tempo transaction.",
161            ));
162        };
163
164        let mut calls = self.calls;
165        if let Some(to) = self.inner.to {
166            calls.push(Call {
167                to,
168                value: self.inner.value.unwrap_or_default(),
169                input: self.inner.input.into_input().unwrap_or_default(),
170            });
171        }
172
173        Ok(TempoTransaction {
174            // TODO: use tempo mainnet chainid once assigned
175            chain_id: self.inner.chain_id.unwrap_or(1),
176            nonce,
177            fee_payer_signature: None,
178            valid_before: None,
179            valid_after: None,
180            gas_limit,
181            max_fee_per_gas,
182            max_priority_fee_per_gas,
183            fee_token: self.fee_token,
184            access_list: self.inner.access_list.unwrap_or_default(),
185            calls,
186            tempo_authorization_list: self.tempo_authorization_list,
187            nonce_key: self.nonce_key.unwrap_or_default(),
188            key_authorization: None,
189        })
190    }
191}
192
193impl AsRef<TransactionRequest> for TempoTransactionRequest {
194    fn as_ref(&self) -> &TransactionRequest {
195        &self.inner
196    }
197}
198
199impl AsMut<TransactionRequest> for TempoTransactionRequest {
200    fn as_mut(&mut self) -> &mut TransactionRequest {
201        &mut self.inner
202    }
203}
204
205impl From<TransactionRequest> for TempoTransactionRequest {
206    fn from(value: TransactionRequest) -> Self {
207        Self {
208            inner: value,
209            fee_token: None,
210            ..Default::default()
211        }
212    }
213}
214
215impl From<TempoTransactionRequest> for TransactionRequest {
216    fn from(value: TempoTransactionRequest) -> Self {
217        value.inner
218    }
219}
220
221impl From<TempoTxEnvelope> for TempoTransactionRequest {
222    fn from(value: TempoTxEnvelope) -> Self {
223        match value {
224            TempoTxEnvelope::Legacy(tx) => tx.into(),
225            TempoTxEnvelope::Eip2930(tx) => tx.into(),
226            TempoTxEnvelope::Eip1559(tx) => tx.into(),
227            TempoTxEnvelope::Eip7702(tx) => tx.into(),
228            TempoTxEnvelope::FeeToken(tx) => tx.into(),
229            TempoTxEnvelope::AA(tx) => tx.into(),
230        }
231    }
232}
233
234pub trait FeeToken {
235    fn fee_token(&self) -> Option<Address>;
236}
237
238impl FeeToken for TxFeeToken {
239    fn fee_token(&self) -> Option<Address> {
240        self.fee_token
241    }
242}
243
244impl FeeToken for TempoTransaction {
245    fn fee_token(&self) -> Option<Address> {
246        self.fee_token
247    }
248}
249
250impl FeeToken for TxEip7702 {
251    fn fee_token(&self) -> Option<Address> {
252        None
253    }
254}
255
256impl FeeToken for TxEip1559 {
257    fn fee_token(&self) -> Option<Address> {
258        None
259    }
260}
261
262impl FeeToken for TxEip2930 {
263    fn fee_token(&self) -> Option<Address> {
264        None
265    }
266}
267
268impl FeeToken for TxLegacy {
269    fn fee_token(&self) -> Option<Address> {
270        None
271    }
272}
273
274impl<T: TransactionTrait + FeeToken> From<Signed<T>> for TempoTransactionRequest {
275    fn from(value: Signed<T>) -> Self {
276        Self {
277            fee_token: value.tx().fee_token(),
278            inner: TransactionRequest::from_transaction(value),
279            ..Default::default()
280        }
281    }
282}
283
284impl From<TempoTransaction> for TempoTransactionRequest {
285    fn from(tx: TempoTransaction) -> Self {
286        Self {
287            fee_token: tx.fee_token,
288            inner: TransactionRequest {
289                from: None,
290                to: Some(tx.kind()),
291                gas: Some(tx.gas_limit()),
292                gas_price: tx.gas_price(),
293                max_fee_per_gas: Some(tx.max_fee_per_gas()),
294                max_priority_fee_per_gas: tx.max_priority_fee_per_gas(),
295                value: Some(tx.value()),
296                input: alloy_rpc_types_eth::TransactionInput::new(tx.input().clone()),
297                nonce: Some(tx.nonce()),
298                chain_id: tx.chain_id(),
299                access_list: tx.access_list().cloned(),
300                max_fee_per_blob_gas: None,
301                blob_versioned_hashes: None,
302                sidecar: None,
303                authorization_list: None,
304                transaction_type: Some(tx.ty()),
305            },
306            calls: tx.calls,
307            tempo_authorization_list: tx.tempo_authorization_list,
308            key_type: None,
309            key_data: None,
310            nonce_key: Some(tx.nonce_key),
311        }
312    }
313}
314
315impl From<AASigned> for TempoTransactionRequest {
316    fn from(value: AASigned) -> Self {
317        value.into_parts().0.into()
318    }
319}
320
321impl From<TempoTypedTransaction> for TempoTransactionRequest {
322    fn from(value: TempoTypedTransaction) -> Self {
323        match value {
324            TempoTypedTransaction::Legacy(tx) => Self {
325                inner: tx.into(),
326                fee_token: None,
327                ..Default::default()
328            },
329            TempoTypedTransaction::Eip2930(tx) => Self {
330                inner: tx.into(),
331                fee_token: None,
332                ..Default::default()
333            },
334            TempoTypedTransaction::Eip1559(tx) => Self {
335                inner: tx.into(),
336                fee_token: None,
337                ..Default::default()
338            },
339            TempoTypedTransaction::Eip7702(tx) => Self {
340                inner: tx.into(),
341                fee_token: None,
342                ..Default::default()
343            },
344            TempoTypedTransaction::FeeToken(tx) => Self {
345                fee_token: tx.fee_token,
346                inner: TransactionRequest::from_transaction(tx),
347                ..Default::default()
348            },
349            TempoTypedTransaction::AA(tx) => tx.into(),
350        }
351    }
352}
353
354/// Extension trait for [`CallBuilder`]
355pub trait TempoCallBuilderExt {
356    /// Sets the `fee_token` field in the [`TempoTransaction`] or [`TxFeeToken`] transaction to the provided value
357    fn fee_token(self, fee_token: Address) -> Self;
358
359    /// Sets the `nonce_key` field in the [`TempoTransaction`] transaction to the provided value
360    fn nonce_key(self, nonce_key: U256) -> Self;
361}
362
363impl<P: Provider<TempoNetwork>, D: CallDecoder> TempoCallBuilderExt
364    for CallBuilder<P, D, TempoNetwork>
365{
366    fn fee_token(self, fee_token: Address) -> Self {
367        self.map(|request| request.with_fee_token(fee_token))
368    }
369
370    fn nonce_key(self, nonce_key: U256) -> Self {
371        self.map(|request| request.with_nonce_key(nonce_key))
372    }
373}