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
20pub type TempoFillers<N> = JoinFill<N, JoinFill<GasFiller, ChainIdFiller>>;
24
25#[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 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}