--- /dev/null +++ b/crates/evm-coder-macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "evm-coder-macros" +version = "0.1.0" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +sha3 = "0.9.1" +quote = "1.0" +proc-macro2 = "1.0" +syn = { version = "1.0", features = ["full"] } +hex = "0.4.3" +Inflector = "0.11.4" +darling = "0.13.0" \ No newline at end of file --- /dev/null +++ b/crates/evm-coder-macros/src/lib.rs @@ -0,0 +1,224 @@ +use darling::FromMeta; +use inflector::cases; +use proc_macro::TokenStream; +use quote::quote; +use sha3::{Digest, Keccak256}; +use syn::{ + AttributeArgs, DeriveInput, GenericArgument, Ident, ItemTrait, Pat, Path, PathArguments, + PathSegment, Type, parse_macro_input, spanned::Spanned, +}; + +mod solidity_interface; +mod to_log; + +fn fn_selector_str(input: &str) -> u32 { + let mut hasher = Keccak256::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + + let mut selector_bytes = [0; 4]; + selector_bytes.copy_from_slice(&result[0..4]); + + u32::from_be_bytes(selector_bytes) +} + +/// Returns solidity function selector (first 4 bytes of hash) by its +/// textual representation +/// +/// ```rs +/// use evm_coder_macros::fn_selector; +/// +/// assert_eq!(fn_selector!(transfer(address, uint256)), 0xa9059cbb); +/// ``` +#[proc_macro] +pub fn fn_selector(input: TokenStream) -> TokenStream { + let input = input.to_string().replace(' ', ""); + let selector = fn_selector_str(&input); + + (quote! { + #selector + }) + .into() +} + +fn event_selector_str(input: &str) -> [u8; 32] { + let mut hasher = Keccak256::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + + let mut selector_bytes = [0; 32]; + selector_bytes.copy_from_slice(&result[0..32]); + selector_bytes +} + +/// Returns solidity topic (hash) by its textual representation +/// +/// ```rs +/// use evm_coder_macros::event_topic; +/// +/// assert_eq!( +/// format!("{:x}", event_topic!(Transfer(address, address, uint256))), +/// "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", +/// ); +/// ``` +#[proc_macro] +pub fn event_topic(stream: TokenStream) -> TokenStream { + let input = stream.to_string().replace(' ', ""); + let selector_bytes = event_selector_str(&input); + + (quote! { + ::primitive_types::H256([#( + #selector_bytes, + )*]) + }) + .into() +} + +fn parse_path(ty: &Type) -> syn::Result<&Path> { + match &ty { + syn::Type::Path(pat) => { + if let Some(qself) = &pat.qself { + return Err(syn::Error::new(qself.ty.span(), "no receiver expected")); + } + Ok(&pat.path) + } + _ => Err(syn::Error::new(ty.span(), "expected ty to be path")), + } +} + +fn parse_path_segment(path: &Path) -> syn::Result<&PathSegment> { + if path.segments.len() != 1 { + return Err(syn::Error::new( + path.span(), + "expected path to have only segment", + )); + } + let last_segment = &path.segments.last().unwrap(); + Ok(last_segment) +} + +fn parse_ident_from_pat(pat: &Pat) -> syn::Result<&Ident> { + match pat { + Pat::Ident(i) => Ok(&i.ident), + _ => Err(syn::Error::new(pat.span(), "expected pat ident")), + } +} + +fn parse_ident_from_segment(segment: &PathSegment) -> syn::Result<&Ident> { + if segment.arguments != PathArguments::None { + return Err(syn::Error::new( + segment.arguments.span(), + "unexpected generic type", + )); + } + Ok(&segment.ident) +} + +fn parse_ident_from_path(path: &Path) -> syn::Result<&Ident> { + let segment = parse_path_segment(path)?; + parse_ident_from_segment(segment) +} + +fn parse_ident_from_type(ty: &Type) -> syn::Result<&Ident> { + let path = parse_path(ty)?; + parse_ident_from_path(path) +} + +// Gets T out of Result +fn parse_result_ok(ty: &Type) -> syn::Result<&Ident> { + let path = parse_path(ty)?; + let segment = parse_path_segment(path)?; + + if segment.ident != "Result" { + return Err(syn::Error::new( + ty.span(), + "expected Result as return type (no renamed aliases allowed)", + )); + } + let args = match &segment.arguments { + PathArguments::AngleBracketed(e) => e, + _ => { + return Err(syn::Error::new( + segment.arguments.span(), + "missing Result generics", + )) + } + }; + + let args = &args.args; + let arg = args.first().unwrap(); + + let ty = match arg { + GenericArgument::Type(ty) => ty, + _ => { + return Err(syn::Error::new( + arg.span(), + "expected first generic to be type", + )) + } + }; + + parse_ident_from_type(ty) +} + +fn pascal_ident_to_call(ident: &Ident) -> Ident { + let name = format!("{}Call", ident); + Ident::new(&name, ident.span()) +} +fn snake_ident_to_pascal(ident: &Ident) -> Ident { + let name = ident.to_string(); + let name = cases::pascalcase::to_pascal_case(&name); + Ident::new(&name, ident.span()) +} +fn snake_ident_to_screaming(ident: &Ident) -> Ident { + let name = ident.to_string(); + let name = cases::screamingsnakecase::to_screaming_snake_case(&name); + Ident::new(&name, ident.span()) +} +fn pascal_ident_to_snake_call(ident: &Ident) -> Ident { + let name = ident.to_string(); + let name = cases::snakecase::to_snake_case(&name); + let name = format!("call_{}", name); + Ident::new(&name, ident.span()) +} + +fn format_ty(ty: &Ident) -> String { + if ty == "string" { + format!("{} memory", ty) + } else { + ty.to_string() + } +} + +#[proc_macro_attribute] +pub fn solidity_interface(args: TokenStream, stream: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as AttributeArgs); + let args = solidity_interface::InterfaceInfo::from_list(&args).unwrap(); + + let input: ItemTrait = match syn::parse(stream) { + Ok(t) => t, + Err(e) => return e.to_compile_error().into(), + }; + + match solidity_interface::SolidityInterface::try_from(args, &input) { + Ok(v) => v.expand(), + Err(e) => e.to_compile_error(), + } + .into() +} + +#[proc_macro_attribute] +pub fn solidity(_args: TokenStream, stream: TokenStream) -> TokenStream { + stream +} + +#[proc_macro_derive(ToLog, attributes(indexed))] +pub fn to_log(value: TokenStream) -> TokenStream { + let input = parse_macro_input!(value as DeriveInput); + + match to_log::Events::try_from(&input) { + Ok(e) => e.expand(), + Err(e) => e.to_compile_error(), + } + .into() +} --- /dev/null +++ b/crates/evm-coder-macros/src/solidity_interface.rs @@ -0,0 +1,569 @@ +use quote::quote; +use darling::FromMeta; +use inflector::cases; +use std::fmt::Write; +use syn::{ + FnArg, Ident, ItemTrait, Meta, NestedMeta, PatType, Path, ReturnType, TraitItem, + TraitItemMethod, Visibility, spanned::Spanned, +}; + +use crate::{ + fn_selector_str, format_ty, parse_ident_from_pat, parse_ident_from_path, parse_ident_from_type, + parse_result_ok, pascal_ident_to_call, pascal_ident_to_snake_call, snake_ident_to_pascal, + snake_ident_to_screaming, +}; + +struct Is { + name: Ident, + pascal_call_name: Ident, + snake_call_name: Ident, +} +impl Is { + fn try_from(path: &Path) -> syn::Result { + let name = parse_ident_from_path(path)?.clone(); + Ok(Self { + pascal_call_name: pascal_ident_to_call(&name), + snake_call_name: pascal_ident_to_snake_call(&name), + name, + }) + } + + fn expand_call_def(&self) -> proc_macro2::TokenStream { + let name = &self.name; + let pascal_call_name = &self.pascal_call_name; + quote! { + #name(#pascal_call_name) + } + } + + fn expand_interface_id(&self) -> proc_macro2::TokenStream { + let pascal_call_name = &self.pascal_call_name; + quote! { + interface_id ^= #pascal_call_name::interface_id(); + } + } + + fn expand_supports_interface(&self) -> proc_macro2::TokenStream { + let pascal_call_name = &self.pascal_call_name; + quote! { + #pascal_call_name::supports_interface(interface_id) + } + } + + fn expand_variant_call(&self) -> proc_macro2::TokenStream { + let name = &self.name; + let snake_call_name = &self.snake_call_name; + quote! { + InternalCall::#name(call) => return self.#snake_call_name(Msg { + call, + caller: c.caller, + value: c.value, + }) + } + } + + fn expand_call_inner(&self) -> proc_macro2::TokenStream { + let snake_call_name = &self.snake_call_name; + let pascal_call_name = &self.pascal_call_name; + quote! { + fn #snake_call_name(&mut self, c: Msg<#pascal_call_name>) -> ::core::result::Result<::evm_coder::abi::AbiWriter, Self::Error>; + } + } + + fn expand_parse(&self) -> proc_macro2::TokenStream { + let name = &self.name; + let pascal_call_name = &self.pascal_call_name; + quote! { + if let Some(parsed_call) = #pascal_call_name::parse(method_id, reader)? { + return Ok(Some(Self::#name(parsed_call))) + } + } + } +} + +#[derive(Default)] +struct IsList(Vec); +impl FromMeta for IsList { + fn from_list(items: &[NestedMeta]) -> darling::Result { + let mut out = Vec::new(); + for item in items { + match item { + NestedMeta::Meta(Meta::Path(path)) => out.push(Is::try_from(path)?), + _ => return Err(syn::Error::new(item.span(), "expected path").into()), + } + } + Ok(Self(out)) + } +} + +#[derive(FromMeta)] +pub struct InterfaceInfo { + #[darling(default)] + is: IsList, + #[darling(default)] + inline_is: IsList, + #[darling(default)] + _events: IsList, +} + +#[derive(FromMeta)] +struct MethodInfo { + #[darling(default)] + rename_selector: Option, +} + +struct MethodArg { + name: Ident, + ty: Ident, +} +impl MethodArg { + fn try_from(value: &PatType) -> syn::Result { + Ok(Self { + name: parse_ident_from_pat(&value.pat)?.clone(), + ty: parse_ident_from_type(&value.ty)?.clone(), + }) + } + fn is_value(&self) -> bool { + self.ty == "value" + } + fn is_caller(&self) -> bool { + self.ty == "caller" + } + fn is_special(&self) -> bool { + self.is_value() || self.is_caller() + } + fn selector_ty(&self) -> &Ident { + assert!(!self.is_special()); + &self.ty + } + + fn expand_call_def(&self) -> proc_macro2::TokenStream { + assert!(!self.is_special()); + let name = &self.name; + let ty = &self.ty; + + quote! { + #name: #ty + } + } + + fn expand_parse(&self) -> proc_macro2::TokenStream { + assert!(!self.is_special()); + let name = &self.name; + quote! { + #name: reader.abi_read()? + } + } + + fn expand_call_arg(&self) -> proc_macro2::TokenStream { + if self.is_value() { + quote! { + c.value.clone() + } + } else if self.is_caller() { + quote! { + c.caller.clone() + } + } else { + let name = &self.name; + quote! { + #name + } + } + } + + fn solidity_def(&self) -> String { + assert!(!self.is_special()); + format!("{} {}", format_ty(&self.ty), self.name) + } +} + +#[derive(PartialEq)] +enum Mutability { + Mutable, + View, + Pure, +} + +struct Method { + name: Ident, + camel_name: String, + pascal_name: Ident, + screaming_name: Ident, + selector_str: String, + selector: u32, + args: Vec, + has_normal_args: bool, + mutability: Mutability, + result: Ident, +} +impl Method { + fn try_from(value: &TraitItemMethod) -> syn::Result { + let mut info = MethodInfo { + rename_selector: None, + }; + for attr in &value.attrs { + let ident = parse_ident_from_path(&attr.path)?; + if ident == "solidity" { + let args = attr.parse_meta().unwrap(); + info = MethodInfo::from_meta(&args).unwrap(); + } else if ident == "doc" { + // TODO: Add docs to evm interfaces + } + } + let ident = &value.sig.ident; + let ident_str = ident.to_string(); + if !cases::snakecase::is_snake_case(&ident_str) { + return Err(syn::Error::new(ident.span(), "method name should be snake_cased\nif alternative solidity name needs to be set - use #[solidity] attribute")); + } + + let mut mutability = Mutability::Pure; + + if let Some(FnArg::Receiver(receiver)) = value + .sig + .inputs + .iter() + .find(|arg| matches!(arg, FnArg::Receiver(_))) + { + if receiver.reference.is_none() { + return Err(syn::Error::new( + receiver.span(), + "receiver should be by ref", + )); + } + if receiver.mutability.is_some() { + mutability = Mutability::Mutable; + } else { + mutability = Mutability::View; + } + } + let mut args = Vec::new(); + for typ in value + .sig + .inputs + .iter() + .filter(|arg| matches!(arg, FnArg::Typed(_))) + { + let typ = match typ { + FnArg::Typed(typ) => typ, + _ => unreachable!(), + }; + args.push(MethodArg::try_from(typ)?); + } + + if mutability != Mutability::Mutable && args.iter().any(|arg| arg.is_value()) { + return Err(syn::Error::new( + args.iter().find(|arg| arg.is_value()).unwrap().ty.span(), + "payable function should be mutable", + )); + } + + let result = match &value.sig.output { + ReturnType::Type(_, ty) => ty, + _ => return Err(syn::Error::new(value.sig.output.span(), "interface method should return Result\nif there is no value to return - specify void (which is alias to unit)")), + }; + let result = parse_result_ok(&result)?; + + let camel_name = info + .rename_selector + .unwrap_or_else(|| cases::camelcase::to_camel_case(&ident.to_string())); + let mut selector_str = camel_name.clone(); + selector_str.push('('); + let mut has_normal_args = false; + for (i, arg) in args.iter().filter(|arg| !arg.is_special()).enumerate() { + if i != 0 { + selector_str.push(','); + } + write!(selector_str, "{}", arg.selector_ty()).unwrap(); + has_normal_args = true; + } + selector_str.push(')'); + let selector = fn_selector_str(&selector_str); + + Ok(Self { + name: ident.clone(), + camel_name, + pascal_name: snake_ident_to_pascal(&ident), + screaming_name: snake_ident_to_screaming(&ident), + selector_str, + selector, + args, + has_normal_args, + mutability, + result: result.clone(), + }) + } + fn expand_call_def(&self) -> proc_macro2::TokenStream { + let defs = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(|a| a.expand_call_def()); + let pascal_name = &self.pascal_name; + + if self.has_normal_args { + quote! { + #pascal_name { + #( + #defs, + )* + } + } + } else { + quote! {#pascal_name} + } + } + + fn expand_const(&self) -> proc_macro2::TokenStream { + let screaming_name = &self.screaming_name; + let selector = self.selector; + let selector_str = &self.selector_str; + quote! { + #[doc = #selector_str] + const #screaming_name: u32 = #selector; + } + } + + fn expand_interface_id(&self) -> proc_macro2::TokenStream { + let screaming_name = &self.screaming_name; + quote! { + interface_id ^= Self::#screaming_name; + } + } + + fn expand_parse(&self) -> proc_macro2::TokenStream { + let pascal_name = &self.pascal_name; + let screaming_name = &self.screaming_name; + if self.has_normal_args { + let parsers = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(|a| a.expand_parse()); + quote! { + Self::#screaming_name => return Ok(Some(Self::#pascal_name { + #( + #parsers, + )* + })) + } + } else { + quote! { Self::#screaming_name => return Ok(Some(Self::#pascal_name)) } + } + } + + fn expand_variant_call(&self) -> proc_macro2::TokenStream { + let pascal_name = &self.pascal_name; + let name = &self.name; + + let matcher = if self.has_normal_args { + let names = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(|a| &a.name); + + quote! {{ + #( + #names, + )* + }} + } else { + quote! {} + }; + + let receiver = match self.mutability { + Mutability::Mutable | Mutability::View => quote! {self.}, + Mutability::Pure => quote! {Self::}, + }; + let args = self.args.iter().map(|a| a.expand_call_arg()); + + quote! { + InternalCall::#pascal_name #matcher => { + let result = #receiver #name( + #( + #args, + )* + )?; + (&result).abi_write(&mut writer); + } + } + } + + fn solidity_def(&self) -> String { + let mut out = format!("function {}(", self.camel_name); + for (i, arg) in self.args.iter().filter(|a| !a.is_special()).enumerate() { + if i != 0 { + out.push_str(", "); + } + out.push_str(&arg.solidity_def()); + } + out.push(')'); + match self.mutability { + Mutability::Mutable => {} + Mutability::View => write!(out, " view").unwrap(), + Mutability::Pure => write!(out, " pure").unwrap(), + } + if self.result != "void" { + write!(out, " returns ({})", format_ty(&self.result)).unwrap(); + } + out.push(';'); + out + } +} + +pub struct SolidityInterface { + vis: Visibility, + name: Ident, + info: InterfaceInfo, + methods: Vec, + items: Vec, +} +impl SolidityInterface { + pub fn try_from(info: InterfaceInfo, value: &ItemTrait) -> syn::Result { + let mut found_error = false; + let mut methods = Vec::new(); + + for item in &value.items { + match item { + TraitItem::Type(ty) => { + if ty.ident == "Error" { + found_error = true; + } + } + TraitItem::Method(method) => methods.push(Method::try_from(&method)?), + _ => {} + } + } + if !found_error { + return Err(syn::Error::new( + value.span(), + "expected associated type called Error, which should implement From<&str>", + )); + } + Ok(Self { + vis: value.vis.clone(), + name: value.ident.clone(), + info, + methods, + items: value.items.clone(), + }) + } + pub fn expand(self) -> proc_macro2::TokenStream { + let vis = self.vis; + let name = self.name; + let items = self.items; + + let call_name = pascal_ident_to_call(&name); + + let call_sub = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(Is::expand_call_def); + let call_inner = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(Is::expand_call_inner); + let call_parse = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(Is::expand_parse); + let call_variants = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(Is::expand_variant_call); + + let inline_interface_id = self.info.inline_is.0.iter().map(Is::expand_interface_id); + let supports_interface = self.info.is.0.iter().map(Is::expand_supports_interface); + + let calls = self.methods.iter().map(Method::expand_call_def); + let consts = self.methods.iter().map(Method::expand_const); + let interface_id = self.methods.iter().map(Method::expand_interface_id); + let parsers = self.methods.iter().map(Method::expand_parse); + let call_variants_this = self.methods.iter().map(Method::expand_variant_call); + + // let methods = self.methods.iter().map(Method::solidity_def); + + quote! { + #[derive(Debug)] + #vis enum #call_name { + #( + #calls, + )* + #( + #call_sub, + )* + } + impl #call_name { + #( + #consts + )* + pub fn parse(method_id: u32, reader: &mut ::evm_coder::abi::AbiReader) -> ::evm_coder::abi::Result> { + use ::evm_coder::abi::AbiRead; + match method_id { + #( + #parsers, + )* + _ => {}, + } + #( + #call_parse + )else* + return Ok(None); + } + pub const fn interface_id() -> u32 { + let mut interface_id = 0; + #(#interface_id)* + #(#inline_interface_id)* + interface_id + } + pub fn supports_interface(interface_id: u32) -> bool { + interface_id != 0xffffff && ( + interface_id == Self::interface_id() + #( + || #supports_interface + )* + ) + } + } + #vis trait #name { + #( + #items + )* + #( + #call_inner + )* + fn call(&mut self, c: Msg<#call_name>) -> ::core::result::Result<::evm_coder::abi::AbiWriter, Self::Error> { + use ::evm_coder::abi::AbiWrite; + type InternalCall = #call_name; + match c.call { + #( + #call_variants, + )* + _ => {}, + } + let mut writer = ::evm_coder::abi::AbiWriter::default(); + match c.call { + #( + #call_variants_this, + )* + _ => unreachable!() + } + Ok(writer) + } + } + } + } +} --- /dev/null +++ b/crates/evm-coder-macros/src/to_log.rs @@ -0,0 +1,170 @@ +use syn::{Data, DeriveInput, Field, Fields, Ident, Variant, spanned::Spanned}; +use std::fmt::Write; +use quote::quote; + +use crate::{parse_ident_from_path, parse_ident_from_type, snake_ident_to_screaming}; + +struct EventField { + name: Ident, + ty: Ident, + indexed: bool, +} + +impl EventField { + fn try_from(field: &Field) -> syn::Result { + let name = field.ident.as_ref().unwrap(); + let ty = parse_ident_from_type(&field.ty)?; + let mut indexed = false; + for attr in &field.attrs { + if let Ok(ident) = parse_ident_from_path(&attr.path) { + if ident == "indexed" { + indexed = true; + } + } + } + Ok(Self { + name: name.to_owned(), + ty: ty.to_owned(), + indexed, + }) + } +} + +struct Event { + name: Ident, + name_screaming: Ident, + fields: Vec, + selector: [u8; 32], + selector_str: String, +} + +impl Event { + fn try_from(variant: &Variant) -> syn::Result { + let name = &variant.ident; + let name_screaming = snake_ident_to_screaming(&name); + + let named = match &variant.fields { + Fields::Named(named) => named, + _ => { + return Err(syn::Error::new( + variant.fields.span(), + "expected named fields", + )) + } + }; + let mut fields = Vec::new(); + for field in &named.named { + fields.push(EventField::try_from(&field)?); + } + let mut selector_str = format!("{}(", name); + for (i, arg) in fields.iter().enumerate() { + if i != 0 { + write!(selector_str, ",").unwrap(); + } + write!(selector_str, "{}", arg.ty).unwrap(); + } + selector_str.push(')'); + let selector = crate::event_selector_str(&selector_str); + + Ok(Self { + name: name.to_owned(), + name_screaming, + fields, + selector, + selector_str, + }) + } + + fn expand_serializers(&self) -> proc_macro2::TokenStream { + let name = &self.name; + let name_screaming = &self.name_screaming; + let fields = self.fields.iter().map(|f| &f.name); + + let indexed = self.fields.iter().filter(|f| f.indexed).map(|f| &f.name); + let plain = self.fields.iter().filter(|f| !f.indexed).map(|f| &f.name); + + quote! { + Self::#name {#( + #fields, + )*} => { + topics.push(topic::from(Self::#name_screaming)); + #( + topics.push(#indexed.to_topic()); + )* + #( + #plain.abi_write(&mut writer); + )* + } + } + } + + fn expand_consts(&self) -> proc_macro2::TokenStream { + let name_screaming = &self.name_screaming; + let selector_str = &self.selector_str; + let selector = &self.selector; + + quote! { + #[doc = #selector_str] + const #name_screaming: [u8; 32] = [#( + #selector, + )*]; + } + } +} + +pub struct Events { + name: Ident, + events: Vec, +} + +impl Events { + pub fn try_from(data: &DeriveInput) -> syn::Result { + let name = &data.ident; + let en = match &data.data { + Data::Enum(en) => en, + _ => return Err(syn::Error::new(data.span(), "expected enum")), + }; + let mut events = Vec::new(); + for variant in &en.variants { + events.push(Event::try_from(variant)?); + } + Ok(Self { + name: name.to_owned(), + events, + }) + } + pub fn expand(&self) -> proc_macro2::TokenStream { + let name = &self.name; + + let consts = self.events.iter().map(Event::expand_consts); + let serializers = self.events.iter().map(Event::expand_serializers); + + quote! { + impl #name { + #( + #consts + )* + } + + #[automatically_derived] + impl ::evm_coder::events::ToLog for #name { + fn to_log(&self, contract: address) -> ::ethereum::Log { + use ::evm_coder::events::ToTopic; + use ::evm_coder::abi::AbiWrite; + let mut writer = ::evm_coder::abi::AbiWriter::new(); + let mut topics = Vec::new(); + match self { + #( + #serializers, + )* + } + ::ethereum::Log { + address: contract, + topics, + data: writer.finish(), + } + } + } + } + } +}