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