Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/common/fmt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ serde_json.workspace = true
chrono.workspace = true
revm.workspace = true
yansi.workspace = true
comfy-table.workspace = true

# Tempo
tempo-alloy.workspace = true
Expand Down
107 changes: 107 additions & 0 deletions crates/common/fmt/src/console.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::UIfmt;
use alloy_primitives::{Address, Bytes, FixedBytes, I256, U256};
use comfy_table::{Table, TableComponent, presets::UTF8_FULL};
use std::fmt::{self, Write};

/// A piece is a portion of the format string which represents the next part to emit.
Expand Down Expand Up @@ -407,6 +408,36 @@ fn format_spec<'a>(
}
}

pub fn console_table_format(
keys: Option<&[&dyn ConsoleFmt]>,
values: &[&dyn ConsoleFmt],
) -> String {
let keys_strings: Vec<String> = match keys {
Some(keys) => keys.iter().map(|k| k.fmt(FormatSpec::String)).collect(),
None => (0..values.len()).map(|i| i.to_string()).collect(),
};
let values_strings: Vec<String> = values.iter().map(|v| v.fmt(FormatSpec::String)).collect();

let mut table = Table::new();
table.load_preset(UTF8_FULL);
table.set_style(TableComponent::VerticalLines, '│');
table.set_style(TableComponent::HeaderLines, '─');
table.set_style(TableComponent::MiddleHeaderIntersections, '┼');
table.set_style(TableComponent::LeftHeaderIntersection, '├');
table.set_style(TableComponent::RightHeaderIntersection, '┤');
table.set_header(vec!["(index)", "Values"]);
table.remove_style(TableComponent::HorizontalLines);
table.remove_style(TableComponent::MiddleIntersections);
table.remove_style(TableComponent::LeftBorderIntersections);
table.remove_style(TableComponent::RightBorderIntersections);
for i in 0..keys_strings.len().max(values_strings.len()) {
let key = keys_strings.get(i).map(String::as_str).unwrap_or("");
let value = values_strings.get(i).map(String::as_str).unwrap_or("");
table.add_row(vec![key, value]);
}
table.to_string()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -610,4 +641,80 @@ mod tests {
let call = Logs::Log1(log1);
assert_eq!(call.fmt(Default::default()), "foo 42 bar");
}

#[test]
fn test_console_table_format() {
// auto-indexed, uint256 values
let values: &[&dyn ConsoleFmt] = &[&U256::from(100), &U256::from(200), &U256::from(300)];
assert_eq!(
console_table_format(None, values),
"┌─────────┬────────┐\n\
│ (index) │ Values │\n\
├─────────┼────────┤\n\
│ 0 │ 100 │\n\
│ 1 │ 200 │\n\
│ 2 │ 300 │\n\
└─────────┴────────┘"
);

// string keys, uint256 values
// key col expands to fit "charlie123" and value col expands to fit "20000000000000000"
let keys: &[&dyn ConsoleFmt] =
&[&String::from("alice"), &String::from("bob"), &String::from("charlie123")];
let values: &[&dyn ConsoleFmt] = &[
&U256::from(1),
&U256::from_str("20000000000000000").unwrap(),
&U256::from_str("30000000000").unwrap(),
];
assert_eq!(
console_table_format(Some(keys), values),
"┌────────────┬───────────────────┐\n\
│ (index) │ Values │\n\
├────────────┼───────────────────┤\n\
│ alice │ 1 │\n\
│ bob │ 20000000000000000 │\n\
│ charlie123 │ 30000000000 │\n\
└────────────┴───────────────────┘"
);

// empty table
assert_eq!(
console_table_format(None, &[]),
"┌─────────┬────────┐\n\
│ (index) │ Values │\n\
├─────────┼────────┤\n\
└─────────┴────────┘"
);

// more keys than values
let keys: &[&dyn ConsoleFmt] =
&[&String::from("alice"), &String::from("bob"), &String::from("charlie")];
let values: &[&dyn ConsoleFmt] = &[&U256::from(1), &U256::from(2)];
assert_eq!(
console_table_format(Some(keys), values),
"┌─────────┬────────┐\n\
│ (index) │ Values │\n\
├─────────┼────────┤\n\
│ alice │ 1 │\n\
│ bob │ 2 │\n\
│ charlie │ │\n\
└─────────┴────────┘"
);

// more values than keys
let keys: &[&dyn ConsoleFmt] = &[&String::from("alice"), &String::from("bob")];
let values: &[&dyn ConsoleFmt] =
&[&U256::from(1), &U256::from(2), &U256::from(3), &U256::from(4)];
assert_eq!(
console_table_format(Some(keys), values),
"┌─────────┬────────┐\n\
│ (index) │ Values │\n\
├─────────┼────────┤\n\
│ alice │ 1 │\n\
│ bob │ 2 │\n\
│ │ 3 │\n\
│ │ 4 │\n\
└─────────┴────────┘"
);
}
}
2 changes: 1 addition & 1 deletion crates/common/fmt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]

mod console;
pub use console::{ConsoleFmt, FormatSpec, console_format};
pub use console::{ConsoleFmt, FormatSpec, console_format, console_table_format};

mod dynamic;
pub use dynamic::{
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/abi/src/Console.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions crates/evm/abi/src/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ def main():
# Parse signatures from `console.sol`'s string literals
console_sol = open(console_file).read()
sig_strings = re.findall(
r'"(log.*?)"',
r'"((?:log|table).*?)"',
console_sol,
)
raw_sigs = [s.strip().strip('"') for s in sig_strings]
sigs = [
s.replace("string", "string memory").replace("bytes)", "bytes memory)")
re.sub(r"(\w+\[\])", r"\1 memory", s)
.replace("string,", "string memory,")
.replace("string)", "string memory)")
.replace("bytes)", "bytes memory)")
for s in raw_sigs
]
sigs = list(set(sigs))
Expand All @@ -38,6 +41,7 @@ def main():
)
combined = json.loads(r.stdout.strip())
abi = combined["contracts"]["<stdin>:HardhatConsole"]["abi"]

open(abi_file, "w").write(json.dumps(abi, separators=(",", ":"), indent=None))


Expand Down
4 changes: 3 additions & 1 deletion crates/evm/evm/src/inspectors/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ impl LogCollector {

fn hardhat_log(&mut self, data: &[u8]) -> alloy_sol_types::Result<()> {
let decoded = console::hh::ConsoleCalls::abi_decode(data)?;
self.push_msg(&decoded.fmt(Default::default()));
for line in decoded.fmt(Default::default()).lines() {
self.push_msg(line);
}
Ok(())
}

Expand Down
46 changes: 41 additions & 5 deletions crates/macros/src/console_fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use syn::{
pub fn console_fmt(input: &DeriveInput) -> TokenStream {
let name = &input.ident;
let tokens = match &input.data {
Data::Struct(s) => derive_struct(s),
Data::Struct(s) => derive_struct(s, name),
Data::Enum(e) => derive_enum(e),
Data::Union(_) => return quote!(compile_error!("Unions are unsupported");),
};
Expand All @@ -18,16 +18,16 @@ pub fn console_fmt(input: &DeriveInput) -> TokenStream {
}
}

fn derive_struct(s: &DataStruct) -> TokenStream {
let imp = impl_struct(s).unwrap_or_else(|| quote!(String::new()));
fn derive_struct(s: &DataStruct, name: &Ident) -> TokenStream {
let imp = impl_struct(s, name).unwrap_or_else(|| quote!(String::new()));
quote! {
fn fmt(&self, _spec: FormatSpec) -> String {
#imp
}
}
}

fn impl_struct(s: &DataStruct) -> Option<TokenStream> {
fn impl_struct(s: &DataStruct, name: &Ident) -> Option<TokenStream> {
if s.fields.is_empty() {
return None;
}
Expand All @@ -36,13 +36,49 @@ fn impl_struct(s: &DataStruct) -> Option<TokenStream> {
return None;
}

let members = s.fields.members().collect::<Vec<_>>();
let fields = s.fields.iter().collect::<Vec<_>>();

// Detect table call structs: name must start with "table" (from the ABI function name) and
// all fields must be Vec<T> types (Solidity arrays). Both conditions together prevent
// accidental table rendering for unrelated structs that happen to have Vec fields.
let is_table = name.to_string().starts_with("table")
&& !fields.is_empty()
&& fields.iter().all(|f| match &f.ty {
Type::Path(path) => path.path.segments.last().is_some_and(|seg| seg.ident == "Vec"),
_ => false,
});
if is_table {
let member_ref = |m: &Member| match m {
Member::Named(ident) => quote!(&self.#ident),
Member::Unnamed(idx) => quote!(&self.#idx),
};
let imp = if members.len() == 1 {
let vals = member_ref(&members[0]);
quote! {
let values: ::std::vec::Vec<&dyn ConsoleFmt> =
(#vals).iter().map(|v| v as &dyn ConsoleFmt).collect();
console_table_format(None, &values)
}
} else {
let keys = member_ref(&members[0]);
let vals = member_ref(&members[1]);
quote! {
let keys: ::std::vec::Vec<&dyn ConsoleFmt> =
(#keys).iter().map(|v| v as &dyn ConsoleFmt).collect();
let values: ::std::vec::Vec<&dyn ConsoleFmt> =
(#vals).iter().map(|v| v as &dyn ConsoleFmt).collect();
console_table_format(Some(&keys), &values)
}
};
return Some(imp);
}

let first_ty = match &fields.first().unwrap().ty {
Type::Path(path) => path.path.segments.last().unwrap().ident.to_string(),
_ => String::new(),
};

let members = s.fields.members().collect::<Vec<_>>();
let args: Punctuated<TokenStream, Token![,]> = members
.into_iter()
.map(|member| match member {
Expand Down
Loading