tempo_primitives/transaction/
tt_signed.rs1use super::{
2 tempo_transaction::{TEMPO_TX_TYPE_ID, TempoTransaction},
3 tt_signature::TempoSignature,
4};
5use alloy_consensus::{Transaction, transaction::TxHashRef};
6use alloy_eips::{
7 Decodable2718, Encodable2718, Typed2718,
8 eip2718::{Eip2718Error, Eip2718Result},
9 eip2930::AccessList,
10 eip7702::SignedAuthorization,
11};
12use alloy_primitives::{B256, Bytes, TxKind, U256};
13use alloy_rlp::{BufMut, Decodable, Encodable};
14use core::{
15 fmt::Debug,
16 hash::{Hash, Hasher},
17 mem,
18};
19use reth_primitives_traits::InMemorySize;
20use std::sync::OnceLock;
21
22#[derive(Clone, Debug)]
27pub struct AASigned {
28 tx: TempoTransaction,
30 signature: TempoSignature,
32 #[doc(alias = "tx_hash", alias = "transaction_hash")]
34 hash: OnceLock<B256>,
35}
36
37impl AASigned {
38 pub fn new_unchecked(tx: TempoTransaction, signature: TempoSignature, hash: B256) -> Self {
41 let value = OnceLock::new();
42 #[allow(clippy::useless_conversion)]
43 value.get_or_init(|| hash.into());
44 Self {
45 tx,
46 signature,
47 hash: value,
48 }
49 }
50
51 pub const fn new_unhashed(tx: TempoTransaction, signature: TempoSignature) -> Self {
54 Self {
55 tx,
56 signature,
57 hash: OnceLock::new(),
58 }
59 }
60
61 #[doc(alias = "transaction")]
63 pub const fn tx(&self) -> &TempoTransaction {
64 &self.tx
65 }
66
67 pub const fn tx_mut(&mut self) -> &mut TempoTransaction {
69 &mut self.tx
70 }
71
72 pub const fn signature(&self) -> &TempoSignature {
74 &self.signature
75 }
76
77 pub fn strip_signature(self) -> TempoTransaction {
79 self.tx
80 }
81
82 #[doc(alias = "tx_hash", alias = "transaction_hash")]
84 pub fn hash(&self) -> &B256 {
85 self.hash.get_or_init(|| self.compute_hash())
86 }
87
88 fn compute_hash(&self) -> B256 {
90 let mut buf = Vec::new();
91 self.eip2718_encode(&mut buf);
92 alloy_primitives::keccak256(&buf)
93 }
94
95 pub fn signature_hash(&self) -> B256 {
97 self.tx.signature_hash()
98 }
99
100 #[inline]
103 fn rlp_header(&self) -> alloy_rlp::Header {
104 let payload_length = self.tx.rlp_encoded_fields_length_default() + self.signature.length();
105 alloy_rlp::Header {
106 list: true,
107 payload_length,
108 }
109 }
110
111 pub fn rlp_encode(&self, out: &mut dyn BufMut) {
113 self.rlp_header().encode(out);
115
116 self.tx.rlp_encode_fields_default(out);
118
119 self.signature.encode(out);
121 }
122
123 pub fn into_parts(self) -> (TempoTransaction, TempoSignature, B256) {
125 let hash = *self.hash();
126 (self.tx, self.signature, hash)
127 }
128
129 fn rlp_encoded_length(&self) -> usize {
131 self.rlp_header().length_with_payload()
132 }
133
134 fn eip2718_encoded_length(&self) -> usize {
136 1 + self.rlp_encoded_length()
137 }
138
139 pub fn eip2718_encode(&self, out: &mut dyn BufMut) {
141 out.put_u8(TEMPO_TX_TYPE_ID);
143 self.rlp_encode(out);
145 }
146
147 pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
149 let header = alloy_rlp::Header::decode(buf)?;
150 if !header.list {
151 return Err(alloy_rlp::Error::UnexpectedString);
152 }
153 let remaining = buf.len();
154
155 if header.payload_length > remaining {
156 return Err(alloy_rlp::Error::InputTooShort);
157 }
158
159 let tx = TempoTransaction::rlp_decode_fields(buf)?;
161
162 let sig_bytes: Bytes = Decodable::decode(buf)?;
164
165 let consumed = remaining - buf.len();
167 if consumed != header.payload_length {
168 return Err(alloy_rlp::Error::UnexpectedLength);
169 }
170
171 let signature = TempoSignature::from_bytes(&sig_bytes).map_err(alloy_rlp::Error::Custom)?;
173
174 Ok(Self::new_unhashed(tx, signature))
175 }
176}
177
178impl TxHashRef for AASigned {
179 fn tx_hash(&self) -> &B256 {
180 self.hash()
181 }
182}
183
184impl Typed2718 for AASigned {
185 fn ty(&self) -> u8 {
186 TEMPO_TX_TYPE_ID
187 }
188}
189
190impl Transaction for AASigned {
191 #[inline]
192 fn chain_id(&self) -> Option<u64> {
193 self.tx.chain_id()
194 }
195
196 #[inline]
197 fn nonce(&self) -> u64 {
198 self.tx.nonce()
199 }
200
201 #[inline]
202 fn gas_limit(&self) -> u64 {
203 self.tx.gas_limit()
204 }
205
206 #[inline]
207 fn gas_price(&self) -> Option<u128> {
208 self.tx.gas_price()
209 }
210
211 #[inline]
212 fn max_fee_per_gas(&self) -> u128 {
213 self.tx.max_fee_per_gas()
214 }
215
216 #[inline]
217 fn max_priority_fee_per_gas(&self) -> Option<u128> {
218 self.tx.max_priority_fee_per_gas()
219 }
220
221 #[inline]
222 fn max_fee_per_blob_gas(&self) -> Option<u128> {
223 None
224 }
225
226 #[inline]
227 fn priority_fee_or_price(&self) -> u128 {
228 self.tx.priority_fee_or_price()
229 }
230
231 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
232 self.tx.effective_gas_price(base_fee)
233 }
234
235 #[inline]
236 fn is_dynamic_fee(&self) -> bool {
237 true
238 }
239
240 #[inline]
241 fn kind(&self) -> TxKind {
242 self.tx
244 .calls
245 .first()
246 .map(|c| c.to)
247 .unwrap_or(TxKind::Create)
248 }
249
250 #[inline]
251 fn is_create(&self) -> bool {
252 self.kind().is_create()
253 }
254
255 #[inline]
256 fn value(&self) -> U256 {
257 self.tx
259 .calls
260 .iter()
261 .fold(U256::ZERO, |acc, call| acc + call.value)
262 }
263
264 #[inline]
265 fn input(&self) -> &Bytes {
266 static EMPTY_BYTES: Bytes = Bytes::new();
268 self.tx
269 .calls
270 .first()
271 .map(|c| &c.input)
272 .unwrap_or(&EMPTY_BYTES)
273 }
274
275 #[inline]
276 fn access_list(&self) -> Option<&AccessList> {
277 Some(&self.tx.access_list)
278 }
279
280 #[inline]
281 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
282 None
283 }
284
285 #[inline]
286 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
287 None
288 }
289}
290
291impl Hash for AASigned {
292 fn hash<H: Hasher>(&self, state: &mut H) {
293 self.hash().hash(state);
294 self.tx.hash(state);
295 self.signature.hash(state);
296 }
297}
298
299impl PartialEq for AASigned {
300 fn eq(&self, other: &Self) -> bool {
301 self.hash() == other.hash() && self.tx == other.tx && self.signature == other.signature
302 }
303}
304
305impl Eq for AASigned {}
306
307impl InMemorySize for AASigned {
308 fn size(&self) -> usize {
309 mem::size_of::<Self>()
310 + self.tx.size()
311 + self.signature.encoded_length()
312 + mem::size_of::<B256>()
313 }
314}
315
316impl alloy_consensus::transaction::SignerRecoverable for AASigned {
317 fn recover_signer(
318 &self,
319 ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
320 let sig_hash = self.signature_hash();
321 self.signature.recover_signer(&sig_hash)
322 }
323
324 fn recover_signer_unchecked(
325 &self,
326 ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
327 self.recover_signer()
330 }
331}
332
333impl Encodable2718 for AASigned {
334 fn encode_2718_len(&self) -> usize {
335 self.eip2718_encoded_length()
336 }
337
338 fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
339 self.eip2718_encode(out)
340 }
341
342 fn trie_hash(&self) -> B256 {
343 *self.hash()
344 }
345}
346
347impl Decodable2718 for AASigned {
348 fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
349 if ty != TEMPO_TX_TYPE_ID {
350 return Err(Eip2718Error::UnexpectedType(ty));
351 }
352 Self::rlp_decode(buf).map_err(Into::into)
353 }
354
355 fn fallback_decode(_: &mut &[u8]) -> Eip2718Result<Self> {
356 Err(Eip2718Error::UnexpectedType(0))
357 }
358}
359
360#[cfg(any(test, feature = "arbitrary"))]
361impl<'a> arbitrary::Arbitrary<'a> for AASigned {
362 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
363 let tx = TempoTransaction::arbitrary(u)?;
364 let signature = TempoSignature::arbitrary(u)?;
365 Ok(Self::new_unhashed(tx, signature))
366 }
367}
368
369#[cfg(feature = "serde")]
370mod serde_impl {
371 use super::*;
372 use serde::{Deserialize, Deserializer, Serialize, Serializer};
373 use std::borrow::Cow;
374
375 #[derive(Serialize, Deserialize)]
376 struct AASignedHelper<'a> {
377 #[serde(flatten)]
378 tx: Cow<'a, TempoTransaction>,
379 signature: Cow<'a, TempoSignature>,
380 hash: Cow<'a, B256>,
381 }
382
383 impl Serialize for super::AASigned {
384 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
385 where
386 S: Serializer,
387 {
388 if let TempoSignature::Keychain(keychain_sig) = &self.signature {
389 let _ = keychain_sig.key_id(&self.signature_hash());
391 }
392 AASignedHelper {
393 tx: Cow::Borrowed(&self.tx),
394 signature: Cow::Borrowed(&self.signature),
395 hash: Cow::Borrowed(self.hash()),
396 }
397 .serialize(serializer)
398 }
399 }
400
401 impl<'de> Deserialize<'de> for super::AASigned {
402 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
403 where
404 D: Deserializer<'de>,
405 {
406 AASignedHelper::deserialize(deserializer).map(|value| {
407 Self::new_unchecked(
408 value.tx.into_owned(),
409 value.signature.into_owned(),
410 value.hash.into_owned(),
411 )
412 })
413 }
414 }
415
416 #[cfg(test)]
417 mod tests {
418 use crate::transaction::{
419 tempo_transaction::{Call, TempoTransaction},
420 tt_signature::{PrimitiveSignature, TempoSignature},
421 };
422 use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
423
424 #[test]
425 fn test_serde_output() {
426 let tx = TempoTransaction {
428 chain_id: 1337,
429 fee_token: None,
430 max_priority_fee_per_gas: 1000000000,
431 max_fee_per_gas: 2000000000,
432 gas_limit: 21000,
433 calls: vec![Call {
434 to: TxKind::Call(Address::repeat_byte(0x42)),
435 value: U256::from(1000),
436 input: Bytes::from(vec![1, 2, 3, 4]),
437 }],
438 nonce_key: U256::ZERO,
439 nonce: 5,
440 ..Default::default()
441 };
442
443 let signature = TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
445 Signature::test_signature(),
446 ));
447
448 let aa_signed = super::super::AASigned::new_unhashed(tx, signature);
449
450 let json = serde_json::to_string_pretty(&aa_signed).unwrap();
452
453 println!("\n=== AASigned JSON Output ===");
454 println!("{json}");
455 println!("============================\n");
456
457 let deserialized: super::super::AASigned = serde_json::from_str(&json).unwrap();
459 assert_eq!(aa_signed.tx(), deserialized.tx());
460 }
461 }
462}