foundry_evm_traces/
erc8021.rs1use alloy_primitives::hex;
7use itertools::Itertools;
8use serde::Deserialize;
9use std::fmt;
10
11const MARKER: [u8; 16] = hex!("80218021802180218021802180218021");
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
16pub struct Attribution {
17 codes: Vec<(String, Option<&'static str>)>,
19}
20
21impl Attribution {
22 pub fn decode(data: &[u8]) -> Option<Self> {
27 let (_, schema_id, schema_data) = split_suffix(data)?;
28 let codes = match schema_id {
29 0 | 1 => {
31 let (&codes_len, rest) = schema_data.split_last()?;
32 let codes = std::str::from_utf8(&rest[rest.len() - codes_len as usize..]).ok()?;
33 codes
34 .split(',')
35 .filter(|code| !code.is_empty())
36 .map(|code| (code.into(), None))
37 .collect::<Vec<_>>()
38 }
39 2 => {
41 let cbor = &schema_data[..schema_data.len() - 2];
42 let CborAttribution { app, wallet, services } = ciborium::from_reader(cbor).ok()?;
43 app.map(|code| (code, Some("app")))
44 .into_iter()
45 .chain(wallet.map(|code| (code, Some("wallet"))))
46 .chain(services.into_iter().map(|code| (code, Some("service"))))
47 .collect::<Vec<_>>()
48 }
49 _ => return None,
50 };
51 (!codes.is_empty()).then_some(Self { codes })
52 }
53}
54
55impl fmt::Display for Attribution {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 let codes = self.codes.iter().format_with(", ", |(code, role), f| match role {
59 Some(role) => f(&format_args!("{} ({role})", code.escape_debug())),
60 None => f(&code.escape_debug()),
61 });
62 write!(f, "{codes}")
63 }
64}
65
66#[derive(Deserialize)]
68struct CborAttribution {
69 #[serde(rename = "a")]
70 app: Option<String>,
71 #[serde(rename = "w")]
72 wallet: Option<String>,
73 #[serde(rename = "s", default)]
74 services: Vec<String>,
75}
76
77pub fn strip_suffix(data: &[u8]) -> &[u8] {
79 split_suffix(data).map_or(data, |(data, ..)| data)
80}
81
82fn split_suffix(data: &[u8]) -> Option<(&[u8], u8, &[u8])> {
85 let rest = data.strip_suffix(&MARKER)?;
86 let (&schema_id, rest) = rest.split_last()?;
87 let schema_data_len = match schema_id {
88 0 => 1 + *rest.last()? as usize,
90 1 => {
92 let codes_len = *rest.last()? as usize;
93 let chain_id_len = *rest.get(rest.len().checked_sub(2 + codes_len)?)? as usize;
94 22 + codes_len + chain_id_len
95 }
96 _ => 2 + u16::from_be_bytes(*rest.last_chunk()?) as usize,
98 };
99 let (data, schema_data) = rest.split_at(rest.len().checked_sub(schema_data_len)?);
100 Some((data, schema_id, schema_data))
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 const SCHEMA_0: &[u8] = &hex!("dddddddd62617365617070070080218021802180218021802180218021");
109 const SCHEMA_1: &[u8] = &hex!(
110 "ddddddddcccccccccccccccccccccccccccccccccccccccc210502626173656170702C6D6F7270686F0E0180218021802180218021802180218021"
111 );
112 const SCHEMA_2: &[u8] =
113 &hex!("dddddddda161616762617365617070000b0280218021802180218021802180218021");
114 const SCHEMA_2_METADATA: &[u8] = &hex!(
115 "dddddddda46161676261736561707061776570726976796172a16161a26163663078323130356161782a307842636632423935393845633037383165453439323330436141434630464233433838314235313935616da26c75746d5f63616d706169676e6c77696e7465722d70726f6d6f66736f7572636566776562617070007b0280218021802180218021802180218021"
116 );
117 const SCHEMA_2_SERVICES: &[u8] = &hex!(
118 "dddddddda361616762617365617070617765707269767961738269666c617368626f747365746974616e00260280218021802180218021802180218021"
119 );
120 const UNKNOWN_SCHEMA: &[u8] = &hex!("ddddddddff80218021802180218021802180218021");
121
122 #[test]
123 fn test_strip_suffix() {
124 for data in [SCHEMA_0, SCHEMA_1, SCHEMA_2, SCHEMA_2_METADATA, SCHEMA_2_SERVICES] {
125 assert_eq!(strip_suffix(data), hex!("dddddddd"));
126 }
127 assert_eq!(strip_suffix(UNKNOWN_SCHEMA), UNKNOWN_SCHEMA);
128 }
129
130 #[test]
131 fn test_decode_attribution() {
132 let decode =
133 |data: &[u8]| Attribution::decode(data).map(|attribution| attribution.to_string());
134 assert_eq!(decode(SCHEMA_0).as_deref(), Some("baseapp"));
135 assert_eq!(decode(SCHEMA_1).as_deref(), Some("baseapp, morpho"));
136 assert_eq!(decode(SCHEMA_2).as_deref(), Some("baseapp (app)"));
137 assert_eq!(decode(SCHEMA_2_METADATA).as_deref(), Some("baseapp (app), privy (wallet)"));
138 assert_eq!(
139 decode(SCHEMA_2_SERVICES).as_deref(),
140 Some("baseapp (app), privy (wallet), flashbots (service), titan (service)")
141 );
142 assert_eq!(decode(UNKNOWN_SCHEMA), None);
143 let data = [&hex!("1b5b33316d0500")[..], &MARKER].concat();
145 assert_eq!(decode(&data).as_deref(), Some("\\u{1b}[31m"));
146 }
147}