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
20pub(crate) type TempoFillers<N> = JoinFill<GasFiller, JoinFill<N, 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<TempoNetwork> for TempoTransactionRequest {
44 fn chain_id(&self) -> Option<ChainId> {
45 self.inner.chain_id()
46 }
47
48 fn set_chain_id(&mut self, chain_id: ChainId) {
49 self.inner.set_chain_id(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 self.inner.set_nonce(nonce)
58 }
59
60 fn take_nonce(&mut self) -> Option<u64> {
61 self.inner.take_nonce()
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 self.inner.kind()
82 }
83
84 fn clear_kind(&mut self) {
85 self.inner.clear_kind()
86 }
87
88 fn set_kind(&mut self, kind: TxKind) {
89 self.inner.set_kind(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 self.inner.set_value(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::FeeToken => self.complete_fee_token(),
143 TempoTxType::AA => self.complete_aa(),
144 TempoTxType::Legacy
145 | TempoTxType::Eip2930
146 | TempoTxType::Eip1559
147 | TempoTxType::Eip7702 => self
148 .inner
149 .complete_type(ty.try_into().expect("tempo tx types checked")),
150 }
151 }
152
153 fn can_submit(&self) -> bool {
154 self.inner.can_submit()
155 }
156
157 fn can_build(&self) -> bool {
158 self.inner.can_build()
159 }
160
161 fn output_tx_type(&self) -> TempoTxType {
162 if !self.calls.is_empty() || self.nonce_key.is_some() {
163 TempoTxType::AA
164 } else if self.fee_token.is_some() {
165 TempoTxType::FeeToken
166 } else {
167 match self.inner.output_tx_type() {
168 TxType::Legacy => TempoTxType::Legacy,
169 TxType::Eip2930 => TempoTxType::Eip2930,
170 TxType::Eip1559 => TempoTxType::Eip1559,
171 TxType::Eip4844 => TempoTxType::Legacy,
173 TxType::Eip7702 => TempoTxType::Eip7702,
174 }
175 }
176 }
177
178 fn output_tx_type_checked(&self) -> Option<TempoTxType> {
179 match self.output_tx_type() {
180 TempoTxType::AA => Some(TempoTxType::AA).filter(|_| self.can_build_aa()),
181 TempoTxType::FeeToken => {
182 Some(TempoTxType::FeeToken).filter(|_| self.can_build_fee_token())
183 }
184 TempoTxType::Legacy
185 | TempoTxType::Eip2930
186 | TempoTxType::Eip1559
187 | TempoTxType::Eip7702 => self.inner.output_tx_type_checked()?.try_into().ok(),
188 }
189 }
190
191 fn prep_for_submission(&mut self) {
192 self.inner.transaction_type = Some(self.output_tx_type() as u8);
193 self.inner.trim_conflicting_keys();
194 self.inner.populate_blob_hashes();
195 }
196
197 fn build_unsigned(self) -> BuildResult<TempoTypedTransaction, TempoNetwork> {
198 match self.output_tx_type() {
199 TempoTxType::AA => match self.complete_aa() {
200 Ok(..) => Ok(self.build_aa().expect("checked by above condition").into()),
201 Err(missing) => Err(TransactionBuilderError::InvalidTransactionRequest(
202 TempoTxType::AA,
203 missing,
204 )
205 .into_unbuilt(self)),
206 },
207 TempoTxType::FeeToken => match self.complete_fee_token() {
208 Ok(..) => Ok(self
209 .build_fee_token()
210 .expect("checked by above condition")
211 .into()),
212 Err(missing) => Err(TransactionBuilderError::InvalidTransactionRequest(
213 TempoTxType::FeeToken,
214 missing,
215 )
216 .into_unbuilt(self)),
217 },
218 _ => {
219 if let Err((tx_type, missing)) = self.inner.missing_keys() {
220 return Err(match tx_type.try_into() {
221 Ok(tx_type) => {
222 TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
223 }
224 Err(err) => TransactionBuilderError::from(err),
225 }
226 .into_unbuilt(self));
227 }
228
229 if let Some(TxType::Eip4844) = self.inner.buildable_type() {
230 return Err(UnbuiltTransactionError {
231 request: self,
232 error: TransactionBuilderError::Custom(Box::new(
233 UnsupportedTransactionType::new(TxType::Eip4844),
234 )),
235 });
236 }
237
238 let inner = self
239 .inner
240 .build_typed_tx()
241 .expect("checked by missing_keys");
242
243 Ok(inner.try_into().expect("checked by above condition"))
244 }
245 }
246 }
247
248 async fn build<W: NetworkWallet<TempoNetwork>>(
249 self,
250 wallet: &W,
251 ) -> Result<TempoTxEnvelope, TransactionBuilderError<TempoNetwork>> {
252 Ok(wallet.sign_request(self).await?)
253 }
254}
255
256impl TempoTransactionRequest {
257 fn can_build_aa(&self) -> bool {
258 (!self.calls.is_empty() || self.inner.to.is_some())
259 && self.inner.nonce.is_some()
260 && self.inner.gas.is_some()
261 && self.inner.max_fee_per_gas.is_some()
262 && self.inner.max_priority_fee_per_gas.is_some()
263 }
264
265 fn can_build_fee_token(&self) -> bool {
266 self.fee_token.is_some()
267 && self.inner.nonce.is_some()
268 && self.inner.gas.is_some()
269 && self.inner.max_fee_per_gas.is_some()
270 && self.inner.max_priority_fee_per_gas.is_some()
271 && (self
272 .inner
273 .authorization_list
274 .as_ref()
275 .map(Vec::is_empty)
276 .unwrap_or(true)
277 || matches!(self.inner.to, Some(TxKind::Call(..))))
278 }
279
280 fn complete_aa(&self) -> Result<(), Vec<&'static str>> {
281 let mut fields = Vec::new();
282
283 if self.calls.is_empty() && self.inner.to.is_none() {
284 fields.push("calls or to");
285 }
286 if self.inner.nonce.is_none() {
287 fields.push("nonce");
288 }
289 if self.inner.gas.is_none() {
290 fields.push("gas");
291 }
292 if self.inner.max_fee_per_gas.is_none() {
293 fields.push("max_fee_per_gas");
294 }
295 if self.inner.max_priority_fee_per_gas.is_none() {
296 fields.push("max_priority_fee_per_gas");
297 }
298
299 if fields.is_empty() {
300 Ok(())
301 } else {
302 Err(fields)
303 }
304 }
305
306 fn complete_fee_token(&self) -> Result<(), Vec<&'static str>> {
307 let mut fields = Vec::new();
308
309 if self.fee_token.is_none() {
310 fields.push("fee_token");
311 }
312 if self.inner.gas.is_none() {
313 fields.push("gas");
314 }
315 if self.inner.max_fee_per_gas.is_none() {
316 fields.push("max_fee_per_gas");
317 }
318 if self.inner.max_priority_fee_per_gas.is_none() {
319 fields.push("max_priority_fee_per_gas");
320 }
321 if !self
322 .inner
323 .authorization_list
324 .as_ref()
325 .map(Vec::is_empty)
326 .unwrap_or(true)
327 && !matches!(self.inner.to, Some(TxKind::Call(..)))
328 {
329 fields.push("to");
330 }
331
332 if fields.is_empty() {
333 Ok(())
334 } else {
335 Err(fields)
336 }
337 }
338}
339
340impl RecommendedFillers for TempoNetwork {
341 type RecommendedFillers = TempoFillers<NonceFiller>;
342
343 fn recommended_fillers() -> Self::RecommendedFillers {
344 Default::default()
345 }
346}
347
348impl NetworkWallet<TempoNetwork> for EthereumWallet {
349 fn default_signer_address(&self) -> Address {
350 NetworkWallet::<Ethereum>::default_signer_address(self)
351 }
352
353 fn has_signer_for(&self, address: &Address) -> bool {
354 NetworkWallet::<Ethereum>::has_signer_for(self, address)
355 }
356
357 fn signer_addresses(&self) -> impl Iterator<Item = Address> {
358 NetworkWallet::<Ethereum>::signer_addresses(self)
359 }
360
361 #[doc(alias = "sign_tx_from")]
362 async fn sign_transaction_from(
363 &self,
364 sender: Address,
365 mut tx: TempoTypedTransaction,
366 ) -> alloy_signer::Result<TempoTxEnvelope> {
367 let signer = self.signer_by_address(sender).ok_or_else(|| {
368 alloy_signer::Error::other(format!("Missing signing credential for {sender}"))
369 })?;
370 let sig = signer.sign_transaction(tx.as_dyn_signable_mut()).await?;
371 Ok(tx.into_envelope(sig))
372 }
373}
374
375impl IntoWallet<TempoNetwork> for PrivateKeySigner {
376 type NetworkWallet = EthereumWallet;
377
378 fn into_wallet(self) -> Self::NetworkWallet {
379 self.into()
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use alloy_consensus::{TxEip1559, TxEip2930, TxEip7702, TxLegacy};
387 use alloy_eips::eip7702::SignedAuthorization;
388 use alloy_primitives::B256;
389 use alloy_rpc_types_eth::{AccessListItem, Authorization, TransactionRequest};
390 use tempo_primitives::TxFeeToken;
391
392 #[test_case::test_case(
393 TempoTransactionRequest {
394 inner: TransactionRequest {
395 to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
396 gas_price: Some(1234),
397 nonce: Some(57),
398 gas: Some(123456),
399 ..Default::default()
400 },
401 ..Default::default()
402 },
403 TempoTypedTransaction::Legacy(TxLegacy {
404 to: TxKind::Call(Address::repeat_byte(0xDE)),
405 gas_price: 1234,
406 nonce: 57,
407 gas_limit: 123456,
408 ..Default::default()
409 });
410 "Legacy"
411 )]
412 #[test_case::test_case(
413 TempoTransactionRequest {
414 inner: TransactionRequest {
415 to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
416 max_fee_per_gas: Some(1234),
417 max_priority_fee_per_gas: Some(987),
418 nonce: Some(57),
419 gas: Some(123456),
420 ..Default::default()
421 },
422 ..Default::default()
423 },
424 TempoTypedTransaction::Eip1559(TxEip1559 {
425 to: TxKind::Call(Address::repeat_byte(0xDE)),
426 max_fee_per_gas: 1234,
427 max_priority_fee_per_gas: 987,
428 nonce: 57,
429 gas_limit: 123456,
430 chain_id: 1,
431 ..Default::default()
432 });
433 "EIP-1559"
434 )]
435 #[test_case::test_case(
436 TempoTransactionRequest {
437 inner: TransactionRequest {
438 to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
439 gas_price: Some(1234),
440 nonce: Some(57),
441 gas: Some(123456),
442 access_list: Some(AccessList(vec![AccessListItem {
443 address: Address::from([3u8; 20]),
444 storage_keys: vec![B256::from([4u8; 32])],
445 }])),
446 ..Default::default()
447 },
448 ..Default::default()
449 },
450 TempoTypedTransaction::Eip2930(TxEip2930 {
451 to: TxKind::Call(Address::repeat_byte(0xDE)),
452 gas_price: 1234,
453 nonce: 57,
454 gas_limit: 123456,
455 chain_id: 1,
456 access_list: AccessList(vec![AccessListItem {
457 address: Address::from([3u8; 20]),
458 storage_keys: vec![B256::from([4u8; 32])],
459 }]),
460 ..Default::default()
461 });
462 "EIP-2930"
463 )]
464 #[test_case::test_case(
465 TempoTransactionRequest {
466 inner: TransactionRequest {
467 to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
468 max_fee_per_gas: Some(1234),
469 max_priority_fee_per_gas: Some(987),
470 nonce: Some(57),
471 gas: Some(123456),
472 authorization_list: Some(vec![SignedAuthorization::new_unchecked(
473 Authorization {
474 chain_id: U256::from(1337),
475 address: Address::ZERO,
476 nonce: 0
477 },
478 0,
479 U256::ZERO,
480 U256::ZERO,
481 )]),
482 ..Default::default()
483 },
484 ..Default::default()
485 },
486 TempoTypedTransaction::Eip7702(TxEip7702 {
487 to: Address::repeat_byte(0xDE),
488 max_fee_per_gas: 1234,
489 max_priority_fee_per_gas: 987,
490 nonce: 57,
491 gas_limit: 123456,
492 chain_id: 1,
493 authorization_list: vec![SignedAuthorization::new_unchecked(
494 Authorization {
495 chain_id: U256::from(1337),
496 address: Address::ZERO,
497 nonce: 0
498 },
499 0,
500 U256::ZERO,
501 U256::ZERO,
502 )],
503 ..Default::default()
504 });
505 "EIP-7702"
506 )]
507 #[test_case::test_case(
508 TempoTransactionRequest {
509 inner: TransactionRequest {
510 to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
511 max_fee_per_gas: Some(1234),
512 max_priority_fee_per_gas: Some(987),
513 nonce: Some(57),
514 gas: Some(123456),
515 ..Default::default()
516 },
517 fee_token: Some(Address::repeat_byte(0xFA)),
518 ..Default::default()
519 },
520 TempoTypedTransaction::FeeToken(TxFeeToken {
521 to: TxKind::Call(Address::repeat_byte(0xDE)),
522 max_fee_per_gas: 1234,
523 max_priority_fee_per_gas: 987,
524 nonce: 57,
525 gas_limit: 123456,
526 fee_token: Some(Address::repeat_byte(0xFA)),
527 chain_id: 1,
528 ..Default::default()
529 });
530 "Fee token of call kind"
531 )]
532 #[test_case::test_case(
533 TempoTransactionRequest {
534 inner: TransactionRequest {
535 to: Some(TxKind::Create),
536 max_fee_per_gas: Some(987),
537 max_priority_fee_per_gas: Some(987),
538 nonce: Some(57),
539 gas: Some(123456),
540 ..Default::default()
541 },
542 fee_token: Some(Address::repeat_byte(0xFA)),
543 ..Default::default()
544 },
545 TempoTypedTransaction::FeeToken(TxFeeToken {
546 to: TxKind::Create,
547 max_fee_per_gas: 987,
548 max_priority_fee_per_gas: 987,
549 nonce: 57,
550 gas_limit: 123456,
551 chain_id: 1,
552 fee_token: Some(Address::repeat_byte(0xFA)),
553 ..Default::default()
554 });
555 "Fee token of create kind"
556 )]
557 #[test_case::test_case(
558 TempoTransactionRequest {
559 inner: TransactionRequest {
560 to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
561 max_fee_per_gas: Some(987),
562 max_priority_fee_per_gas: Some(987),
563 nonce: Some(57),
564 gas: Some(123456),
565 authorization_list: Some(vec![SignedAuthorization::new_unchecked(
566 Authorization {
567 chain_id: U256::from(1337),
568 address: Address::ZERO,
569 nonce: 0
570 },
571 0,
572 U256::ZERO,
573 U256::ZERO,
574 )]),
575 ..Default::default()
576 },
577 fee_token: Some(Address::repeat_byte(0xFA)),
578 ..Default::default()
579 },
580 TempoTypedTransaction::FeeToken(TxFeeToken {
581 to: TxKind::Call(Address::repeat_byte(0xDE)),
582 max_fee_per_gas: 987,
583 max_priority_fee_per_gas: 987,
584 nonce: 57,
585 gas_limit: 123456,
586 chain_id: 1,
587 fee_token: Some(Address::repeat_byte(0xFA)),
588 authorization_list: vec![SignedAuthorization::new_unchecked(
589 Authorization {
590 chain_id: U256::from(1337),
591 address: Address::ZERO,
592 nonce: 0
593 },
594 0,
595 U256::ZERO,
596 U256::ZERO,
597 )],
598 ..Default::default()
599 });
600 "Fee token of call kind with authorization list"
601 )]
602 fn test_transaction_builds_successfully(
603 request: TempoTransactionRequest,
604 expected_transaction: TempoTypedTransaction,
605 ) {
606 let actual_transaction = request
607 .build_unsigned()
608 .expect("required fields should be filled out");
609
610 assert_eq!(actual_transaction, expected_transaction);
611 }
612
613 #[test_case::test_case(
614 TempoTransactionRequest {
615 inner: TransactionRequest {
616 to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
617 max_priority_fee_per_gas: Some(987),
618 nonce: Some(57),
619 gas: Some(123456),
620 ..Default::default()
621 },
622 ..Default::default()
623 },
624 "Failed to build transaction: EIP-1559 transaction can't be built due to missing keys: [\"max_fee_per_gas\"]";
625 "EIP-1559 missing max fee"
626 )]
627 #[test_case::test_case(
628 TempoTransactionRequest {
629 inner: TransactionRequest {
630 to: Some(TxKind::Call(Address::repeat_byte(0xDE))),
631 max_priority_fee_per_gas: Some(987),
632 nonce: Some(57),
633 gas: Some(123456),
634 ..Default::default()
635 },
636 fee_token: Some(Address::repeat_byte(0xFA)),
637 ..Default::default()
638 },
639 "Failed to build transaction: FeeToken transaction can't be built due to missing keys: [\"max_fee_per_gas\"]";
640 "Fee token missing max fee"
641 )]
642 #[test_case::test_case(
643 TempoTransactionRequest {
644 inner: TransactionRequest {
645 to: Some(TxKind::Create),
646 max_fee_per_gas: Some(987),
647 max_priority_fee_per_gas: Some(987),
648 nonce: Some(57),
649 gas: Some(123456),
650 authorization_list: Some(vec![SignedAuthorization::new_unchecked(
651 Authorization {
652 chain_id: U256::from(1337),
653 address: Address::ZERO,
654 nonce: 0
655 },
656 0,
657 U256::ZERO,
658 U256::ZERO,
659 )]),
660 ..Default::default()
661 },
662 fee_token: Some(Address::repeat_byte(0xFA)),
663 ..Default::default()
664 },
665 "Failed to build transaction: FeeToken transaction can't be built due to missing keys: [\"to\"]";
666 "Fee token of create kind with authorization list"
667 )]
668 fn test_transaction_fails_to_build(request: TempoTransactionRequest, expected_error: &str) {
669 let actual_error = request
670 .build_unsigned()
671 .expect_err("some required fields should be missing")
672 .to_string();
673
674 assert_eq!(actual_error, expected_error);
675 }
676}