Skip to main content

fuels_core/codec/
logs.rs

1use std::{
2    any::TypeId,
3    collections::{HashMap, HashSet},
4    fmt::{Debug, Formatter},
5    iter::FilterMap,
6};
7
8/// Trait that represents a log with a unique identifier.
9pub trait Log {
10    /// Returns the unique identifier of the log as a string.
11    const LOG_ID: &'static str;
12    /// Returns the unique identifier of the log as a `u64`.
13    const LOG_ID_U64: u64;
14}
15
16#[derive(Debug, Clone)]
17pub struct ErrorDetails {
18    pub(crate) pkg: String,
19    pub(crate) file: String,
20    pub(crate) line: u64,
21    pub(crate) column: u64,
22    pub(crate) log_id: Option<String>,
23    pub(crate) msg: Option<String>,
24}
25
26impl ErrorDetails {
27    pub fn new(
28        pkg: String,
29        file: String,
30        line: u64,
31        column: u64,
32        log_id: Option<String>,
33        msg: Option<String>,
34    ) -> Self {
35        Self {
36            pkg,
37            file,
38            line,
39            column,
40            log_id,
41            msg,
42        }
43    }
44}
45
46use fuel_tx::{ContractId, Receipt};
47
48use crate::{
49    codec::{ABIDecoder, DecoderConfig},
50    traits::{Parameterize, Tokenizable},
51    types::errors::{Error, Result, error},
52};
53
54#[derive(Clone)]
55pub struct LogFormatter {
56    formatter: fn(DecoderConfig, &[u8]) -> Result<String>,
57    type_id: TypeId,
58}
59
60impl LogFormatter {
61    pub fn new_log<T: Tokenizable + Parameterize + Debug + 'static>() -> Self {
62        Self {
63            formatter: Self::format_log::<T>,
64            type_id: TypeId::of::<T>(),
65        }
66    }
67
68    pub fn new_error<T: Tokenizable + Parameterize + std::error::Error + 'static>() -> Self {
69        Self {
70            formatter: Self::format_error::<T>,
71            type_id: TypeId::of::<T>(),
72        }
73    }
74
75    fn format_log<T: Parameterize + Tokenizable + Debug>(
76        decoder_config: DecoderConfig,
77        bytes: &[u8],
78    ) -> Result<String> {
79        let token = ABIDecoder::new(decoder_config).decode(&T::param_type(), bytes)?;
80
81        Ok(format!("{:?}", T::from_token(token)?))
82    }
83
84    fn format_error<T: Parameterize + Tokenizable + std::error::Error>(
85        decoder_config: DecoderConfig,
86        bytes: &[u8],
87    ) -> Result<String> {
88        let token = ABIDecoder::new(decoder_config).decode(&T::param_type(), bytes)?;
89
90        Ok(T::from_token(token)?.to_string())
91    }
92
93    pub fn can_handle_type<T: Tokenizable + Parameterize + 'static>(&self) -> bool {
94        TypeId::of::<T>() == self.type_id
95    }
96
97    pub fn format(&self, decoder_config: DecoderConfig, bytes: &[u8]) -> Result<String> {
98        (self.formatter)(decoder_config, bytes)
99    }
100}
101
102impl Debug for LogFormatter {
103    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("LogFormatter")
105            .field("type_id", &self.type_id)
106            .finish()
107    }
108}
109
110/// Holds a unique log ID
111#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
112pub struct LogId(ContractId, String);
113
114/// Struct used to pass the log mappings from the Abigen
115#[derive(Debug, Clone, Default)]
116pub struct LogDecoder {
117    /// A mapping of LogId and param-type
118    log_formatters: HashMap<LogId, LogFormatter>,
119    error_codes: HashMap<u64, ErrorDetails>,
120    decoder_config: DecoderConfig,
121}
122
123#[derive(Debug)]
124pub struct LogResult {
125    pub results: Vec<Result<String>>,
126}
127
128impl LogResult {
129    pub fn filter_succeeded(&self) -> Vec<&str> {
130        self.results
131            .iter()
132            .filter_map(|result| result.as_deref().ok())
133            .collect()
134    }
135
136    pub fn filter_failed(&self) -> Vec<&Error> {
137        self.results
138            .iter()
139            .filter_map(|result| result.as_ref().err())
140            .collect()
141    }
142}
143
144impl LogDecoder {
145    pub fn new(
146        log_formatters: HashMap<LogId, LogFormatter>,
147        error_codes: HashMap<u64, ErrorDetails>,
148    ) -> Self {
149        Self {
150            log_formatters,
151            error_codes,
152            decoder_config: Default::default(),
153        }
154    }
155
156    pub fn get_error_codes(&self, id: &u64) -> Option<&ErrorDetails> {
157        self.error_codes.get(id)
158    }
159
160    pub fn set_decoder_config(&mut self, decoder_config: DecoderConfig) -> &mut Self {
161        self.decoder_config = decoder_config;
162        self
163    }
164
165    /// Get all logs results from the given receipts as `Result<String>`
166    pub fn decode_logs(&self, receipts: &[Receipt]) -> LogResult {
167        let results = receipts
168            .iter()
169            .extract_log_id_and_data()
170            .map(|(log_id, data)| self.format_log(&log_id, &data))
171            .collect();
172
173        LogResult { results }
174    }
175
176    fn format_log(&self, log_id: &LogId, data: &[u8]) -> Result<String> {
177        self.log_formatters
178            .get(log_id)
179            .ok_or_else(|| {
180                error!(
181                    Codec,
182                    "missing log formatter for log_id: `{:?}`, data: `{:?}`. \
183                     Consider adding external contracts using `with_contracts()`",
184                    log_id,
185                    data
186                )
187            })
188            .and_then(|log_formatter| log_formatter.format(self.decoder_config, data))
189    }
190
191    pub(crate) fn decode_last_log(&self, receipts: &[Receipt]) -> Result<String> {
192        receipts
193            .iter()
194            .rev()
195            .extract_log_id_and_data()
196            .next()
197            .ok_or_else(|| error!(Codec, "no receipts found for decoding last log"))
198            .and_then(|(log_id, data)| self.format_log(&log_id, &data))
199    }
200
201    pub(crate) fn decode_last_two_logs(&self, receipts: &[Receipt]) -> Result<(String, String)> {
202        let res = receipts
203            .iter()
204            .rev()
205            .extract_log_id_and_data()
206            .map(|(log_id, data)| self.format_log(&log_id, &data))
207            .take(2)
208            .collect::<Result<Vec<_>>>();
209
210        match res.as_deref() {
211            Ok([rhs, lhs]) => Ok((lhs.to_string(), rhs.to_string())),
212            Ok(some_slice) => Err(error!(
213                Codec,
214                "expected to have two logs. Found {}",
215                some_slice.len()
216            )),
217            Err(_) => Err(res.expect_err("must be an error")),
218        }
219    }
220
221    /// Get decoded logs with specific type from the given receipts.
222    /// Note that this method returns the actual type and not a `String` representation.
223    pub fn decode_logs_with_type<T: Tokenizable + Parameterize + 'static>(
224        &self,
225        receipts: &[Receipt],
226    ) -> Result<Vec<T>> {
227        let target_ids: HashSet<LogId> = self
228            .log_formatters
229            .iter()
230            .filter(|(_, log_formatter)| log_formatter.can_handle_type::<T>())
231            .map(|(log_id, _)| log_id.clone())
232            .collect();
233
234        receipts
235            .iter()
236            .extract_log_id_and_data()
237            .filter_map(|(log_id, bytes)| {
238                target_ids.contains(&log_id).then(|| {
239                    let token = ABIDecoder::new(self.decoder_config)
240                        .decode(&T::param_type(), bytes.as_slice())?;
241
242                    T::from_token(token)
243                })
244            })
245            .collect()
246    }
247
248    /// Get LogIds and lazy decoders for specific type from a single receipt.
249    pub fn decode_logs_lazy<'a, T: Tokenizable + Parameterize + 'static>(
250        &'a self,
251        receipt: &'a Receipt,
252    ) -> impl Iterator<Item = impl FnOnce() -> Result<T>> + 'a {
253        let target_ids: HashSet<&LogId> = self
254            .log_formatters
255            .iter()
256            .filter(|(_, log_formatter)| log_formatter.can_handle_type::<T>())
257            .map(|(log_id, _)| log_id)
258            .collect();
259
260        std::iter::once(receipt).extract_matching_logs_lazy::<T>(target_ids, self.decoder_config)
261    }
262
263    pub fn merge(&mut self, log_decoder: LogDecoder) {
264        self.log_formatters.extend(log_decoder.log_formatters);
265        self.error_codes.extend(log_decoder.error_codes);
266    }
267}
268
269trait ExtractLogIdData {
270    type Output: Iterator<Item = (LogId, Vec<u8>)>;
271    fn extract_log_id_and_data(self) -> Self::Output;
272}
273
274trait ExtractLogIdLazy {
275    fn extract_matching_logs_lazy<T: Tokenizable + Parameterize + 'static>(
276        self,
277        target_ids: HashSet<&LogId>,
278        decoder_config: DecoderConfig,
279    ) -> impl Iterator<Item = impl FnOnce() -> Result<T>>;
280}
281
282impl<'a, I: Iterator<Item = &'a Receipt>> ExtractLogIdData for I {
283    type Output = FilterMap<Self, fn(&Receipt) -> Option<(LogId, Vec<u8>)>>;
284    fn extract_log_id_and_data(self) -> Self::Output {
285        self.filter_map(|r| match r {
286            Receipt::LogData {
287                rb,
288                data: Some(data),
289                id,
290                ..
291            } => Some((LogId(*id, (*rb).to_string()), data.to_vec())),
292            Receipt::Log { ra, rb, id, .. } => {
293                Some((LogId(*id, (*rb).to_string()), ra.to_be_bytes().to_vec()))
294            }
295            _ => None,
296        })
297    }
298}
299
300impl<'a, I: Iterator<Item = &'a Receipt>> ExtractLogIdLazy for I {
301    fn extract_matching_logs_lazy<T: Tokenizable + Parameterize + 'static>(
302        self,
303        target_ids: HashSet<&LogId>,
304        decoder_config: DecoderConfig,
305    ) -> impl Iterator<Item = impl FnOnce() -> Result<T>> {
306        self.filter_map(move |r| {
307            let log_id = match r {
308                Receipt::LogData { rb, id, .. } => LogId(*id, (*rb).to_string()),
309                Receipt::Log { rb, id, .. } => LogId(*id, (*rb).to_string()),
310                _ => return None,
311            };
312
313            if !target_ids.contains(&log_id) {
314                return None;
315            }
316
317            enum Data<'a> {
318                LogData(&'a [u8]),
319                LogRa(u64),
320            }
321
322            let data = match r {
323                Receipt::LogData {
324                    data: Some(data), ..
325                } => Some(Data::LogData(data.as_slice())),
326                Receipt::Log { ra, .. } => Some(Data::LogRa(*ra)),
327                _ => None,
328            };
329
330            data.map(move |data| {
331                move || {
332                    let normalized_data = match data {
333                        Data::LogData(data) => data,
334                        Data::LogRa(ra) => &ra.to_be_bytes(),
335                    };
336                    let token = ABIDecoder::new(decoder_config)
337                        .decode(&T::param_type(), normalized_data)?;
338                    T::from_token(token)
339                }
340            })
341        })
342    }
343}
344
345pub fn log_formatters_lookup(
346    log_id_log_formatter_pairs: Vec<(String, LogFormatter)>,
347    contract_id: ContractId,
348) -> HashMap<LogId, LogFormatter> {
349    log_id_log_formatter_pairs
350        .into_iter()
351        .map(|(id, log_formatter)| (LogId(contract_id, id), log_formatter))
352        .collect()
353}