diff --git a/Cargo.lock b/Cargo.lock index ea3e2cb94..98ff9d04b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,7 @@ dependencies = [ "rustc-hex 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.78 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.78 (registry+https://github.com/rust-lang/crates.io-index)", + "tiny-keccak 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/README.md b/README.md index e5efcb2d1..067d31fef 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,11 @@ This library encodes function calls and decodes their output. [Documentation](https://docs.rs/ethabi) +### Disclaimer + +This library intends to support only valid ABIs generated by recent Solidity versions. Specifically, we don't intend to support ABIs that are invalid due to known Solidity bugs or by external libraries that don't strictly follow the specification. +Please make sure to pre-process your ABI in case it's not supported before using it with `ethabi`. + ### Installation - via cargo diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 66d4a6515..5ec8bff28 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -14,6 +14,7 @@ serde_derive = "1.0" docopt = "1.0" ethabi = { version = "8.0.0", path = "../ethabi" } error-chain = { version = "0.12.1", default-features = false } +tiny-keccak = "1.0" [features] backtrace = ["error-chain/backtrace"] diff --git a/cli/src/error.rs b/cli/src/error.rs index 432f85830..a640da013 100644 --- a/cli/src/error.rs +++ b/cli/src/error.rs @@ -2,6 +2,7 @@ use std::io; use {ethabi, docopt, hex}; +use ethabi::Hash; error_chain! { links { @@ -13,4 +14,16 @@ error_chain! { Docopt(docopt::Error); Hex(hex::FromHexError); } + + errors { + InvalidSignature(signature: Hash) { + description("Invalid signature"), + display("Invalid signature `{}`", signature), + } + + AmbiguousEventName(name: String) { + description("More than one event found for name, try providing the full signature"), + display("Ambiguous event name `{}`", name), + } + } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 62071f111..7835c1630 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -6,6 +6,7 @@ extern crate serde_derive; #[macro_use] extern crate error_chain; extern crate ethabi; +extern crate tiny_keccak; mod error; @@ -16,7 +17,8 @@ use hex::{ToHex, FromHex}; use ethabi::param_type::{ParamType, Reader}; use ethabi::token::{Token, Tokenizer, StrictTokenizer, LenientTokenizer}; use ethabi::{encode, decode, Contract, Function, Event, Hash}; -use error::{Error, ResultExt}; +use error::{Error, ErrorKind, ResultExt}; +use tiny_keccak::Keccak; pub const ETHABI: &str = r#" Ethereum ABI coder. @@ -27,7 +29,7 @@ Usage: ethabi encode params [-v ]... [-l | --lenient] ethabi decode function ethabi decode params [-t ]... - ethabi decode log [-l ]... + ethabi decode log [-l ]... ethabi -h | --help Options: @@ -51,7 +53,7 @@ struct Args { cmd_log: bool, arg_abi_path: String, arg_function_name: String, - arg_event_name: String, + arg_event_name_or_signature: String, arg_param: Vec, arg_type: Vec, arg_data: String, @@ -89,7 +91,7 @@ fn execute(command: I) -> Result where I: IntoIterator Result { Ok(function) } -fn load_event(path: &str, event: &str) -> Result { +fn load_event(path: &str, name_or_signature: &str) -> Result { let file = File::open(path)?; let contract = Contract::load(file)?; - let event = contract.event(event)?.clone(); - Ok(event) + let params_start = name_or_signature.find('('); + + match params_start { + // It's a signature. + Some(params_start) => { + let name = &name_or_signature[..params_start]; + let signature = hash_signature(name_or_signature); + contract.events_by_name(name)?.iter().find(|event| + event.signature() == signature + ).cloned().ok_or(ErrorKind::InvalidSignature(signature).into()) + } + + // It's a name. + None => { + let events = contract.events_by_name(name_or_signature)?; + match events.len() { + 0 => unreachable!(), + 1 => Ok(events[0].clone()), + _ => Err(ErrorKind::AmbiguousEventName(name_or_signature.to_owned()).into()) + } + } + } } fn parse_tokens(params: &[(ParamType, &str)], lenient: bool) -> Result, Error> { @@ -187,8 +209,8 @@ fn decode_params(types: &[String], data: &str) -> Result { Ok(result) } -fn decode_log(path: &str, event: &str, topics: &[String], data: &str) -> Result { - let event = load_event(path, event)?; +fn decode_log(path: &str, name_or_signature: &str, topics: &[String], data: &str) -> Result { + let event = load_event(path, name_or_signature)?; let topics: Vec = topics.into_iter() .map(|t| t.parse() ) .collect::>()?; @@ -203,6 +225,16 @@ fn decode_log(path: &str, event: &str, topics: &[String], data: &str) -> Result< Ok(result) } + +fn hash_signature(sig: &str) -> Hash { + let mut result = [0u8; 32]; + let data = sig.replace(" ", "").into_bytes(); + let mut sponge = Keccak::new_keccak256(); + sponge.update(&data); + sponge.finalize(&mut result); + Hash::from_slice(&result) +} + #[cfg(test)] mod tests { use super::execute; @@ -287,6 +319,15 @@ bool false"; let command = "ethabi decode log ../res/event.abi Event -l 0000000000000000000000000000000000000000000000000000000000000001 0000000000000000000000004444444444444444444444444444444444444444".split(" "); let expected = "a true +b 4444444444444444444444444444444444444444"; + assert_eq!(execute(command).unwrap(), expected); + } + + #[test] + fn log_decode_signature() { + let command = "ethabi decode log ../res/event.abi Event(bool,address) -l 0000000000000000000000000000000000000000000000000000000000000001 0000000000000000000000004444444444444444444444444444444444444444".split(" "); + let expected = +"a true b 4444444444444444444444444444444444444444"; assert_eq!(execute(command).unwrap(), expected); } diff --git a/ethabi/src/contract.rs b/ethabi/src/contract.rs index 253f025a2..7c791c5ad 100644 --- a/ethabi/src/contract.rs +++ b/ethabi/src/contract.rs @@ -1,6 +1,7 @@ use std::{io, fmt}; use std::collections::HashMap; use std::collections::hash_map::Values; +use std::iter::Flatten; use serde::{Deserialize, Deserializer}; use serde::de::{Visitor, SeqAccess}; use serde_json; @@ -14,8 +15,8 @@ pub struct Contract { pub constructor: Option, /// Contract functions. pub functions: HashMap, - /// Contract events. - pub events: HashMap, + /// Contract events, maps signature to event. + pub events: HashMap>, /// Contract has fallback function. pub fallback: bool, } @@ -52,7 +53,7 @@ impl<'a> Visitor<'a> for ContractVisitor { result.functions.insert(func.name.clone(), func); }, Operation::Event(event) => { - result.events.insert(event.name.clone(), event); + result.events.entry(event.name.clone()).or_default().push(event); }, Operation::Fallback => { result.fallback = true; @@ -80,9 +81,18 @@ impl Contract { self.functions.get(name).ok_or_else(|| ErrorKind::InvalidName(name.to_owned()).into()) } - /// Creates event decoder. + /// Get the contract event named `name`, the first if there are multiple. pub fn event(&self, name: &str) -> errors::Result<&Event> { - self.events.get(name).ok_or_else(|| ErrorKind::InvalidName(name.to_owned()).into()) + self.events.get(name).into_iter() + .flatten() + .next() + .ok_or_else(|| ErrorKind::InvalidName(name.to_owned()).into()) + } + + /// Get all contract events named `name`. + pub fn events_by_name(&self, name: &str) -> errors::Result<&Vec> { + self.events.get(name) + .ok_or_else(|| ErrorKind::InvalidName(name.to_owned()).into()) } /// Iterate over all functions of the contract in arbitrary order. @@ -92,7 +102,7 @@ impl Contract { /// Iterate over all events of the contract in arbitrary order. pub fn events(&self) -> Events { - Events(self.events.values()) + Events(self.events.values().flatten()) } /// Returns true if contract has fallback @@ -113,7 +123,7 @@ impl<'a> Iterator for Functions<'a> { } /// Contract events interator. -pub struct Events<'a>(Values<'a, String, Event>); +pub struct Events<'a>(Flatten>>); impl<'a> Iterator for Events<'a> { type Item = &'a Event; diff --git a/ethabi/src/event.rs b/ethabi/src/event.rs index 8d22e8b73..4d1109e43 100644 --- a/ethabi/src/event.rs +++ b/ethabi/src/event.rs @@ -103,6 +103,20 @@ impl Event { Ok(result) } + // Converts param types for indexed parameters to bytes32 where appropriate + // This applies to strings, arrays and bytes to follow the encoding of + // these indexed param types according to + // https://solidity.readthedocs.io/en/develop/abi-spec.html#encoding-of-indexed-event-parameters + fn convert_topic_param_type(&self, kind: &ParamType) -> ParamType { + match kind { + ParamType::String + | ParamType::Bytes + | ParamType::Array(_) + | ParamType::FixedArray(_, _) => ParamType::FixedBytes(32), + _ => kind.clone() + } + } + /// Parses `RawLog` and retrieves all log params from it. pub fn parse_log(&self, log: RawLog) -> Result { let topics = log.topics; @@ -124,7 +138,7 @@ impl Event { }; let topic_types = topic_params.iter() - .map(|p| p.kind.clone()) + .map(|p| self.convert_topic_param_type(&p.kind)) .collect::>(); let flat_topics = topics.into_iter() @@ -201,15 +215,38 @@ mod tests { name: "d".to_owned(), kind: ParamType::Address, indexed: true, + }, EventParam { + name: "e".to_owned(), + kind: ParamType::String, + indexed: true, + }, EventParam { + name: "f".to_owned(), + kind: ParamType::Array(Box::new(ParamType::Int(256))), + indexed: true + }, EventParam { + name: "g".to_owned(), + kind: ParamType::FixedArray(Box::new(ParamType::Address), 5), + indexed: true, }], anonymous: false, }; let log = RawLog { topics: vec![ - long_signature("foo", &[ParamType::Int(256), ParamType::Int(256), ParamType::Address, ParamType::Address]), + long_signature("foo", &[ + ParamType::Int(256), + ParamType::Int(256), + ParamType::Address, + ParamType::Address, + ParamType::String, + ParamType::Array(Box::new(ParamType::Int(256))), + ParamType::FixedArray(Box::new(ParamType::Address), 5), + ]), "0000000000000000000000000000000000000000000000000000000000000002".parse().unwrap(), "0000000000000000000000001111111111111111111111111111111111111111".parse().unwrap(), + "00000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + "00000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".parse().unwrap(), + "00000000000000000ccccccccccccccccccccccccccccccccccccccccccccccc".parse().unwrap(), ], data: ("".to_owned() + @@ -223,6 +260,9 @@ mod tests { ("b".to_owned(), Token::Int("0000000000000000000000000000000000000000000000000000000000000002".into())), ("c".to_owned(), Token::Address("2222222222222222222222222222222222222222".parse().unwrap())), ("d".to_owned(), Token::Address("1111111111111111111111111111111111111111".parse().unwrap())), + ("e".to_owned(), Token::FixedBytes("00000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".from_hex().unwrap())), + ("f".to_owned(), Token::FixedBytes("00000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".from_hex().unwrap())), + ("g".to_owned(), Token::FixedBytes("00000000000000000ccccccccccccccccccccccccccccccccccccccccccccccc".from_hex().unwrap())), ].into_iter().map(|(name, value)| LogParam { name, value }).collect::>()}); } }