use std::{
any::TypeId,
collections::{HashMap, HashSet},
fmt::{Debug, Formatter},
iter::FilterMap,
};
pub trait Log {
const LOG_ID: &'static str;
const LOG_ID_U64: u64;
}
#[derive(Debug, Clone)]
pub struct ErrorDetails {
pub(crate) pkg: String,
pub(crate) file: String,
pub(crate) line: u64,
pub(crate) column: u64,
pub(crate) log_id: Option<String>,
pub(crate) msg: Option<String>,
}
impl ErrorDetails {
pub fn new(
pkg: String,
file: String,
line: u64,
column: u64,
log_id: Option<String>,
msg: Option<String>,
) -> Self {
Self {
pkg,
file,
line,
column,
log_id,
msg,
}
}
}
use fuel_tx::{ContractId, Receipt};
use crate::{
codec::{ABIDecoder, DecoderConfig},
traits::{Parameterize, Tokenizable},
types::errors::{Error, Result, error},
};
#[derive(Clone)]
pub struct LogFormatter {
formatter: fn(DecoderConfig, &[u8]) -> Result<String>,
type_id: TypeId,
}
impl LogFormatter {
pub fn new_log<T: Tokenizable + Parameterize + Debug + 'static>() -> Self {
Self {
formatter: Self::format_log::<T>,
type_id: TypeId::of::<T>(),
}
}
pub fn new_error<T: Tokenizable + Parameterize + std::error::Error + 'static>() -> Self {
Self {
formatter: Self::format_error::<T>,
type_id: TypeId::of::<T>(),
}
}
fn format_log<T: Parameterize + Tokenizable + Debug>(
decoder_config: DecoderConfig,
bytes: &[u8],
) -> Result<String> {
let token = ABIDecoder::new(decoder_config).decode(&T::param_type(), bytes)?;
Ok(format!("{:?}", T::from_token(token)?))
}
fn format_error<T: Parameterize + Tokenizable + std::error::Error>(
decoder_config: DecoderConfig,
bytes: &[u8],
) -> Result<String> {
let token = ABIDecoder::new(decoder_config).decode(&T::param_type(), bytes)?;
Ok(T::from_token(token)?.to_string())
}
pub fn can_handle_type<T: Tokenizable + Parameterize + 'static>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}
pub fn format(&self, decoder_config: DecoderConfig, bytes: &[u8]) -> Result<String> {
(self.formatter)(decoder_config, bytes)
}
}
impl Debug for LogFormatter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LogFormatter")
.field("type_id", &self.type_id)
.finish()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct LogId(ContractId, String);
#[derive(Debug, Clone, Default)]
pub struct LogDecoder {
log_formatters: HashMap<LogId, LogFormatter>,
error_codes: HashMap<u64, ErrorDetails>,
decoder_config: DecoderConfig,
}
#[derive(Debug)]
pub struct LogResult {
pub results: Vec<Result<String>>,
}
impl LogResult {
pub fn filter_succeeded(&self) -> Vec<&str> {
self.results
.iter()
.filter_map(|result| result.as_deref().ok())
.collect()
}
pub fn filter_failed(&self) -> Vec<&Error> {
self.results
.iter()
.filter_map(|result| result.as_ref().err())
.collect()
}
}
impl LogDecoder {
pub fn new(
log_formatters: HashMap<LogId, LogFormatter>,
error_codes: HashMap<u64, ErrorDetails>,
) -> Self {
Self {
log_formatters,
error_codes,
decoder_config: Default::default(),
}
}
pub fn get_error_codes(&self, id: &u64) -> Option<&ErrorDetails> {
self.error_codes.get(id)
}
pub fn set_decoder_config(&mut self, decoder_config: DecoderConfig) -> &mut Self {
self.decoder_config = decoder_config;
self
}
pub fn decode_logs(&self, receipts: &[Receipt]) -> LogResult {
let results = receipts
.iter()
.extract_log_id_and_data()
.map(|(log_id, data)| self.format_log(&log_id, &data))
.collect();
LogResult { results }
}
fn format_log(&self, log_id: &LogId, data: &[u8]) -> Result<String> {
self.log_formatters
.get(log_id)
.ok_or_else(|| {
error!(
Codec,
"missing log formatter for log_id: `{:?}`, data: `{:?}`. \
Consider adding external contracts using `with_contracts()`",
log_id,
data
)
})
.and_then(|log_formatter| log_formatter.format(self.decoder_config, data))
}
pub(crate) fn decode_last_log(&self, receipts: &[Receipt]) -> Result<String> {
receipts
.iter()
.rev()
.extract_log_id_and_data()
.next()
.ok_or_else(|| error!(Codec, "no receipts found for decoding last log"))
.and_then(|(log_id, data)| self.format_log(&log_id, &data))
}
pub(crate) fn decode_last_two_logs(&self, receipts: &[Receipt]) -> Result<(String, String)> {
let res = receipts
.iter()
.rev()
.extract_log_id_and_data()
.map(|(log_id, data)| self.format_log(&log_id, &data))
.take(2)
.collect::<Result<Vec<_>>>();
match res.as_deref() {
Ok([rhs, lhs]) => Ok((lhs.to_string(), rhs.to_string())),
Ok(some_slice) => Err(error!(
Codec,
"expected to have two logs. Found {}",
some_slice.len()
)),
Err(_) => Err(res.expect_err("must be an error")),
}
}
pub fn decode_logs_with_type<T: Tokenizable + Parameterize + 'static>(
&self,
receipts: &[Receipt],
) -> Result<Vec<T>> {
let target_ids: HashSet<LogId> = self
.log_formatters
.iter()
.filter(|(_, log_formatter)| log_formatter.can_handle_type::<T>())
.map(|(log_id, _)| log_id.clone())
.collect();
receipts
.iter()
.extract_log_id_and_data()
.filter_map(|(log_id, bytes)| {
target_ids.contains(&log_id).then(|| {
let token = ABIDecoder::new(self.decoder_config)
.decode(&T::param_type(), bytes.as_slice())?;
T::from_token(token)
})
})
.collect()
}
pub fn decode_logs_lazy<'a, T: Tokenizable + Parameterize + 'static>(
&'a self,
receipt: &'a Receipt,
) -> impl Iterator<Item = impl FnOnce() -> Result<T>> + 'a {
let target_ids: HashSet<&LogId> = self
.log_formatters
.iter()
.filter(|(_, log_formatter)| log_formatter.can_handle_type::<T>())
.map(|(log_id, _)| log_id)
.collect();
std::iter::once(receipt).extract_matching_logs_lazy::<T>(target_ids, self.decoder_config)
}
pub fn merge(&mut self, log_decoder: LogDecoder) {
self.log_formatters.extend(log_decoder.log_formatters);
self.error_codes.extend(log_decoder.error_codes);
}
}
trait ExtractLogIdData {
type Output: Iterator<Item = (LogId, Vec<u8>)>;
fn extract_log_id_and_data(self) -> Self::Output;
}
trait ExtractLogIdLazy {
fn extract_matching_logs_lazy<T: Tokenizable + Parameterize + 'static>(
self,
target_ids: HashSet<&LogId>,
decoder_config: DecoderConfig,
) -> impl Iterator<Item = impl FnOnce() -> Result<T>>;
}
impl<'a, I: Iterator<Item = &'a Receipt>> ExtractLogIdData for I {
type Output = FilterMap<Self, fn(&Receipt) -> Option<(LogId, Vec<u8>)>>;
fn extract_log_id_and_data(self) -> Self::Output {
self.filter_map(|r| match r {
Receipt::LogData {
rb,
data: Some(data),
id,
..
} => Some((LogId(*id, (*rb).to_string()), data.to_vec())),
Receipt::Log { ra, rb, id, .. } => {
Some((LogId(*id, (*rb).to_string()), ra.to_be_bytes().to_vec()))
}
_ => None,
})
}
}
impl<'a, I: Iterator<Item = &'a Receipt>> ExtractLogIdLazy for I {
fn extract_matching_logs_lazy<T: Tokenizable + Parameterize + 'static>(
self,
target_ids: HashSet<&LogId>,
decoder_config: DecoderConfig,
) -> impl Iterator<Item = impl FnOnce() -> Result<T>> {
self.filter_map(move |r| {
let log_id = match r {
Receipt::LogData { rb, id, .. } => LogId(*id, (*rb).to_string()),
Receipt::Log { rb, id, .. } => LogId(*id, (*rb).to_string()),
_ => return None,
};
if !target_ids.contains(&log_id) {
return None;
}
enum Data<'a> {
LogData(&'a [u8]),
LogRa(u64),
}
let data = match r {
Receipt::LogData {
data: Some(data), ..
} => Some(Data::LogData(data.as_slice())),
Receipt::Log { ra, .. } => Some(Data::LogRa(*ra)),
_ => None,
};
data.map(move |data| {
move || {
let normalized_data = match data {
Data::LogData(data) => data,
Data::LogRa(ra) => &ra.to_be_bytes(),
};
let token = ABIDecoder::new(decoder_config)
.decode(&T::param_type(), normalized_data)?;
T::from_token(token)
}
})
})
}
}
pub fn log_formatters_lookup(
log_id_log_formatter_pairs: Vec<(String, LogFormatter)>,
contract_id: ContractId,
) -> HashMap<LogId, LogFormatter> {
log_id_log_formatter_pairs
.into_iter()
.map(|(id, log_formatter)| (LogId(contract_id, id), log_formatter))
.collect()
}