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 => 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}