foundry_common/tempo/
lane.rs1use alloy_eips::eip2718::Decodable2718;
4use serde::{Deserialize, Serialize};
5use tempo_primitives::TempoTxEnvelope;
6
7#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct PaymentLaneClassification {
11 pub lane: PaymentLane,
13 pub payment: bool,
15 #[serde(skip_serializing_if = "Option::is_none")]
17 pub reason: Option<PaymentLaneReason>,
18}
19
20impl PaymentLaneClassification {
21 pub const fn payment() -> Self {
23 Self { lane: PaymentLane::Payment, payment: true, reason: None }
24 }
25
26 pub const fn general(reason: PaymentLaneReason) -> Self {
28 Self { lane: PaymentLane::General, payment: false, reason: Some(reason) }
29 }
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum PaymentLane {
36 Payment,
37 General,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum PaymentLaneReason {
44 NotTempo,
46 T5NotActive,
48 UnsupportedTransactionType,
50 NotPaymentLane,
52}
53
54pub fn classify_payment_lane(mut raw: &[u8]) -> PaymentLaneClassification {
60 let Ok(tx) = TempoTxEnvelope::decode_2718(&mut raw) else {
61 return PaymentLaneClassification::general(PaymentLaneReason::UnsupportedTransactionType);
62 };
63
64 if tx.is_payment_v2() {
65 PaymentLaneClassification::payment()
66 } else {
67 PaymentLaneClassification::general(PaymentLaneReason::NotPaymentLane)
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use alloy_consensus::{Signed, TxEip1559};
75 use alloy_eips::eip2718::Encodable2718;
76 use alloy_primitives::{Address, Signature, TxKind, U256, address};
77 use alloy_sol_types::SolCall;
78 use tempo_alloy::contracts::precompiles::ITIP20;
79
80 const PAYMENT_TOKEN: Address = address!("20c0000000000000000000000000000000000001");
81
82 fn encode_eip1559(to: Address, input: Vec<u8>) -> Vec<u8> {
83 let tx = TxEip1559 { to: TxKind::Call(to), input: input.into(), ..Default::default() };
84 TempoTxEnvelope::Eip1559(Signed::new_unhashed(tx, Signature::test_signature()))
85 .encoded_2718()
86 }
87
88 #[test]
89 fn classifies_tip20_transfer_as_payment() {
90 let input = ITIP20::transferCall { to: Address::ZERO, amount: U256::from(1) }.abi_encode();
91 let raw = encode_eip1559(PAYMENT_TOKEN, input);
92
93 assert_eq!(classify_payment_lane(&raw), PaymentLaneClassification::payment());
94 }
95
96 #[test]
97 fn classifies_non_payment_call_as_general() {
98 let raw = encode_eip1559(address!("1234567890123456789012345678901234567890"), vec![]);
99
100 assert_eq!(
101 classify_payment_lane(&raw),
102 PaymentLaneClassification::general(PaymentLaneReason::NotPaymentLane)
103 );
104 }
105
106 #[test]
107 fn classifies_non_tempo_transaction_type_as_unsupported() {
108 for type_byte in [0x03_u8, 0x7e] {
110 let raw = [type_byte, 0xc0];
111
112 assert_eq!(
113 classify_payment_lane(&raw),
114 PaymentLaneClassification::general(PaymentLaneReason::UnsupportedTransactionType)
115 );
116 }
117 }
118
119 #[test]
120 fn serializes_stable_json_shape() {
121 let payment = serde_json::to_value(PaymentLaneClassification::payment()).unwrap();
122 assert_eq!(payment, serde_json::json!({ "lane": "payment", "payment": true }));
123
124 let general =
125 serde_json::to_value(PaymentLaneClassification::general(PaymentLaneReason::NotTempo))
126 .unwrap();
127 assert_eq!(
128 general,
129 serde_json::json!({ "lane": "general", "payment": false, "reason": "not_tempo" })
130 );
131 }
132}