foundry_common/tempo/
lane.rs1use alloy_eips::eip2718::Decodable2718;
4use serde::{Deserialize, Serialize};
5use tempo_primitives::TempoTxEnvelope;
6
7pub 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct PaymentLaneClassification {
28 pub lane: PaymentLane,
30 pub payment: bool,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub reason: Option<PaymentLaneReason>,
35}
36
37impl PaymentLaneClassification {
38 pub const fn payment() -> Self {
40 Self { lane: PaymentLane::Payment, payment: true, reason: None }
41 }
42
43 pub const fn general(reason: PaymentLaneReason) -> Self {
45 Self { lane: PaymentLane::General, payment: false, reason: Some(reason) }
46 }
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum PaymentLane {
53 Payment,
54 General,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum PaymentLaneReason {
61 NotTempo,
63 T5NotActive,
65 UnsupportedTransactionType,
67 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 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}