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