Skip to main content

foundry_common/tempo/
lane.rs

1//! Tempo T5 payment-lane classification shared by anvil and cast.
2
3use alloy_eips::eip2718::Decodable2718;
4use serde::{Deserialize, Serialize};
5use tempo_primitives::TempoTxEnvelope;
6
7/// Structured T5 payment-lane classification for Foundry-facing APIs.
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct PaymentLaneClassification {
11    /// The classified lane.
12    pub lane: PaymentLane,
13    /// Convenience boolean for consumers that only need the lane predicate.
14    pub payment: bool,
15    /// Structured reason for general-lane classification, when known.
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub reason: Option<PaymentLaneReason>,
18}
19
20impl PaymentLaneClassification {
21    /// Constructs a payment-lane classification.
22    pub const fn payment() -> Self {
23        Self { lane: PaymentLane::Payment, payment: true, reason: None }
24    }
25
26    /// Constructs a general-lane classification with a structured reason.
27    pub const fn general(reason: PaymentLaneReason) -> Self {
28        Self { lane: PaymentLane::General, payment: false, reason: Some(reason) }
29    }
30}
31
32/// Payment-lane classifier output lane.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum PaymentLane {
36    Payment,
37    General,
38}
39
40/// Stable Foundry-facing reasons for general-lane classification.
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum PaymentLaneReason {
44    /// The active network is not Tempo.
45    NotTempo,
46    /// Tempo is active but the T5 classifier is not active.
47    T5NotActive,
48    /// The transaction type cannot be classified by Tempo's payment-lane classifier.
49    UnsupportedTransactionType,
50    /// Tempo's T5 classifier classified the transaction as general.
51    NotPaymentLane,
52}
53
54/// Classifies a raw EIP-2718 encoded transaction with Tempo's T5 payment-lane classifier.
55///
56/// Bytes that do not decode as a Tempo envelope (e.g. EIP-4844 or Optimism deposit
57/// transactions) classify as [`PaymentLaneReason::UnsupportedTransactionType`]; callers that
58/// need to reject invalid transactions should validate the bytes beforehand.
59pub 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        // EIP-4844 (0x03) and Optimism deposit (0x7e) have no Tempo envelope variant.
109        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}