diff --git a/ethereum-explorer/Cargo.lock b/ethereum-explorer/Cargo.lock index a82d03e..5572aa3 100644 --- a/ethereum-explorer/Cargo.lock +++ b/ethereum-explorer/Cargo.lock @@ -508,6 +508,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + [[package]] name = "petgraph" version = "0.6.3" @@ -770,6 +776,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "sha3" version = "0.10.8" @@ -879,6 +896,8 @@ dependencies = [ "hex-literal", "num-bigint", "prost", + "serde", + "serde_qs", "substreams", "substreams-ethereum", ] diff --git a/ethereum-explorer/Cargo.toml b/ethereum-explorer/Cargo.toml index 020e14c..c66fb83 100644 --- a/ethereum-explorer/Cargo.toml +++ b/ethereum-explorer/Cargo.toml @@ -19,6 +19,8 @@ prost = "0.11" substreams = "0.5" # Use latest from https://crates.io/crates/substreams-ethereum substreams-ethereum = "0.9" +serde_qs = "0.12.0" +serde = { version = "1.0", features = ["derive"] } # Required so that ethabi > ethereum-types build correctly under wasm32-unknown-unknown [target.wasm32-unknown-unknown.dependencies] diff --git a/ethereum-explorer/Makefile b/ethereum-explorer/Makefile index c97e5d6..0edf461 100644 --- a/ethereum-explorer/Makefile +++ b/ethereum-explorer/Makefile @@ -6,6 +6,6 @@ build: protogen: substreams protogen ./substreams.yaml --exclude-paths="sf/substreams,google" -.PHONY: stream +.PHONY: package package: build - substreams package substreams.yaml + substreams pack substreams.yaml diff --git a/ethereum-explorer/build.sh b/ethereum-explorer/build.sh deleted file mode 100755 index 1cdc395..0000000 --- a/ethereum-explorer/build.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -cargo build --target wasm32-unknown-unknown --release diff --git a/ethereum-explorer/proto/transaction.proto b/ethereum-explorer/proto/transaction.proto index 3b7ecbd..9bdf1b0 100644 --- a/ethereum-explorer/proto/transaction.proto +++ b/ethereum-explorer/proto/transaction.proto @@ -2,8 +2,8 @@ syntax = "proto3"; package eth.transaction.v1; -message TransactionOption { - Transaction transaction = 1; +message Transactions { + repeated Transaction transactions = 1; } message Transaction { diff --git a/ethereum-explorer/rust-toolchain.toml b/ethereum-explorer/rust-toolchain.toml index ec334c0..e892501 100644 --- a/ethereum-explorer/rust-toolchain.toml +++ b/ethereum-explorer/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.65" +channel = "1.69" components = [ "rustfmt" ] targets = [ "wasm32-unknown-unknown" ] \ No newline at end of file diff --git a/ethereum-explorer/src/lib.rs b/ethereum-explorer/src/lib.rs index 70d6aed..b1e3689 100644 --- a/ethereum-explorer/src/lib.rs +++ b/ethereum-explorer/src/lib.rs @@ -1,40 +1,7 @@ mod pb; - -#[path = "map_block_meta.rs"] -mod block_meta; - -#[path = "map_filter_transaction.rs"] -mod filter_transaction; - -#[path = "map_contract_events.rs"] -mod contract_events; - - -use pb::eth::transaction::v1::TransactionOption; -use substreams_ethereum::pb::eth::v2::Block; -use pb::eth::block_meta::v1::BlockMeta; -use crate::pb::eth::event::v1::Events; +mod map_block_meta; +mod map_filter_transactions; +mod map_contract_events; +mod util; substreams_ethereum::init!(); - -#[substreams::handlers::map] -fn map_block_meta(blk: Block) -> Result { - let block_meta = block_meta::map_block_meta(&blk); - - Ok(block_meta) -} - -#[substreams::handlers::map] -pub fn map_filter_transaction(transaction_hash: String, blk: Block) -> Result { - let filtered_transaction = filter_transaction::filter_by_transaction_hash(transaction_hash, &blk); - - Ok(filtered_transaction) -} - -#[substreams::handlers::map] -fn map_contract_events(contract_address: String, blk: Block) -> Result { - let events: Events = contract_events::map_contract_events(contract_address, &blk); - - Ok(events) -} - diff --git a/ethereum-explorer/src/map_block_meta.rs b/ethereum-explorer/src/map_block_meta.rs index 41cbb74..e39c183 100644 --- a/ethereum-explorer/src/map_block_meta.rs +++ b/ethereum-explorer/src/map_block_meta.rs @@ -2,15 +2,16 @@ use substreams_ethereum::pb::eth::v2::Block; use crate::pb::eth::block_meta::v1::BlockMeta; use substreams::Hex; -pub fn map_block_meta (blk: &Block) -> BlockMeta { +#[substreams::handlers::map] +fn map_block_meta(blk: Block) -> Result { let header = blk.header.as_ref().unwrap(); let hash_string = Hex(&blk.hash).to_string(); let parent_hash_string = Hex(&header.parent_hash).to_string(); - return BlockMeta { + Ok(BlockMeta { number: blk.number, hash: hash_string, parent_hash: parent_hash_string - } + }) } \ No newline at end of file diff --git a/ethereum-explorer/src/map_contract_events.rs b/ethereum-explorer/src/map_contract_events.rs index fc427a6..3d4bfc9 100644 --- a/ethereum-explorer/src/map_contract_events.rs +++ b/ethereum-explorer/src/map_contract_events.rs @@ -1,34 +1,52 @@ -mod pb; use crate::pb::eth::event::v1::Events; use crate::pb::eth::event::v1::Event; use substreams_ethereum::pb::eth::v2::Block; use substreams::Hex; use substreams_ethereum::pb::eth::v2::Log; use substreams_ethereum::pb::eth::v2::TransactionTrace; +use substreams::errors::Error; +use crate::util; + +#[substreams::handlers::map] +fn map_contract_events(contract_address: String, blk: Block) -> Result { + let error = verify_parameter(&contract_address); + if error.is_some() { + return Err(error.unwrap()); + } -pub fn map_contract_events(contract_address: String, blk: &Block) -> Events { let mut events: Vec = Vec::new(); + let contract_address_as_vec = match Hex::decode(&contract_address) { + Ok(address) => address, + Err(error) => return Err(Error::Unexpected(error.to_string())), + }; - for tr in &blk.transaction_traces { - let to = Hex(&tr.to).to_string(); - - if to == contract_address { - let transaction_events = &mut get_transaction_events(&tr); - events.append(transaction_events); + for transaction in &blk.transaction_traces { + if transaction.to == contract_address_as_vec { + let transaction_events = get_transaction_events(&transaction); + events.extend(transaction_events); } } - return Events { events } + Ok(Events { events }) +} + +fn verify_parameter(contract_address: &String) -> Option { + if !util::is_address_valid(contract_address) { + return Some(Error::Unexpected(String::from("Contract address is not valid"))) + } + + return None } fn get_transaction_events(transaction: &TransactionTrace) -> Vec { let mut transaction_events: Vec = Vec::new(); for log in &transaction.receipt().receipt.logs { - let address = Hex(&log.address).to_string(); + let address = util::hexadecimal_to_string(&log.address); let topics = get_log_topics(&log); - transaction_events.push(create_event_from(address, topics, &transaction.hash)) + let event = create_event_from(address, topics, util::hexadecimal_to_string(&transaction.hash)); + transaction_events.push(event) } return transaction_events; @@ -38,19 +56,18 @@ fn get_log_topics(log: &Log) -> Vec { let mut topics: Vec = Vec::new(); for topic in &log.topics { - let topic_string = Hex(topic).to_string(); + let topic_string = util::hexadecimal_to_string(topic); topics.push(topic_string) } return topics; } -fn create_event_from(address: String, topics: Vec, hash: &Vec) -> Event { - let hash_as_string = Hex(hash).to_string(); +fn create_event_from(address: String, topics: Vec, hash: String) -> Event { return Event { address, topics, - tx_hash: hash_as_string + tx_hash: hash } } \ No newline at end of file diff --git a/ethereum-explorer/src/map_filter_transaction.rs b/ethereum-explorer/src/map_filter_transaction.rs deleted file mode 100644 index 75e89d9..0000000 --- a/ethereum-explorer/src/map_filter_transaction.rs +++ /dev/null @@ -1,32 +0,0 @@ -mod pb; -use substreams_ethereum::pb::eth::v2::Block; -use substreams::Hex; -use crate::pb::eth::transaction::v1::TransactionOption; -use crate::pb::eth::transaction::v1::Transaction; - - -pub fn filter_by_transaction_hash(transaction_hash: String, blk: &Block) -> TransactionOption { - let transaction_traces = &blk.transaction_traces; - - for transfer in transaction_traces { - //let transferValue = transfer; - let hash = &transfer.hash; - let from = &transfer.from; - let to = &transfer.to; - - if Hex(hash).to_string() == transaction_hash { - let trans = Transaction { from: Hex(from).to_string(), to: Hex(to).to_string(), hash: Hex(hash).to_string() }; - return transfer_option_of(Some(trans)); - } - } - - return empty_transfer_option() -} - -fn transfer_option_of (transaction: Option) -> TransactionOption { - return TransactionOption { transaction } -} - -fn empty_transfer_option() -> TransactionOption { - return transfer_option_of(None) -} \ No newline at end of file diff --git a/ethereum-explorer/src/map_filter_transactions.rs b/ethereum-explorer/src/map_filter_transactions.rs new file mode 100644 index 0000000..f2e53b2 --- /dev/null +++ b/ethereum-explorer/src/map_filter_transactions.rs @@ -0,0 +1,77 @@ +use substreams_ethereum::pb::eth::v2::{Block, TransactionTraceStatus}; +use crate::pb::eth::transaction::v1::{Transaction, Transactions}; +use serde::Deserialize; +use substreams::errors::Error; +use crate::util; + +#[derive(Deserialize)] +struct TransactionFilterParams { + hash: Option, + to: Option, + from: Option +} + +#[substreams::handlers::map] +pub fn map_filter_transactions(params: String, blk: Block) -> Result> { + let filters: TransactionFilterParams = serde_qs::from_str(¶ms).unwrap(); + let errors = verify_filter_params(&filters); + if errors.len() > 0 { + return Err(errors) + } + + let mut filtered_transactions: Vec = Vec::new(); + + for transaction in &blk.transaction_traces { + let tx_hash = util::hexadecimal_to_string(&transaction.hash); + let tx_from = util::hexadecimal_to_string(&transaction.from); + let tx_to = util::hexadecimal_to_string(&transaction.to); + let mut current_transaction_filtered = true; + + if !filter_by_parameter(&filters.hash, &tx_hash) || + !filter_by_parameter(&filters.from, &tx_from) || + !filter_by_parameter(&filters.to, &tx_to) || + transaction.status != (TransactionTraceStatus::Succeeded as i32) { + current_transaction_filtered = false + } + + if current_transaction_filtered { + let trans = Transaction { from: tx_from, to: tx_to, hash: tx_hash }; + filtered_transactions.push(trans) + } + } + + Ok(Transactions { transactions: filtered_transactions }) +} + +fn verify_filter_params(params: &TransactionFilterParams) -> Vec { + let mut errors: Vec = Vec::new(); + + if params.hash.is_some() + && !util::is_transaction_hash_valid(¶ms.hash.as_ref().unwrap()) { + errors.push(Error::Unexpected(String::from("Transaction hash is not valid"))); + } + + if params.from.is_some() + && !util::is_address_valid(¶ms.from.as_ref().unwrap()) { + errors.push(Error::Unexpected(String::from("'from' address is not valid"))); + } + + if params.to.is_some() + && !util::is_address_valid(¶ms.to.as_ref().unwrap()) { + errors.push(Error::Unexpected(String::from("'to' address is not valid"))); + } + + return errors; +} + +fn filter_by_parameter(parameter: &Option, transaction_field: &String) -> bool { + if parameter.is_none() { + return true; + } + + if parameter.as_ref().unwrap() == transaction_field { + return true + } + + return false; +} \ No newline at end of file diff --git a/ethereum-explorer/src/pb/eth.transaction.v1.rs b/ethereum-explorer/src/pb/eth.transaction.v1.rs index 28b25b2..f299d0f 100644 --- a/ethereum-explorer/src/pb/eth.transaction.v1.rs +++ b/ethereum-explorer/src/pb/eth.transaction.v1.rs @@ -1,9 +1,9 @@ // @generated #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct TransactionOption { - #[prost(message, optional, tag="1")] - pub transaction: ::core::option::Option, +pub struct Transactions { + #[prost(message, repeated, tag="1")] + pub transactions: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/ethereum-explorer/src/util.rs b/ethereum-explorer/src/util.rs new file mode 100644 index 0000000..42e4997 --- /dev/null +++ b/ethereum-explorer/src/util.rs @@ -0,0 +1,23 @@ +use substreams::Hex; + +pub fn hexadecimal_to_string(hex: &Vec) -> String { + return Hex::encode(hex); +} + +pub fn is_transaction_hash_valid(hash: &String) -> bool { + // A transaction hash is always 64 hexadecimal characters + if hash.len() != 64 { + return false; + } + + return true; +} + +pub fn is_address_valid(address: &String) -> bool { + // An address is always 40 hexadecimal characters + if address.len() != 40 { + return false; + } + + return true; +} diff --git a/ethereum-explorer/substreams.yaml b/ethereum-explorer/substreams.yaml index 21e6fe9..51c6f6a 100644 --- a/ethereum-explorer/substreams.yaml +++ b/ethereum-explorer/substreams.yaml @@ -23,13 +23,13 @@ modules: - source: sf.ethereum.type.v2.Block output: type: proto:eth.block_meta.v1.BlockMeta - - name: map_filter_transaction + - name: map_filter_transactions kind: map inputs: - params: string - source: sf.ethereum.type.v2.Block output: - type: proto:eth.transaction.v1.TransactionOption + type: proto:eth.transaction.v1.Transactions - name: map_contract_events kind: map inputs: @@ -39,5 +39,5 @@ modules: type: proto:eth.event.v1.Events params: - map_filter_transaction: "4faa877df84080a9d98b1e28294c4680bb141ec27a1a5dee009c3e02dfa65ab7" + map_filter_transactions: "to=dac17f958d2ee523a2206206994597c13d831ec7&from=46340b20830761efd32832a74d7169b29feb9758" map_contract_events: "bc4ca0eda7647a8ab7c2061c2e118a18a936f13d"