From 91c18041e77dbbf9f2f017baa1f20fbecac1749f Mon Sep 17 00:00:00 2001 From: zolting <44510235+zolting@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:19:58 -0500 Subject: [PATCH 01/20] Kickstart evm parquet --- blocks/evm/Cargo.toml | 2 + blocks/evm/Makefile | 40 +--- blocks/evm/src/blocks.rs | 56 +++++- blocks/evm/src/lib.rs | 5 +- blocks/evm/src/pb/evm.rs | 403 +++++++++++++++++++++++++++++++++++++ blocks/evm/src/pb/mod.rs | 6 + blocks/evm/src/sinks.rs | 36 +++- blocks/evm/src/size.rs | 2 +- blocks/evm/substreams.yaml | 26 +-- common/src/utils.rs | 7 + proto/evm.proto | 218 ++++++++++++++++++++ 11 files changed, 737 insertions(+), 64 deletions(-) create mode 100644 blocks/evm/src/pb/evm.rs create mode 100644 blocks/evm/src/pb/mod.rs create mode 100644 proto/evm.proto diff --git a/blocks/evm/Cargo.toml b/blocks/evm/Cargo.toml index 7376bd3..6e07691 100644 --- a/blocks/evm/Cargo.toml +++ b/blocks/evm/Cargo.toml @@ -11,3 +11,5 @@ common = { path = "../../common" } substreams-ethereum = { workspace = true } substreams-database-change = { workspace = true } substreams = { workspace = true } +prost-types = { workspace = true } +prost = { workspace = true } diff --git a/blocks/evm/Makefile b/blocks/evm/Makefile index a836b5b..6d35d20 100644 --- a/blocks/evm/Makefile +++ b/blocks/evm/Makefile @@ -29,39 +29,9 @@ run: gui: substreams gui -e eth.substreams.pinax.network:443 ch_out -s 20500000 -t 20501001 --production-mode -.PHONY: sql-setup -sql-setup: - # EVM blocks - substreams-sink-sql setup clickhouse://default:default@localhost:9000/eth substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/ethtest substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/eth substreams.yaml --cursors-table cursors1 - substreams-sink-sql setup clickhouse://default:default@localhost:9000/eth substreams.yaml --cursors-table cursors2 - substreams-sink-sql setup clickhouse://default:default@localhost:9000/eth substreams.yaml --cursors-table cursors3 - substreams-sink-sql setup clickhouse://default:default@localhost:9000/base substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/bsc substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/polygon substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/arbone substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/mode substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/zora substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/xai substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/linea substreams.yaml - - # testnets - substreams-sink-sql setup clickhouse://default:default@localhost:9000/arbsepolia substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/sepolia substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/chapel substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/holesky substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/amoy substreams.yaml - - # not detailed EVM blocks - substreams-sink-sql setup clickhouse://default:default@localhost:9000/avalanche substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/optimism substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/blast substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/boba substreams.yaml - substreams-sink-sql setup clickhouse://default:default@localhost:9000/fantom substreams.yaml - - # not compatible with EVM blocks - substreams-sink-sql setup clickhouse://default:default@localhost:9000/injective substreams.yaml +.PHONY: protogen +protogen: + substreams protogen --exclude-paths sf/substreams,google # EVM blocks .PHONY: sql-run-eth @@ -164,6 +134,10 @@ sql-run-injective: sql-run-fantom: substreams-sink-sql run clickhouse://default:default@localhost:9000/fantom substreams.yaml -e fantom.substreams.pinax.network:443 87606376:87714334 --final-blocks-only --undo-buffer-size 1 --on-module-hash-mistmatch=warn --batch-block-flush-interval 100 --development-mode +.PHONY: parquet +parquet: + substreams-sink-files run eth.substreams.pinax.network:443 substreams.yaml map_events './out' 20444295:20444795 --encoder parquet --file-block-count 100 --development-mode + .PHONY: deploy deploy: graph build diff --git a/blocks/evm/src/blocks.rs b/blocks/evm/src/blocks.rs index 233d10e..1794a3c 100644 --- a/blocks/evm/src/blocks.rs +++ b/blocks/evm/src/blocks.rs @@ -1,5 +1,8 @@ +use std::vec; + use common::blocks::insert_timestamp; -use common::utils::{bytes_to_hex, optional_u64_to_string}; +use common::structs::BlockTimestamp; +use common::utils::{bytes_to_hex, optional_bigint_to_u64, optional_u64_to_string}; use common::{keys::blocks_keys, utils::optional_bigint_to_string}; use substreams::pb::substreams::Clock; use substreams_database_change::pb::database::{table_change, DatabaseChanges}; @@ -7,6 +10,7 @@ use substreams_ethereum::pb::eth::v2::Block; use crate::balance_changes::insert_balance_change; use crate::code_changes::insert_code_change; +use crate::pb::evm::Block as RawBlock; use crate::size::insert_size; use crate::traces::insert_system_trace; use crate::transactions::insert_transaction; @@ -79,11 +83,9 @@ pub fn insert_blocks(tables: &mut DatabaseChanges, clock: &Clock, block: &Block) .change("parent_beacon_root", ("", parent_beacon_root.as_str())) .change("excess_blob_gas", ("", excess_blob_gas.as_str())) .change("blob_gas_used", ("", blob_gas_used.as_str())) - // block detail levels .change("detail_level", ("", detail_level.as_str())) - .change("detail_level_code", ("", detail_level_code.to_string().as_str())) - ; + .change("detail_level_code", ("", detail_level_code.to_string().as_str())); insert_timestamp(row, clock, true, true); insert_size(row, &block); @@ -102,6 +104,50 @@ pub fn insert_blocks(tables: &mut DatabaseChanges, clock: &Clock, block: &Block) } // TABLE::transactions for transaction in block.transaction_traces.iter() { - insert_transaction( tables, clock, &transaction, &header, &detail_level); + insert_transaction(tables, clock, &transaction, &header, &detail_level); + } +} + +pub fn collect_block(block: &Block, timestamp: &BlockTimestamp) -> RawBlock { + let header = block.header.as_ref().unwrap(); + + let total_transactions = block.transaction_traces.len() as u64; + let successful_transactions = block.transaction_traces.iter().filter(|t| t.status == 1).count() as u64; + let failed_transactions = total_transactions - successful_transactions; + let total_withdrawals = block.balance_changes.iter().filter(|t| t.reason == 16).count() as u64; + + RawBlock { + time: Some(timestamp.time), + number: header.number, + date: timestamp.date.clone(), + hash: bytes_to_hex(&block.hash), + parent_hash: bytes_to_hex(&header.parent_hash), + nonce: header.nonce, + ommers_hash: bytes_to_hex(&header.uncle_hash), + logs_bloom: bytes_to_hex(&header.logs_bloom), + transactions_root: bytes_to_hex(&header.transactions_root), + state_root: bytes_to_hex(&header.state_root), + receipts_root: bytes_to_hex(&header.receipt_root), + withdrawals_root: bytes_to_hex(&header.withdrawals_root), + parent_beacon_root: bytes_to_hex(&header.parent_beacon_root), + miner: bytes_to_hex(&header.coinbase), + difficulty: optional_bigint_to_u64(&header.difficulty), + total_difficulty: optional_bigint_to_string(&header.total_difficulty, "0"), + mix_hash: bytes_to_hex(&header.mix_hash), + extra_data: bytes_to_hex(&header.extra_data), + extra_data_utf8: String::from_utf8(header.extra_data.clone()).unwrap_or_default(), + gas_limit: header.gas_limit, + gas_used: header.gas_used, + base_fee_per_gas: optional_bigint_to_string(&header.base_fee_per_gas, ""), + blob_gas_used: optional_u64_to_string(&header.blob_gas_used, ""), + excess_blob_gas: optional_u64_to_string(&header.excess_blob_gas, ""), + size: block.size, + total_transactions: block.transaction_traces.len() as u64, + successful_transactions, + failed_transactions, + total_balance_changes: block.balance_changes.len() as u64, + total_withdrawals, + detail_level: block_detail_to_string(block.detail_level), + detail_level_code: block.detail_level as u32, } } diff --git a/blocks/evm/src/lib.rs b/blocks/evm/src/lib.rs index 2e6711a..9145fd0 100644 --- a/blocks/evm/src/lib.rs +++ b/blocks/evm/src/lib.rs @@ -3,11 +3,12 @@ mod balance_changes; mod blocks; mod code_changes; mod gas_changes; +mod keys; mod logs; mod nonce_changes; +mod pb; mod sinks; +mod size; mod storage_changes; mod traces; mod transactions; -mod size; -mod keys; \ No newline at end of file diff --git a/blocks/evm/src/pb/evm.rs b/blocks/evm/src/pb/evm.rs new file mode 100644 index 0000000..595f645 --- /dev/null +++ b/blocks/evm/src/pb/evm.rs @@ -0,0 +1,403 @@ +// @generated +// This file is @generated by prost-build. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Events { + #[prost(message, repeated, tag = "1")] + pub blocks: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "2")] + pub transactions: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "3")] + pub logs: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "4")] + pub traces: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "5")] + pub balance_changes: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "6")] + pub storage_changes: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "7")] + pub code_changes: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "8")] + pub account_creations: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "9")] + pub nonce_changes: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "10")] + pub gas_changes: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Block { + #[prost(message, optional, tag = "1")] + pub time: ::core::option::Option<::prost_types::Timestamp>, + #[prost(uint64, tag = "2")] + pub number: u64, + #[prost(string, tag = "3")] + pub date: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub hash: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub parent_hash: ::prost::alloc::string::String, + #[prost(uint64, tag = "6")] + pub nonce: u64, + #[prost(string, tag = "7")] + pub ommers_hash: ::prost::alloc::string::String, + #[prost(string, tag = "8")] + pub logs_bloom: ::prost::alloc::string::String, + #[prost(string, tag = "9")] + pub transactions_root: ::prost::alloc::string::String, + #[prost(string, tag = "10")] + pub state_root: ::prost::alloc::string::String, + #[prost(string, tag = "11")] + pub receipts_root: ::prost::alloc::string::String, + #[prost(string, tag = "12")] + pub withdrawals_root: ::prost::alloc::string::String, + #[prost(string, tag = "13")] + pub parent_beacon_root: ::prost::alloc::string::String, + #[prost(string, tag = "14")] + pub miner: ::prost::alloc::string::String, + #[prost(uint64, tag = "15")] + pub difficulty: u64, + #[prost(string, tag = "16")] + pub total_difficulty: ::prost::alloc::string::String, + #[prost(string, tag = "17")] + pub mix_hash: ::prost::alloc::string::String, + #[prost(string, tag = "18")] + pub extra_data: ::prost::alloc::string::String, + #[prost(string, tag = "19")] + pub extra_data_utf8: ::prost::alloc::string::String, + #[prost(uint64, tag = "20")] + pub gas_limit: u64, + #[prost(uint64, tag = "21")] + pub gas_used: u64, + #[prost(string, tag = "22")] + pub base_fee_per_gas: ::prost::alloc::string::String, + #[prost(string, tag = "23")] + pub blob_gas_used: ::prost::alloc::string::String, + #[prost(string, tag = "24")] + pub excess_blob_gas: ::prost::alloc::string::String, + #[prost(uint64, tag = "25")] + pub size: u64, + #[prost(uint64, tag = "26")] + pub total_transactions: u64, + #[prost(uint64, tag = "27")] + pub successful_transactions: u64, + #[prost(uint64, tag = "28")] + pub failed_transactions: u64, + #[prost(uint64, tag = "29")] + pub total_balance_changes: u64, + #[prost(uint64, tag = "30")] + pub total_withdrawals: u64, + #[prost(string, tag = "31")] + pub detail_level: ::prost::alloc::string::String, + #[prost(uint32, tag = "32")] + pub detail_level_code: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Transaction { + #[prost(message, optional, tag = "1")] + pub block_time: ::core::option::Option<::prost_types::Timestamp>, + #[prost(uint64, tag = "2")] + pub block_number: u64, + #[prost(string, tag = "3")] + pub block_hash: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub block_date: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub transactions_root: ::prost::alloc::string::String, + #[prost(string, tag = "6")] + pub receipts_root: ::prost::alloc::string::String, + #[prost(uint32, tag = "7")] + pub index: u32, + #[prost(string, tag = "8")] + pub hash: ::prost::alloc::string::String, + #[prost(string, tag = "9")] + pub from: ::prost::alloc::string::String, + #[prost(string, tag = "10")] + pub to: ::prost::alloc::string::String, + #[prost(uint64, tag = "11")] + pub nonce: u64, + #[prost(string, tag = "12")] + pub status: ::prost::alloc::string::String, + #[prost(uint32, tag = "13")] + pub status_code: u32, + #[prost(bool, tag = "14")] + pub success: bool, + #[prost(string, tag = "15")] + pub gas_price: ::prost::alloc::string::String, + #[prost(uint64, tag = "16")] + pub gas_limit: u64, + #[prost(string, tag = "17")] + pub value: ::prost::alloc::string::String, + #[prost(string, tag = "18")] + pub data: ::prost::alloc::string::String, + #[prost(string, tag = "19")] + pub v: ::prost::alloc::string::String, + #[prost(string, tag = "20")] + pub r: ::prost::alloc::string::String, + #[prost(string, tag = "21")] + pub s: ::prost::alloc::string::String, + #[prost(uint64, tag = "22")] + pub gas_used: u64, + #[prost(string, tag = "23")] + pub r#type: ::prost::alloc::string::String, + #[prost(uint32, tag = "24")] + pub type_code: u32, + #[prost(string, tag = "25")] + pub max_fee_per_gas: ::prost::alloc::string::String, + #[prost(string, tag = "26")] + pub max_priority_fee_per_gas: ::prost::alloc::string::String, + #[prost(uint64, tag = "27")] + pub begin_ordinal: u64, + #[prost(uint64, tag = "28")] + pub end_ordinal: u64, + #[prost(string, tag = "29")] + pub blob_gas_price: ::prost::alloc::string::String, + #[prost(uint64, tag = "30")] + pub blob_gas_used: u64, + #[prost(uint64, tag = "31")] + pub cumulative_gas_used: u64, + #[prost(string, tag = "32")] + pub logs_bloom: ::prost::alloc::string::String, + #[prost(string, tag = "33")] + pub state_root: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Log { + #[prost(message, optional, tag = "1")] + pub block_time: ::core::option::Option<::prost_types::Timestamp>, + #[prost(uint64, tag = "2")] + pub block_number: u64, + #[prost(string, tag = "3")] + pub block_hash: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub block_date: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub tx_hash: ::prost::alloc::string::String, + #[prost(uint32, tag = "6")] + pub tx_index: u32, + #[prost(string, tag = "7")] + pub tx_status: ::prost::alloc::string::String, + #[prost(uint32, tag = "8")] + pub tx_status_code: u32, + #[prost(bool, tag = "9")] + pub tx_success: bool, + #[prost(string, tag = "10")] + pub tx_from: ::prost::alloc::string::String, + #[prost(string, tag = "11")] + pub tx_to: ::prost::alloc::string::String, + #[prost(uint32, tag = "12")] + pub index: u32, + #[prost(uint32, tag = "13")] + pub block_index: u32, + #[prost(string, tag = "14")] + pub contract_address: ::prost::alloc::string::String, + #[prost(string, tag = "15")] + pub topic0: ::prost::alloc::string::String, + #[prost(string, tag = "16")] + pub topic1: ::prost::alloc::string::String, + #[prost(string, tag = "17")] + pub topic2: ::prost::alloc::string::String, + #[prost(string, tag = "18")] + pub topic3: ::prost::alloc::string::String, + #[prost(string, tag = "19")] + pub data: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Trace { + #[prost(message, optional, tag = "1")] + pub block_time: ::core::option::Option<::prost_types::Timestamp>, + #[prost(uint64, tag = "2")] + pub block_number: u64, + #[prost(string, tag = "3")] + pub block_hash: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub block_date: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub tx_hash: ::prost::alloc::string::String, + #[prost(uint32, tag = "6")] + pub tx_index: u32, + #[prost(string, tag = "7")] + pub tx_status: ::prost::alloc::string::String, + #[prost(uint32, tag = "8")] + pub tx_status_code: u32, + #[prost(bool, tag = "9")] + pub tx_success: bool, + #[prost(string, tag = "10")] + pub from: ::prost::alloc::string::String, + #[prost(string, tag = "11")] + pub to: ::prost::alloc::string::String, + #[prost(uint32, tag = "12")] + pub index: u32, + #[prost(uint32, tag = "13")] + pub parent_index: u32, + #[prost(uint32, tag = "14")] + pub depth: u32, + #[prost(string, tag = "15")] + pub caller: ::prost::alloc::string::String, + #[prost(string, tag = "16")] + pub call_type: ::prost::alloc::string::String, + #[prost(uint32, tag = "17")] + pub call_type_code: u32, + #[prost(string, tag = "18")] + pub address: ::prost::alloc::string::String, + #[prost(string, tag = "19")] + pub value: ::prost::alloc::string::String, + #[prost(uint64, tag = "20")] + pub gas_limit: u64, + #[prost(uint64, tag = "21")] + pub gas_consumed: u64, + #[prost(string, tag = "22")] + pub return_data: ::prost::alloc::string::String, + #[prost(string, tag = "23")] + pub input: ::prost::alloc::string::String, + #[prost(bool, tag = "24")] + pub suicide: bool, + #[prost(string, tag = "25")] + pub failure_reason: ::prost::alloc::string::String, + #[prost(bool, tag = "26")] + pub state_reverted: bool, + #[prost(bool, tag = "27")] + pub status_reverted: bool, + #[prost(bool, tag = "28")] + pub status_failed: bool, + #[prost(bool, tag = "29")] + pub executed_code: bool, + #[prost(uint64, tag = "30")] + pub begin_ordinal: u64, + #[prost(uint64, tag = "31")] + pub end_ordinal: u64, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BalanceChange { + #[prost(message, optional, tag = "1")] + pub block_time: ::core::option::Option<::prost_types::Timestamp>, + #[prost(uint64, tag = "2")] + pub block_number: u64, + #[prost(string, tag = "3")] + pub block_hash: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub block_date: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub address: ::prost::alloc::string::String, + #[prost(string, tag = "6")] + pub new_balance: ::prost::alloc::string::String, + #[prost(string, tag = "7")] + pub old_balance: ::prost::alloc::string::String, + #[prost(string, tag = "8")] + pub amount: ::prost::alloc::string::String, + #[prost(uint64, tag = "9")] + pub ordinal: u64, + #[prost(string, tag = "10")] + pub reason: ::prost::alloc::string::String, + #[prost(uint32, tag = "11")] + pub reason_code: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct StorageChange { + #[prost(message, optional, tag = "1")] + pub block_time: ::core::option::Option<::prost_types::Timestamp>, + #[prost(uint64, tag = "2")] + pub block_number: u64, + #[prost(string, tag = "3")] + pub block_hash: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub block_date: ::prost::alloc::string::String, + #[prost(uint64, tag = "5")] + pub ordinal: u64, + #[prost(string, tag = "6")] + pub address: ::prost::alloc::string::String, + #[prost(string, tag = "7")] + pub key: ::prost::alloc::string::String, + #[prost(string, tag = "8")] + pub new_value: ::prost::alloc::string::String, + #[prost(string, tag = "9")] + pub old_value: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CodeChange { + #[prost(message, optional, tag = "1")] + pub block_time: ::core::option::Option<::prost_types::Timestamp>, + #[prost(uint64, tag = "2")] + pub block_number: u64, + #[prost(string, tag = "3")] + pub block_hash: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub block_date: ::prost::alloc::string::String, + #[prost(uint64, tag = "5")] + pub ordinal: u64, + #[prost(string, tag = "6")] + pub address: ::prost::alloc::string::String, + #[prost(string, tag = "7")] + pub old_hash: ::prost::alloc::string::String, + #[prost(string, tag = "8")] + pub old_code: ::prost::alloc::string::String, + #[prost(string, tag = "9")] + pub new_hash: ::prost::alloc::string::String, + #[prost(string, tag = "10")] + pub new_code: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AccountCreation { + #[prost(message, optional, tag = "1")] + pub block_time: ::core::option::Option<::prost_types::Timestamp>, + #[prost(uint64, tag = "2")] + pub block_number: u64, + #[prost(string, tag = "3")] + pub block_hash: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub block_date: ::prost::alloc::string::String, + #[prost(uint64, tag = "5")] + pub ordinal: u64, + #[prost(string, tag = "6")] + pub account: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NonceChange { + #[prost(message, optional, tag = "1")] + pub block_time: ::core::option::Option<::prost_types::Timestamp>, + #[prost(uint64, tag = "2")] + pub block_number: u64, + #[prost(string, tag = "3")] + pub block_hash: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub block_date: ::prost::alloc::string::String, + #[prost(uint64, tag = "5")] + pub ordinal: u64, + #[prost(string, tag = "6")] + pub address: ::prost::alloc::string::String, + #[prost(uint64, tag = "7")] + pub old_value: u64, + #[prost(uint64, tag = "8")] + pub new_value: u64, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GasChange { + #[prost(message, optional, tag = "1")] + pub block_time: ::core::option::Option<::prost_types::Timestamp>, + #[prost(uint64, tag = "2")] + pub block_number: u64, + #[prost(string, tag = "3")] + pub block_hash: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub block_date: ::prost::alloc::string::String, + #[prost(uint64, tag = "5")] + pub ordinal: u64, + #[prost(uint64, tag = "6")] + pub old_value: u64, + #[prost(uint64, tag = "7")] + pub new_value: u64, + #[prost(string, tag = "8")] + pub reason: ::prost::alloc::string::String, + #[prost(uint32, tag = "9")] + pub reason_code: u32, +} +// @@protoc_insertion_point(module) diff --git a/blocks/evm/src/pb/mod.rs b/blocks/evm/src/pb/mod.rs new file mode 100644 index 0000000..04bd25b --- /dev/null +++ b/blocks/evm/src/pb/mod.rs @@ -0,0 +1,6 @@ +// @generated +// @@protoc_insertion_point(attribute:evm) +pub mod evm { + include!("evm.rs"); + // @@protoc_insertion_point(evm) +} diff --git a/blocks/evm/src/sinks.rs b/blocks/evm/src/sinks.rs index b812885..40d324b 100644 --- a/blocks/evm/src/sinks.rs +++ b/blocks/evm/src/sinks.rs @@ -1,18 +1,38 @@ +use common::utils::build_timestamp; use substreams::errors::Error; use substreams::pb::substreams::Clock; -use substreams_database_change::pb::database::DatabaseChanges; + use substreams_ethereum::pb::eth::v2::Block; -use crate::blocks::insert_blocks; +use crate::blocks::collect_block; +use crate::pb::evm::Events; -#[substreams::handlers::map] -pub fn ch_out(clock: Clock, block: Block) -> Result { - let mut tables: DatabaseChanges = DatabaseChanges::default(); +// #[substreams::handlers::map] +// pub fn ch_out(clock: Clock, block: Block) -> Result { +// let mut tables: DatabaseChanges = DatabaseChanges::default(); + +// // TABLE::blocks +// insert_blocks(&mut tables, &clock, &block); - // TABLE::blocks - insert_blocks(&mut tables, &clock, &block); +// Ok(tables) +// } + +#[substreams::handlers::map] +pub fn map_events(clock: Clock, block: Block) -> Result { + let timestamp = build_timestamp(&clock); - Ok(tables) + Ok(Events { + blocks: vec![collect_block(&block, ×tamp)], + transactions: vec![], + logs: vec![], + traces: vec![], + balance_changes: vec![], + storage_changes: vec![], + code_changes: vec![], + account_creations: vec![], + nonce_changes: vec![], + gas_changes: vec![], + }) } // // TO-DO: Implement the `graph_out` function using EntityChanges diff --git a/blocks/evm/src/size.rs b/blocks/evm/src/size.rs index c01bddd..4e7ab6e 100644 --- a/blocks/evm/src/size.rs +++ b/blocks/evm/src/size.rs @@ -43,4 +43,4 @@ pub fn insert_balance_change_counts(row: &mut TableChange, all_balance_changes_r } row.change("total_balance_changes", ("", total_balance_changes.to_string().as_str())) .change("total_withdrawals", ("", total_withdrawals.to_string().as_str())); -} \ No newline at end of file +} diff --git a/blocks/evm/substreams.yaml b/blocks/evm/substreams.yaml index e5b0d3a..2e0f28c 100644 --- a/blocks/evm/substreams.yaml +++ b/blocks/evm/substreams.yaml @@ -5,29 +5,25 @@ package: url: https://github.com/pinax-network/substreams-raw-blocks image: logo.png -imports: - database_change: https://github.com/streamingfast/substreams-sink-database-changes/releases/download/v2.0.0/substreams-database-change-v2.0.0.spkg - sql: https://github.com/streamingfast/substreams-sink-sql/releases/download/protodefs-v1.0.7/substreams-sink-sql-protodefs-v1.0.7.spkg - binaries: default: type: wasm/rust-v1 file: ../../target/wasm32-unknown-unknown/release/raw_blocks_evm.wasm +protobuf: + files: + - evm.proto + importPaths: + - ../../proto + excludePaths: + - sf/substreams + - google + modules: - - name: ch_out + - name: map_events kind: map inputs: - source: sf.substreams.v1.Clock - source: sf.ethereum.type.v2.Block output: - type: proto:sf.substreams.sink.database.v1.DatabaseChanges - -sink: - module: ch_out - type: sf.substreams.sink.sql.v1.Service - config: - schema: "./schema.sql" - engine: clickhouse - postgraphile_frontend: - enabled: false + type: proto:evm.Events \ No newline at end of file diff --git a/common/src/utils.rs b/common/src/utils.rs index 983f355..a8ee9e2 100644 --- a/common/src/utils.rs +++ b/common/src/utils.rs @@ -65,6 +65,13 @@ pub fn optional_bigint_to_string(value: &Option, default: &str) -> Strin } } +pub fn optional_bigint_to_u64(value: &Option) -> u64 { + match value { + Some(bigint) => bigint.clone().with_decimal(0).to_string().parse::().unwrap(), + None => 0, + } +} + pub fn optional_bigint_to_decimal(value: Option) -> BigDecimal { match value { Some(bigint) => bigint.with_decimal(0), diff --git a/proto/evm.proto b/proto/evm.proto new file mode 100644 index 0000000..9ac9215 --- /dev/null +++ b/proto/evm.proto @@ -0,0 +1,218 @@ +syntax = "proto3"; + +package evm; + +import "google/protobuf/timestamp.proto"; + +message Events { + repeated Block blocks = 1; + repeated Transaction transactions = 2; + repeated Log logs = 3; + repeated Trace traces = 4; + repeated BalanceChange balance_changes = 5; + repeated StorageChange storage_changes = 6; + repeated CodeChange code_changes = 7; + repeated AccountCreation account_creations = 8; + repeated NonceChange nonce_changes = 9; + repeated GasChange gas_changes = 10; +} + + +message Block { + google.protobuf.Timestamp time = 1; + uint64 number = 2; + string date = 3; + string hash = 4; + string parent_hash = 5; + uint64 nonce = 6; + string ommers_hash = 7; + string logs_bloom = 8; + string transactions_root = 9; + string state_root = 10; + string receipts_root = 11; + string withdrawals_root = 12; + string parent_beacon_root = 13; + string miner = 14; + uint64 difficulty = 15; + string total_difficulty = 16; + string mix_hash = 17; + string extra_data = 18; + string extra_data_utf8 = 19; + uint64 gas_limit = 20; + uint64 gas_used = 21; + string base_fee_per_gas = 22; + string blob_gas_used = 23; + string excess_blob_gas = 24; + uint64 size = 25; + uint64 total_transactions = 26; + uint64 successful_transactions = 27; + uint64 failed_transactions = 28; + uint64 total_balance_changes = 29; + uint64 total_withdrawals = 30; + string detail_level = 31; + uint32 detail_level_code = 32; +} + + +message Transaction { + google.protobuf.Timestamp block_time = 1; + uint64 block_number = 2; + string block_hash = 3; + string block_date = 4; + string transactions_root = 5; + string receipts_root = 6; + uint32 index = 7; + string hash = 8; + string from = 9; + string to = 10; + uint64 nonce = 11; + string status = 12; + uint32 status_code = 13; + bool success = 14; + string gas_price = 15; + uint64 gas_limit = 16; + string value = 17; + string data = 18; + string v = 19; + string r = 20; + string s = 21; + uint64 gas_used = 22; + string type = 23; + uint32 type_code = 24; + string max_fee_per_gas = 25; + string max_priority_fee_per_gas = 26; + uint64 begin_ordinal = 27; + uint64 end_ordinal = 28; + string blob_gas_price = 29; + uint64 blob_gas_used = 30; + uint64 cumulative_gas_used = 31; + string logs_bloom = 32; + string state_root = 33; +} + +message Log { + google.protobuf.Timestamp block_time = 1; + uint64 block_number = 2; + string block_hash = 3; + string block_date = 4; + string tx_hash = 5; + uint32 tx_index = 6; + string tx_status = 7; + uint32 tx_status_code = 8; + bool tx_success = 9; + string tx_from = 10; + string tx_to = 11; + uint32 index = 12; + uint32 block_index = 13; + string contract_address = 14; + string topic0 = 15; + string topic1 = 16; + string topic2 = 17; + string topic3 = 18; + string data = 19; +} + +message Trace { + google.protobuf.Timestamp block_time = 1; + uint64 block_number = 2; + string block_hash = 3; + string block_date = 4; + string tx_hash = 5; + uint32 tx_index = 6; + string tx_status = 7; + uint32 tx_status_code = 8; + bool tx_success = 9; + string from = 10; + string to = 11; + uint32 index = 12; + uint32 parent_index = 13; + uint32 depth = 14; + string caller = 15; + string call_type = 16; + uint32 call_type_code = 17; + string address = 18; + string value = 19; + uint64 gas_limit = 20; + uint64 gas_consumed = 21; + string return_data = 22; + string input = 23; + bool suicide = 24; + string failure_reason = 25; + bool state_reverted = 26; + bool status_reverted = 27; + bool status_failed = 28; + bool executed_code = 29; + uint64 begin_ordinal = 30; + uint64 end_ordinal = 31; +} + +message BalanceChange { + google.protobuf.Timestamp block_time = 1; + uint64 block_number = 2; + string block_hash = 3; + string block_date = 4; + string address = 5; + string new_balance = 6; + string old_balance = 7; + string amount = 8; + uint64 ordinal = 9; + string reason = 10; + uint32 reason_code = 11; +} + +message StorageChange { + google.protobuf.Timestamp block_time = 1; + uint64 block_number = 2; + string block_hash = 3; + string block_date = 4; + uint64 ordinal = 5; + string address = 6; + string key = 7; + string new_value = 8; + string old_value = 9; +} + +message CodeChange { + google.protobuf.Timestamp block_time = 1; + uint64 block_number = 2; + string block_hash = 3; + string block_date = 4; + uint64 ordinal = 5; + string address = 6; + string old_hash = 7; + string old_code = 8; + string new_hash = 9; + string new_code = 10; +} + +message AccountCreation { + google.protobuf.Timestamp block_time = 1; + uint64 block_number = 2; + string block_hash = 3; + string block_date = 4; + uint64 ordinal = 5; + string account = 6; +} + +message NonceChange { + google.protobuf.Timestamp block_time = 1; + uint64 block_number = 2; + string block_hash = 3; + string block_date = 4; + uint64 ordinal = 5; + string address = 6; + uint64 old_value = 7; + uint64 new_value = 8; +} + +message GasChange { + google.protobuf.Timestamp block_time = 1; + uint64 block_number = 2; + string block_hash = 3; + string block_date = 4; + uint64 ordinal = 5; + uint64 old_value = 6; + uint64 new_value = 7; + string reason = 8; + uint32 reason_code = 9; +} From c98479268578213c7f93e7c0378b67ced43258ab Mon Sep 17 00:00:00 2001 From: zolting <44510235+zolting@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:59:58 -0500 Subject: [PATCH 02/20] Add Transactions --- blocks/evm/src/sinks.rs | 3 +- blocks/evm/src/transactions.rs | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/blocks/evm/src/sinks.rs b/blocks/evm/src/sinks.rs index 40d324b..a59d16f 100644 --- a/blocks/evm/src/sinks.rs +++ b/blocks/evm/src/sinks.rs @@ -6,6 +6,7 @@ use substreams_ethereum::pb::eth::v2::Block; use crate::blocks::collect_block; use crate::pb::evm::Events; +use crate::transactions::collect_transactions; // #[substreams::handlers::map] // pub fn ch_out(clock: Clock, block: Block) -> Result { @@ -23,7 +24,7 @@ pub fn map_events(clock: Clock, block: Block) -> Result { Ok(Events { blocks: vec![collect_block(&block, ×tamp)], - transactions: vec![], + transactions: collect_transactions(&block, ×tamp), logs: vec![], traces: vec![], balance_changes: vec![], diff --git a/blocks/evm/src/transactions.rs b/blocks/evm/src/transactions.rs index 8bf9d90..f2e8327 100644 --- a/blocks/evm/src/transactions.rs +++ b/blocks/evm/src/transactions.rs @@ -1,14 +1,19 @@ +use core::time; + use common::blocks::insert_timestamp; +use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; use common::utils::optional_bigint_to_string; use substreams::pb::substreams::Clock; use substreams_database_change::pb::database::TableChange; use substreams_database_change::pb::database::{table_change, DatabaseChanges}; +use substreams_ethereum::pb::eth::v2::Block; use substreams_ethereum::pb::eth::v2::BlockHeader; use substreams_ethereum::pb::eth::v2::TransactionTrace; use crate::keys::transaction_keys; use crate::logs::insert_log; +use crate::pb::evm::Transaction as RawTransaction; use crate::traces::insert_trace; pub fn transaction_type_to_string(r#type: i32) -> String { @@ -170,3 +175,50 @@ pub fn insert_empty_transaction_metadata(row: &mut TableChange, is_transaction: .change("tx_status_code", ("", tx_status_code.to_string().as_str())) .change("tx_success", ("", tx_success.to_string().as_str())); } + +pub fn collect_transactions(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let block_header = block.header.as_ref().unwrap(); + + block + .transaction_traces + .iter() + .map(|transaction| { + let receipt = transaction.receipt.clone().unwrap(); + RawTransaction { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + transactions_root: bytes_to_hex(&block_header.transactions_root), + receipts_root: bytes_to_hex(&block_header.receipt_root), + index: transaction.index, + hash: bytes_to_hex(&transaction.hash), + from: bytes_to_hex(&transaction.from), + to: bytes_to_hex(&transaction.to), + nonce: transaction.nonce, + status: transaction_status_to_string(transaction.status), + status_code: transaction.status as u32, + success: is_transaction_success(transaction.status), + gas_price: optional_bigint_to_string(&transaction.gas_price, "0"), + gas_limit: transaction.gas_limit, + value: optional_bigint_to_string(&transaction.value, "0"), + data: bytes_to_hex(&transaction.input), + v: bytes_to_hex(&transaction.v), + r: bytes_to_hex(&transaction.r), + s: bytes_to_hex(&transaction.s), + gas_used: transaction.gas_used, + r#type: transaction_type_to_string(transaction.r#type), + type_code: transaction.r#type as u32, + max_fee_per_gas: optional_bigint_to_string(&transaction.max_fee_per_gas, "0"), + max_priority_fee_per_gas: optional_bigint_to_string(&transaction.max_priority_fee_per_gas, "0"), + begin_ordinal: transaction.begin_ordinal, + end_ordinal: transaction.end_ordinal, + blob_gas_price: optional_bigint_to_string(&receipt.blob_gas_price, "0"), + blob_gas_used: receipt.blob_gas_used(), + cumulative_gas_used: receipt.cumulative_gas_used, + logs_bloom: bytes_to_hex(&receipt.logs_bloom), + state_root: bytes_to_hex(&receipt.state_root), + } + }) + .collect() +} From bbf7950eacb3de03aa498d84db252883a32b84ec Mon Sep 17 00:00:00 2001 From: zolting <44510235+zolting@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:21:26 -0500 Subject: [PATCH 03/20] Add Logs --- blocks/evm/src/logs.rs | 46 +++++++++++++++++++- blocks/evm/src/sinks.rs | 6 ++- blocks/evm/src/traces.rs | 10 ++++- blocks/evm/src/transactions.rs | 76 +++++++++++++++++----------------- 4 files changed, 95 insertions(+), 43 deletions(-) diff --git a/blocks/evm/src/logs.rs b/blocks/evm/src/logs.rs index cd7c0f3..7e96390 100644 --- a/blocks/evm/src/logs.rs +++ b/blocks/evm/src/logs.rs @@ -1,11 +1,13 @@ use common::blocks::insert_timestamp; +use common::structs::BlockTimestamp; use common::utils::{bytes_to_hex, extract_topic}; use substreams::pb::substreams::Clock; use substreams_database_change::pb::database::{table_change, DatabaseChanges}; -use substreams_ethereum::pb::eth::v2::{Log, TransactionTrace}; +use substreams_ethereum::pb::eth::v2::{Block, Log, TransactionTrace}; use crate::keys::logs_keys; -use crate::transactions::insert_transaction_metadata; +use crate::pb::evm::Log as RawLog; +use crate::transactions::{insert_transaction_metadata, is_transaction_success, transaction_status_to_string}; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L512 // DetailLevel: BASE (only successful transactions) & EXTENDED @@ -41,3 +43,43 @@ pub fn insert_log(tables: &mut DatabaseChanges, clock: &Clock, log: &Log, transa insert_timestamp(row, clock, false, true); insert_transaction_metadata(row, transaction, false); } + +// https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L512 +// DetailLevel: BASE (only successful transactions) & EXTENDED +pub fn collect_logs(block: &Block, timestamp: &BlockTimestamp, detail_level: &str) -> Vec { + // Only required DetailLevel=BASE since traces are not available in BASE + if detail_level == "Base" { + return vec![]; + } + + let mut logs: Vec = vec![]; + + for transaction in &block.transaction_traces { + let receipt = transaction.receipt.as_ref().unwrap(); + for log in &receipt.logs { + logs.push(RawLog { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + tx_hash: bytes_to_hex(&transaction.hash), + tx_index: transaction.index, + tx_status: transaction_status_to_string(transaction.status), + tx_status_code: transaction.status as u32, + tx_success: is_transaction_success(transaction.status), + tx_from: bytes_to_hex(&transaction.from), + tx_to: bytes_to_hex(&transaction.to), + index: log.index, + block_index: log.block_index, + contract_address: bytes_to_hex(&log.address), + topic0: extract_topic(&log.topics, 0), + topic1: extract_topic(&log.topics, 1), + topic2: extract_topic(&log.topics, 2), + topic3: extract_topic(&log.topics, 3), + data: bytes_to_hex(&log.data), + }); + } + } + + logs +} diff --git a/blocks/evm/src/sinks.rs b/blocks/evm/src/sinks.rs index a59d16f..d85049f 100644 --- a/blocks/evm/src/sinks.rs +++ b/blocks/evm/src/sinks.rs @@ -4,7 +4,8 @@ use substreams::pb::substreams::Clock; use substreams_ethereum::pb::eth::v2::Block; -use crate::blocks::collect_block; +use crate::blocks::{block_detail_to_string, collect_block}; +use crate::logs::collect_logs; use crate::pb::evm::Events; use crate::transactions::collect_transactions; @@ -21,11 +22,12 @@ use crate::transactions::collect_transactions; #[substreams::handlers::map] pub fn map_events(clock: Clock, block: Block) -> Result { let timestamp = build_timestamp(&clock); + let detail_level = block_detail_to_string(block.detail_level); Ok(Events { blocks: vec![collect_block(&block, ×tamp)], transactions: collect_transactions(&block, ×tamp), - logs: vec![], + logs: collect_logs(&block, ×tamp, &detail_level), traces: vec![], balance_changes: vec![], storage_changes: vec![], diff --git a/blocks/evm/src/traces.rs b/blocks/evm/src/traces.rs index 46a994b..7873035 100644 --- a/blocks/evm/src/traces.rs +++ b/blocks/evm/src/traces.rs @@ -7,7 +7,15 @@ use substreams_database_change::pb::database::{table_change, DatabaseChanges, Ta use substreams_ethereum::pb::eth::v2::{Call, TransactionTrace}; use crate::{ - account_creations::insert_account_creation, balance_changes::insert_balance_change, code_changes::insert_code_change, gas_changes::insert_gas_change, keys::traces_keys, logs::insert_log, nonce_changes::insert_nonce_change, storage_changes::insert_storage_change, transactions::{insert_empty_transaction_metadata, insert_transaction_metadata} + account_creations::insert_account_creation, + balance_changes::insert_balance_change, + code_changes::insert_code_change, + gas_changes::insert_gas_change, + keys::traces_keys, + logs::insert_log, + nonce_changes::insert_nonce_change, + storage_changes::insert_storage_change, + transactions::{insert_empty_transaction_metadata, insert_transaction_metadata}, }; pub fn call_types_to_string(call_type: i32) -> String { diff --git a/blocks/evm/src/transactions.rs b/blocks/evm/src/transactions.rs index f2e8327..8b42e0b 100644 --- a/blocks/evm/src/transactions.rs +++ b/blocks/evm/src/transactions.rs @@ -138,44 +138,6 @@ pub fn insert_transaction(tables: &mut DatabaseChanges, clock: &Clock, transacti } } -pub fn insert_transaction_metadata(row: &mut TableChange, transaction: &TransactionTrace, is_transaction: bool) { - let tx_hash = bytes_to_hex(&transaction.hash); - let tx_index = transaction.index; - let from = bytes_to_hex(&transaction.from); // does trace contain `from`? - let to = bytes_to_hex(&transaction.to); // does trace contain `to`? - let tx_status = transaction_status_to_string(transaction.status); - let tx_status_code = transaction.status; - let tx_success = is_transaction_success(transaction.status); - let prefix = if is_transaction { "" } else { "tx_" }; - - row.change("tx_hash", ("", tx_hash.as_str())) - .change("tx_index", ("", tx_index.to_string().as_str())) - .change(format!("{}from", prefix).as_str(), ("", from.as_str())) - .change(format!("{}to", prefix).as_str(), ("", to.as_str())) - .change("tx_status", ("", tx_status.as_str())) - .change("tx_status_code", ("", tx_status_code.to_string().as_str())) - .change("tx_success", ("", tx_success.to_string().as_str())); -} - -pub fn insert_empty_transaction_metadata(row: &mut TableChange, is_transaction: bool) { - let tx_hash = ""; - let tx_index = 0; - let from = ""; - let to = ""; - let tx_status = transaction_status_to_string(1); - let tx_status_code = 1; - let tx_success = true; - let prefix = if is_transaction { "" } else { "tx_" }; - - row.change("tx_hash", ("", tx_hash)) - .change("tx_index", ("", tx_index.to_string().as_str())) - .change(format!("{}from", prefix).as_str(), ("", from)) - .change(format!("{}to", prefix).as_str(), ("", to)) - .change("tx_status", ("", tx_status.as_str())) - .change("tx_status_code", ("", tx_status_code.to_string().as_str())) - .change("tx_success", ("", tx_success.to_string().as_str())); -} - pub fn collect_transactions(block: &Block, timestamp: &BlockTimestamp) -> Vec { let block_header = block.header.as_ref().unwrap(); @@ -222,3 +184,41 @@ pub fn collect_transactions(block: &Block, timestamp: &BlockTimestamp) -> Vec Date: Fri, 15 Nov 2024 16:37:39 -0500 Subject: [PATCH 04/20] Add Traces --- blocks/evm/src/sinks.rs | 3 +- blocks/evm/src/traces.rs | 95 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/blocks/evm/src/sinks.rs b/blocks/evm/src/sinks.rs index d85049f..eae4f9e 100644 --- a/blocks/evm/src/sinks.rs +++ b/blocks/evm/src/sinks.rs @@ -7,6 +7,7 @@ use substreams_ethereum::pb::eth::v2::Block; use crate::blocks::{block_detail_to_string, collect_block}; use crate::logs::collect_logs; use crate::pb::evm::Events; +use crate::traces::collect_traces; use crate::transactions::collect_transactions; // #[substreams::handlers::map] @@ -28,7 +29,7 @@ pub fn map_events(clock: Clock, block: Block) -> Result { blocks: vec![collect_block(&block, ×tamp)], transactions: collect_transactions(&block, ×tamp), logs: collect_logs(&block, ×tamp, &detail_level), - traces: vec![], + traces: collect_traces(&block, ×tamp, &detail_level), balance_changes: vec![], storage_changes: vec![], code_changes: vec![], diff --git a/blocks/evm/src/traces.rs b/blocks/evm/src/traces.rs index 7873035..049dd51 100644 --- a/blocks/evm/src/traces.rs +++ b/blocks/evm/src/traces.rs @@ -1,10 +1,11 @@ use common::{ blocks::insert_timestamp, + structs::BlockTimestamp, utils::{bytes_to_hex, optional_bigint_to_string}, }; use substreams::pb::substreams::Clock; use substreams_database_change::pb::database::{table_change, DatabaseChanges, TableChange}; -use substreams_ethereum::pb::eth::v2::{Call, TransactionTrace}; +use substreams_ethereum::pb::eth::v2::{Block, Call, TransactionTrace}; use crate::{ account_creations::insert_account_creation, @@ -14,8 +15,9 @@ use crate::{ keys::traces_keys, logs::insert_log, nonce_changes::insert_nonce_change, + pb::evm::Trace as RawTrace, storage_changes::insert_storage_change, - transactions::{insert_empty_transaction_metadata, insert_transaction_metadata}, + transactions::{insert_empty_transaction_metadata, insert_transaction_metadata, is_transaction_success, transaction_status_to_string}, }; pub fn call_types_to_string(call_type: i32) -> String { @@ -134,3 +136,92 @@ pub fn insert_trace_row(row: &mut TableChange, call: &Call) { .change("failure_reason", ("", failure_reason.as_str())) .change("return_data", ("", return_data.as_str())); } + +pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp, detail_level: &str) -> Vec { + // Only required DetailLevel=EXTENDED + if detail_level != "Extended" { + return vec![]; + } + + let mut traces: Vec = vec![]; + + // Collect system traces + for call in &block.system_calls { + traces.push(RawTrace { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + // As this is a system call, tx_hash is empty + // tx_index, tx_status, tx_status_code, tx_success are irrelevant as well + tx_hash: String::new(), + tx_index: 0, + tx_status: transaction_status_to_string(1), + tx_status_code: 1, + tx_success: true, + from: bytes_to_hex(&call.caller), + to: bytes_to_hex(&call.address), + index: call.index, + parent_index: call.parent_index, + depth: call.depth, + caller: bytes_to_hex(&call.caller), + call_type: call_types_to_string(call.call_type), + call_type_code: call.call_type as u32, + address: bytes_to_hex(&call.address), + value: optional_bigint_to_string(&call.value, "0"), + gas_limit: call.gas_limit, + gas_consumed: call.gas_consumed, + input: bytes_to_hex(&call.input), + return_data: bytes_to_hex(&call.return_data), + failure_reason: call.failure_reason.clone(), + begin_ordinal: call.begin_ordinal, + end_ordinal: call.end_ordinal, + executed_code: call.executed_code, + state_reverted: call.state_reverted, + status_failed: call.status_failed, + status_reverted: call.status_reverted, + suicide: call.suicide, + }); + } + + // Collect transaction traces + for transaction in &block.transaction_traces { + for call in &transaction.calls { + traces.push(RawTrace { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + tx_hash: bytes_to_hex(&transaction.hash), + tx_index: transaction.index, + tx_status: transaction_status_to_string(transaction.status), + tx_status_code: transaction.status as u32, + tx_success: is_transaction_success(transaction.status), + from: bytes_to_hex(&transaction.from), + to: bytes_to_hex(&transaction.to), + index: call.index, + parent_index: call.parent_index, + depth: call.depth, + caller: bytes_to_hex(&call.caller), + call_type: call_types_to_string(call.call_type), + call_type_code: call.call_type as u32, + address: bytes_to_hex(&call.address), + value: optional_bigint_to_string(&call.value, "0"), + gas_limit: call.gas_limit, + gas_consumed: call.gas_consumed, + input: bytes_to_hex(&call.input), + return_data: bytes_to_hex(&call.return_data), + failure_reason: call.failure_reason.clone(), + begin_ordinal: call.begin_ordinal, + end_ordinal: call.end_ordinal, + executed_code: call.executed_code, + state_reverted: call.state_reverted, + status_failed: call.status_failed, + status_reverted: call.status_reverted, + suicide: call.suicide, + }); + } + } + + traces +} From 7f1167e0bc0b4a33fcd6fc0e30dd70b6934cbef8 Mon Sep 17 00:00:00 2001 From: zolting <44510235+zolting@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:12:47 -0500 Subject: [PATCH 05/20] Add BalanceChange --- blocks/evm/src/balance_changes.rs | 56 +++++++++++++++++++++++++++++-- blocks/evm/src/sinks.rs | 3 +- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/blocks/evm/src/balance_changes.rs b/blocks/evm/src/balance_changes.rs index bf9f374..3e6780d 100644 --- a/blocks/evm/src/balance_changes.rs +++ b/blocks/evm/src/balance_changes.rs @@ -1,11 +1,13 @@ use common::blocks::insert_timestamp; -use common::utils::{bytes_to_hex, optional_bigint_to_decimal}; +use common::structs::BlockTimestamp; use common::utils::optional_bigint_to_string; +use common::utils::{bytes_to_hex, optional_bigint_to_decimal}; use substreams::pb::substreams::Clock; use substreams_database_change::pb::database::{table_change, DatabaseChanges, TableChange}; -use substreams_ethereum::pb::eth::v2::BalanceChange; +use substreams_ethereum::pb::eth::v2::{BalanceChange, Block}; use crate::keys::block_ordinal_keys; +use crate::pb::evm::BalanceChange as RawBalanceChange; pub fn balance_change_reason_to_string(reason: i32) -> String { match reason { @@ -62,4 +64,52 @@ pub fn insert_balance_change(tables: &mut DatabaseChanges, clock: &Clock, balanc insert_balance_change_row(row, balance_change); insert_timestamp(row, clock, false, true); -} \ No newline at end of file +} + +pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut balance_changes: Vec = vec![]; + + // Collect balance changes from system calls + for call in &block.system_calls { + for balance_change in &call.balance_changes { + let amount = optional_bigint_to_decimal(balance_change.new_value.clone()) - optional_bigint_to_decimal(balance_change.old_value.clone()); + balance_changes.push(RawBalanceChange { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + address: bytes_to_hex(&balance_change.address), + new_balance: optional_bigint_to_string(&balance_change.new_value, "0"), + old_balance: optional_bigint_to_string(&balance_change.old_value, "0"), + amount: amount.to_string(), + ordinal: balance_change.ordinal, + reason: balance_change_reason_to_string(balance_change.reason), + reason_code: balance_change.reason as u32, + }); + } + } + + // Collect balance changes from transaction traces + for transaction in &block.transaction_traces { + for call in &transaction.calls { + for balance_change in &call.balance_changes { + let amount = optional_bigint_to_decimal(balance_change.new_value.clone()) - optional_bigint_to_decimal(balance_change.old_value.clone()); + balance_changes.push(RawBalanceChange { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + address: bytes_to_hex(&balance_change.address), + new_balance: optional_bigint_to_string(&balance_change.new_value, "0"), + old_balance: optional_bigint_to_string(&balance_change.old_value, "0"), + amount: amount.to_string(), + ordinal: balance_change.ordinal, + reason: balance_change_reason_to_string(balance_change.reason), + reason_code: balance_change.reason as u32, + }); + } + } + } + + balance_changes +} diff --git a/blocks/evm/src/sinks.rs b/blocks/evm/src/sinks.rs index eae4f9e..13fa6b1 100644 --- a/blocks/evm/src/sinks.rs +++ b/blocks/evm/src/sinks.rs @@ -4,6 +4,7 @@ use substreams::pb::substreams::Clock; use substreams_ethereum::pb::eth::v2::Block; +use crate::balance_changes::collect_balance_changes; use crate::blocks::{block_detail_to_string, collect_block}; use crate::logs::collect_logs; use crate::pb::evm::Events; @@ -30,7 +31,7 @@ pub fn map_events(clock: Clock, block: Block) -> Result { transactions: collect_transactions(&block, ×tamp), logs: collect_logs(&block, ×tamp, &detail_level), traces: collect_traces(&block, ×tamp, &detail_level), - balance_changes: vec![], + balance_changes: collect_balance_changes(&block, ×tamp), storage_changes: vec![], code_changes: vec![], account_creations: vec![], From 0e3f3459aaff27a37c696c7816044ce15b024e9b Mon Sep 17 00:00:00 2001 From: zolting <44510235+zolting@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:18:00 -0500 Subject: [PATCH 06/20] Add StorageChange --- blocks/evm/src/sinks.rs | 3 +- blocks/evm/src/storage_changes.rs | 46 ++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/blocks/evm/src/sinks.rs b/blocks/evm/src/sinks.rs index 13fa6b1..90aa2f0 100644 --- a/blocks/evm/src/sinks.rs +++ b/blocks/evm/src/sinks.rs @@ -8,6 +8,7 @@ use crate::balance_changes::collect_balance_changes; use crate::blocks::{block_detail_to_string, collect_block}; use crate::logs::collect_logs; use crate::pb::evm::Events; +use crate::storage_changes::collect_storage_changes; use crate::traces::collect_traces; use crate::transactions::collect_transactions; @@ -32,7 +33,7 @@ pub fn map_events(clock: Clock, block: Block) -> Result { logs: collect_logs(&block, ×tamp, &detail_level), traces: collect_traces(&block, ×tamp, &detail_level), balance_changes: collect_balance_changes(&block, ×tamp), - storage_changes: vec![], + storage_changes: collect_storage_changes(&block, ×tamp), code_changes: vec![], account_creations: vec![], nonce_changes: vec![], diff --git a/blocks/evm/src/storage_changes.rs b/blocks/evm/src/storage_changes.rs index 97e0204..c47cedc 100644 --- a/blocks/evm/src/storage_changes.rs +++ b/blocks/evm/src/storage_changes.rs @@ -1,10 +1,12 @@ use common::blocks::insert_timestamp; +use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; use substreams::pb::substreams::Clock; use substreams_database_change::pb::database::{table_change, DatabaseChanges}; -use substreams_ethereum::pb::eth::v2::StorageChange; +use substreams_ethereum::pb::eth::v2::{Block, StorageChange}; use crate::keys::block_ordinal_keys; +use crate::pb::evm::StorageChange as RawStorageChange; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L647 // DetailLevel: EXTENDED @@ -26,3 +28,45 @@ pub fn insert_storage_change(tables: &mut DatabaseChanges, clock: &Clock, storag insert_timestamp(row, clock, false, true); } + +pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut storage_changes: Vec = vec![]; + + // Collect storage changes from system calls + for call in &block.system_calls { + for storage_change in &call.storage_changes { + storage_changes.push(RawStorageChange { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + address: bytes_to_hex(&storage_change.address), + key: bytes_to_hex(&storage_change.key), + new_value: bytes_to_hex(&storage_change.new_value), + old_value: bytes_to_hex(&storage_change.old_value), + ordinal: storage_change.ordinal, + }); + } + } + + // Collect storage changes from transaction traces + for transaction in &block.transaction_traces { + for call in &transaction.calls { + for storage_change in &call.storage_changes { + storage_changes.push(RawStorageChange { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + address: bytes_to_hex(&storage_change.address), + key: bytes_to_hex(&storage_change.key), + new_value: bytes_to_hex(&storage_change.new_value), + old_value: bytes_to_hex(&storage_change.old_value), + ordinal: storage_change.ordinal, + }); + } + } + } + + storage_changes +} From 3f363c1d00a389d639e46cd7917fac59f4a11567 Mon Sep 17 00:00:00 2001 From: zolting <44510235+zolting@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:20:29 -0500 Subject: [PATCH 07/20] Add CodeChange --- blocks/evm/src/code_changes.rs | 48 +++++++++++++++++++++++++++++++++- blocks/evm/src/sinks.rs | 3 ++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/blocks/evm/src/code_changes.rs b/blocks/evm/src/code_changes.rs index 4949849..cbe52bc 100644 --- a/blocks/evm/src/code_changes.rs +++ b/blocks/evm/src/code_changes.rs @@ -1,10 +1,12 @@ use common::blocks::insert_timestamp; +use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; use substreams::pb::substreams::Clock; use substreams_database_change::pb::database::{table_change, DatabaseChanges, TableChange}; -use substreams_ethereum::pb::eth::v2::CodeChange; +use substreams_ethereum::pb::eth::v2::{Block, CodeChange}; use crate::keys::block_ordinal_keys; +use crate::pb::evm::CodeChange as RawCodeChange; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L744 // DetailLevel: EXTENDED @@ -32,3 +34,47 @@ pub fn insert_code_change(tables: &mut DatabaseChanges, clock: &Clock, code_chan insert_code_change_rows(row, code_change); insert_timestamp(row, clock, false, true); } + +pub fn collect_code_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut code_changes: Vec = vec![]; + + // Collect code changes from system calls + for call in &block.system_calls { + for code_change in &call.code_changes { + code_changes.push(RawCodeChange { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + address: bytes_to_hex(&code_change.address), + old_hash: bytes_to_hex(&code_change.old_hash), + old_code: bytes_to_hex(&code_change.old_code), + new_hash: bytes_to_hex(&code_change.new_hash), + new_code: bytes_to_hex(&code_change.new_code), + ordinal: code_change.ordinal, + }); + } + } + + // Collect code changes from transaction traces + for transaction in &block.transaction_traces { + for call in &transaction.calls { + for code_change in &call.code_changes { + code_changes.push(RawCodeChange { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + address: bytes_to_hex(&code_change.address), + old_hash: bytes_to_hex(&code_change.old_hash), + old_code: bytes_to_hex(&code_change.old_code), + new_hash: bytes_to_hex(&code_change.new_hash), + new_code: bytes_to_hex(&code_change.new_code), + ordinal: code_change.ordinal, + }); + } + } + } + + code_changes +} diff --git a/blocks/evm/src/sinks.rs b/blocks/evm/src/sinks.rs index 90aa2f0..20c44da 100644 --- a/blocks/evm/src/sinks.rs +++ b/blocks/evm/src/sinks.rs @@ -6,6 +6,7 @@ use substreams_ethereum::pb::eth::v2::Block; use crate::balance_changes::collect_balance_changes; use crate::blocks::{block_detail_to_string, collect_block}; +use crate::code_changes::collect_code_changes; use crate::logs::collect_logs; use crate::pb::evm::Events; use crate::storage_changes::collect_storage_changes; @@ -34,7 +35,7 @@ pub fn map_events(clock: Clock, block: Block) -> Result { traces: collect_traces(&block, ×tamp, &detail_level), balance_changes: collect_balance_changes(&block, ×tamp), storage_changes: collect_storage_changes(&block, ×tamp), - code_changes: vec![], + code_changes: collect_code_changes(&block, ×tamp), account_creations: vec![], nonce_changes: vec![], gas_changes: vec![], From 804740d4a4254e3722c690b72b1d62982cdd28f2 Mon Sep 17 00:00:00 2001 From: zolting <44510235+zolting@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:25:06 -0500 Subject: [PATCH 08/20] Add AccountCreation and NonceChange --- blocks/evm/src/account_creations.rs | 40 +++++++++++++++++++++++++- blocks/evm/src/nonce_changes.rs | 44 ++++++++++++++++++++++++++++- blocks/evm/src/sinks.rs | 6 ++-- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/blocks/evm/src/account_creations.rs b/blocks/evm/src/account_creations.rs index 266bf60..6c2830b 100644 --- a/blocks/evm/src/account_creations.rs +++ b/blocks/evm/src/account_creations.rs @@ -1,10 +1,12 @@ use common::blocks::insert_timestamp; +use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; use substreams::pb::substreams::Clock; use substreams_database_change::pb::database::{table_change, DatabaseChanges}; -use substreams_ethereum::pb::eth::v2::AccountCreation; +use substreams_ethereum::pb::eth::v2::{AccountCreation, Block}; use crate::keys::block_ordinal_keys; +use crate::pb::evm::AccountCreation as RawAccountCreation; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L736 // DetailLevel: EXTENDED @@ -20,3 +22,39 @@ pub fn insert_account_creation(tables: &mut DatabaseChanges, clock: &Clock, acco insert_timestamp(row, clock, false, true); } + +pub fn collect_account_creations(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut account_creations: Vec = vec![]; + + // Collect account creations from system calls + for call in &block.system_calls { + for account_creation in &call.account_creations { + account_creations.push(RawAccountCreation { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + account: bytes_to_hex(&account_creation.account), + ordinal: account_creation.ordinal, + }); + } + } + + // Collect account creations from transaction traces + for transaction in &block.transaction_traces { + for call in &transaction.calls { + for account_creation in &call.account_creations { + account_creations.push(RawAccountCreation { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + account: bytes_to_hex(&account_creation.account), + ordinal: account_creation.ordinal, + }); + } + } + } + + account_creations +} diff --git a/blocks/evm/src/nonce_changes.rs b/blocks/evm/src/nonce_changes.rs index 2008593..38b9568 100644 --- a/blocks/evm/src/nonce_changes.rs +++ b/blocks/evm/src/nonce_changes.rs @@ -1,10 +1,12 @@ use common::blocks::insert_timestamp; +use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; use substreams::pb::substreams::Clock; use substreams_database_change::pb::database::{table_change, DatabaseChanges}; -use substreams_ethereum::pb::eth::v2::NonceChange; +use substreams_ethereum::pb::eth::v2::{Block, NonceChange}; use crate::keys::block_ordinal_keys; +use crate::pb::evm::NonceChange as RawNonceChange; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L726C9-L726C20 // DetailLevel: EXTENDED @@ -24,3 +26,43 @@ pub fn insert_nonce_change(tables: &mut DatabaseChanges, clock: &Clock, nonce_ch insert_timestamp(row, clock, false, true); } + +pub fn collect_nonce_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut nonce_changes: Vec = vec![]; + + // Collect nonce changes from system calls + for call in &block.system_calls { + for nonce_change in &call.nonce_changes { + nonce_changes.push(RawNonceChange { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + address: bytes_to_hex(&nonce_change.address), + old_value: nonce_change.old_value, + new_value: nonce_change.new_value, + ordinal: nonce_change.ordinal, + }); + } + } + + // Collect nonce changes from transaction traces + for transaction in &block.transaction_traces { + for call in &transaction.calls { + for nonce_change in &call.nonce_changes { + nonce_changes.push(RawNonceChange { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + address: bytes_to_hex(&nonce_change.address), + old_value: nonce_change.old_value, + new_value: nonce_change.new_value, + ordinal: nonce_change.ordinal, + }); + } + } + } + + nonce_changes +} diff --git a/blocks/evm/src/sinks.rs b/blocks/evm/src/sinks.rs index 20c44da..4f6345a 100644 --- a/blocks/evm/src/sinks.rs +++ b/blocks/evm/src/sinks.rs @@ -4,10 +4,12 @@ use substreams::pb::substreams::Clock; use substreams_ethereum::pb::eth::v2::Block; +use crate::account_creations::collect_account_creations; use crate::balance_changes::collect_balance_changes; use crate::blocks::{block_detail_to_string, collect_block}; use crate::code_changes::collect_code_changes; use crate::logs::collect_logs; +use crate::nonce_changes::collect_nonce_changes; use crate::pb::evm::Events; use crate::storage_changes::collect_storage_changes; use crate::traces::collect_traces; @@ -36,8 +38,8 @@ pub fn map_events(clock: Clock, block: Block) -> Result { balance_changes: collect_balance_changes(&block, ×tamp), storage_changes: collect_storage_changes(&block, ×tamp), code_changes: collect_code_changes(&block, ×tamp), - account_creations: vec![], - nonce_changes: vec![], + account_creations: collect_account_creations(&block, ×tamp), + nonce_changes: collect_nonce_changes(&block, ×tamp), gas_changes: vec![], }) } From 441107ab4f99b1ea78f0e20718c0c5968c5f3915 Mon Sep 17 00:00:00 2001 From: zolting <44510235+zolting@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:27:10 -0500 Subject: [PATCH 09/20] Add GasChange --- blocks/evm/src/gas_changes.rs | 46 ++++++++++++++++++++++++++++++++++- blocks/evm/src/sinks.rs | 3 ++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/blocks/evm/src/gas_changes.rs b/blocks/evm/src/gas_changes.rs index a8382a2..0bce3fc 100644 --- a/blocks/evm/src/gas_changes.rs +++ b/blocks/evm/src/gas_changes.rs @@ -1,9 +1,11 @@ use common::blocks::insert_timestamp; +use common::structs::BlockTimestamp; use substreams::pb::substreams::Clock; use substreams_database_change::pb::database::{table_change, DatabaseChanges}; -use substreams_ethereum::pb::eth::v2::GasChange; +use substreams_ethereum::pb::eth::v2::{Block, GasChange}; use crate::keys::block_ordinal_keys; +use crate::pb::evm::GasChange as RawGasChange; pub fn gas_change_reason_to_string(reason: i32) -> String { match reason { @@ -57,3 +59,45 @@ pub fn insert_gas_change(tables: &mut DatabaseChanges, clock: &Clock, gas_change insert_timestamp(row, clock, false, true); } + +pub fn collect_gas_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut gas_changes: Vec = vec![]; + + // Collect gas changes from system calls + for call in &block.system_calls { + for gas_change in &call.gas_changes { + gas_changes.push(RawGasChange { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + old_value: gas_change.old_value, + new_value: gas_change.new_value, + reason: gas_change_reason_to_string(gas_change.reason), + reason_code: gas_change.reason as u32, + ordinal: gas_change.ordinal, + }); + } + } + + // Collect gas changes from transaction traces + for transaction in &block.transaction_traces { + for call in &transaction.calls { + for gas_change in &call.gas_changes { + gas_changes.push(RawGasChange { + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + old_value: gas_change.old_value, + new_value: gas_change.new_value, + reason: gas_change_reason_to_string(gas_change.reason), + reason_code: gas_change.reason as u32, + ordinal: gas_change.ordinal, + }); + } + } + } + + gas_changes +} diff --git a/blocks/evm/src/sinks.rs b/blocks/evm/src/sinks.rs index 4f6345a..d34bca7 100644 --- a/blocks/evm/src/sinks.rs +++ b/blocks/evm/src/sinks.rs @@ -8,6 +8,7 @@ use crate::account_creations::collect_account_creations; use crate::balance_changes::collect_balance_changes; use crate::blocks::{block_detail_to_string, collect_block}; use crate::code_changes::collect_code_changes; +use crate::gas_changes::collect_gas_changes; use crate::logs::collect_logs; use crate::nonce_changes::collect_nonce_changes; use crate::pb::evm::Events; @@ -40,7 +41,7 @@ pub fn map_events(clock: Clock, block: Block) -> Result { code_changes: collect_code_changes(&block, ×tamp), account_creations: collect_account_creations(&block, ×tamp), nonce_changes: collect_nonce_changes(&block, ×tamp), - gas_changes: vec![], + gas_changes: collect_gas_changes(&block, ×tamp), }) } From 4758358720b3754582208bdc33f9cbbde4cffe42 Mon Sep 17 00:00:00 2001 From: zolting <44510235+zolting@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:58:33 -0500 Subject: [PATCH 10/20] Cleanup ununsed code --- blocks/evm/src/account_creations.rs | 20 +--- blocks/evm/src/balance_changes.rs | 33 +------ blocks/evm/src/blocks.rs | 95 +------------------ blocks/evm/src/code_changes.rs | 31 +------ blocks/evm/src/{sinks.rs => events.rs} | 19 ---- blocks/evm/src/gas_changes.rs | 25 +---- blocks/evm/src/keys.rs | 31 ------- blocks/evm/src/lib.rs | 4 +- blocks/evm/src/logs.rs | 43 +-------- blocks/evm/src/nonce_changes.rs | 23 +---- blocks/evm/src/size.rs | 46 --------- blocks/evm/src/storage_changes.rs | 26 +----- blocks/evm/src/traces.rs | 124 +------------------------ blocks/evm/src/transactions.rs | 99 -------------------- 14 files changed, 18 insertions(+), 601 deletions(-) rename blocks/evm/src/{sinks.rs => events.rs} (69%) delete mode 100644 blocks/evm/src/keys.rs delete mode 100644 blocks/evm/src/size.rs diff --git a/blocks/evm/src/account_creations.rs b/blocks/evm/src/account_creations.rs index 6c2830b..a935a24 100644 --- a/blocks/evm/src/account_creations.rs +++ b/blocks/evm/src/account_creations.rs @@ -1,28 +1,12 @@ -use common::blocks::insert_timestamp; use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; -use substreams::pb::substreams::Clock; -use substreams_database_change::pb::database::{table_change, DatabaseChanges}; -use substreams_ethereum::pb::eth::v2::{AccountCreation, Block}; -use crate::keys::block_ordinal_keys; +use substreams_ethereum::pb::eth::v2::Block; + use crate::pb::evm::AccountCreation as RawAccountCreation; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L736 // DetailLevel: EXTENDED -pub fn insert_account_creation(tables: &mut DatabaseChanges, clock: &Clock, account_creation: &AccountCreation) { - let account = bytes_to_hex(&account_creation.account); - let ordinal = account_creation.ordinal; - - let keys = block_ordinal_keys(&clock, &ordinal); - let row = tables - .push_change_composite("account_creations", keys, 0, table_change::Operation::Create) - .change("account", ("", account.as_str())) - .change("ordinal", ("", ordinal.to_string().as_str())); - - insert_timestamp(row, clock, false, true); -} - pub fn collect_account_creations(block: &Block, timestamp: &BlockTimestamp) -> Vec { let mut account_creations: Vec = vec![]; diff --git a/blocks/evm/src/balance_changes.rs b/blocks/evm/src/balance_changes.rs index 3e6780d..f306bf3 100644 --- a/blocks/evm/src/balance_changes.rs +++ b/blocks/evm/src/balance_changes.rs @@ -1,12 +1,8 @@ -use common::blocks::insert_timestamp; use common::structs::BlockTimestamp; use common::utils::optional_bigint_to_string; use common::utils::{bytes_to_hex, optional_bigint_to_decimal}; -use substreams::pb::substreams::Clock; -use substreams_database_change::pb::database::{table_change, DatabaseChanges, TableChange}; -use substreams_ethereum::pb::eth::v2::{BalanceChange, Block}; +use substreams_ethereum::pb::eth::v2::Block; -use crate::keys::block_ordinal_keys; use crate::pb::evm::BalanceChange as RawBalanceChange; pub fn balance_change_reason_to_string(reason: i32) -> String { @@ -39,33 +35,6 @@ pub fn balance_change_reason_to_string(reason: i32) -> String { // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L658 // DetailLevel: EXTENDED -pub fn insert_balance_change_row(row: &mut TableChange, balance_change: &BalanceChange) { - let address = bytes_to_hex(&balance_change.address); - let new_balance = optional_bigint_to_string(&balance_change.new_value.clone(), "0"); - let old_balance = optional_bigint_to_string(&balance_change.old_value.clone(), "0"); - let amount = optional_bigint_to_decimal(balance_change.new_value.clone()) - optional_bigint_to_decimal(balance_change.old_value.clone()); - let ordinal = balance_change.ordinal; - let reason_code = balance_change.reason; - let reason = balance_change_reason_to_string(reason_code); - - row.change("address", ("", address.as_str())) - .change("new_balance", ("", new_balance.as_str())) - .change("old_balance", ("", old_balance.as_str())) - .change("amount", ("", amount.to_string().as_str())) - .change("ordinal", ("", ordinal.to_string().as_str())) - .change("reason", ("", reason.as_str())) - .change("reason_code", ("", reason_code.to_string().as_str())); -} - -pub fn insert_balance_change(tables: &mut DatabaseChanges, clock: &Clock, balance_change: &BalanceChange) { - let ordinal = balance_change.ordinal; - let keys = block_ordinal_keys(&clock, &ordinal); - let row = tables.push_change_composite("balance_changes", keys, 0, table_change::Operation::Create); - - insert_balance_change_row(row, balance_change); - insert_timestamp(row, clock, false, true); -} - pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { let mut balance_changes: Vec = vec![]; diff --git a/blocks/evm/src/blocks.rs b/blocks/evm/src/blocks.rs index 1794a3c..12fa8d2 100644 --- a/blocks/evm/src/blocks.rs +++ b/blocks/evm/src/blocks.rs @@ -1,19 +1,10 @@ -use std::vec; - -use common::blocks::insert_timestamp; use common::structs::BlockTimestamp; +use common::utils::optional_bigint_to_string; use common::utils::{bytes_to_hex, optional_bigint_to_u64, optional_u64_to_string}; -use common::{keys::blocks_keys, utils::optional_bigint_to_string}; -use substreams::pb::substreams::Clock; -use substreams_database_change::pb::database::{table_change, DatabaseChanges}; + use substreams_ethereum::pb::eth::v2::Block; -use crate::balance_changes::insert_balance_change; -use crate::code_changes::insert_code_change; use crate::pb::evm::Block as RawBlock; -use crate::size::insert_size; -use crate::traces::insert_system_trace; -use crate::transactions::insert_transaction; pub fn block_detail_to_string(detail_level: i32) -> String { match detail_level { @@ -26,88 +17,6 @@ pub fn block_detail_to_string(detail_level: i32) -> String { // https://github.com/streamingfast/firehose-ethereum/blob/develop/proto/sf/ethereum/type/v2/type.proto // DetailLevel: BASE -pub fn insert_blocks(tables: &mut DatabaseChanges, clock: &Clock, block: &Block) { - let header: substreams_ethereum::pb::eth::v2::BlockHeader = block.header.clone().unwrap_or_default(); - let parent_hash = bytes_to_hex(&header.parent_hash); - let nonce = header.nonce; - let ommers_hash = bytes_to_hex(&header.uncle_hash); - let logs_bloom = bytes_to_hex(&header.logs_bloom); - let transactions_root = bytes_to_hex(&header.transactions_root); - let state_root = bytes_to_hex(&header.state_root); - let receipts_root = bytes_to_hex(&header.receipt_root); - let miner = bytes_to_hex(&header.coinbase); // EVM Address - let mix_hash = bytes_to_hex(&header.mix_hash); - let extra_data = bytes_to_hex(&header.extra_data.clone()); - let extra_data_utf8 = String::from_utf8(header.extra_data.clone()).unwrap_or_default(); - let gas_limit = header.gas_limit; - let gas_used = header.gas_used; - let difficulty = optional_bigint_to_string(&header.difficulty, "0"); // UInt64 - let total_difficulty = optional_bigint_to_string(&header.total_difficulty.clone(), "0"); // UInt256 - - // block detail levels - // https://streamingfastio.medium.com/new-block-model-to-accelerate-chain-integration-9f65126e5425 - let detail_level_code = block.detail_level; - let detail_level = block_detail_to_string(detail_level_code); - - // forks - let withdrawals_root = bytes_to_hex(&header.withdrawals_root); // EIP-4895 (Shangai Fork) - let parent_beacon_root = bytes_to_hex(&header.parent_beacon_root); // EIP-4788 (Dencun Fork) - let base_fee_per_gas = optional_bigint_to_string(&header.base_fee_per_gas, ""); // UInt256 - EIP-1559 (London Fork) - let excess_blob_gas = optional_u64_to_string(&header.excess_blob_gas, ""); // UInt64 - EIP-4844 (Dencun Fork) - let blob_gas_used = optional_u64_to_string(&header.blob_gas_used, ""); // UInt64 - EIP-4844 (Dencun Fork) - - // blocks - let keys = blocks_keys(&clock, true); - let row = tables - .push_change_composite("blocks", keys, 0, table_change::Operation::Create) - .change("parent_hash", ("", parent_hash.as_str())) - .change("nonce", ("", nonce.to_string().as_str())) - .change("ommers_hash", ("", ommers_hash.as_str())) - .change("logs_bloom", ("", logs_bloom.as_str())) - .change("transactions_root", ("", transactions_root.as_str())) - .change("state_root", ("", state_root.as_str())) - .change("receipts_root", ("", receipts_root.as_str())) - .change("miner", ("", miner.as_str())) - .change("mix_hash", ("", mix_hash.as_str())) - .change("extra_data", ("", extra_data.as_str())) - .change("extra_data_utf8", ("", extra_data_utf8.as_str())) - .change("gas_limit", ("", gas_limit.to_string().as_str())) - .change("gas_used", ("", gas_used.to_string().as_str())) - .change("difficulty", ("", difficulty.as_str())) - .change("total_difficulty", ("", total_difficulty.as_str())) - // EIP-1559 (London Fork) - .change("base_fee_per_gas", ("", base_fee_per_gas.as_str())) - // EIP-4895 (Shangai Fork) - .change("withdrawals_root", ("", withdrawals_root.as_str())) - // EIP-4844 & EIP-4788 (Dencun Fork) - .change("parent_beacon_root", ("", parent_beacon_root.as_str())) - .change("excess_blob_gas", ("", excess_blob_gas.as_str())) - .change("blob_gas_used", ("", blob_gas_used.as_str())) - // block detail levels - .change("detail_level", ("", detail_level.as_str())) - .change("detail_level_code", ("", detail_level_code.to_string().as_str())); - - insert_timestamp(row, clock, true, true); - insert_size(row, &block); - - // TABLE::code_changes - for code_change in block.code_changes.iter() { - insert_code_change(tables, clock, code_change); - } - // TABLE::traces - for system_call in block.system_calls.iter() { - insert_system_trace(tables, clock, system_call); - } - // TABLE::balance_changes - for balance_change in block.balance_changes.iter() { - insert_balance_change(tables, clock, balance_change); - } - // TABLE::transactions - for transaction in block.transaction_traces.iter() { - insert_transaction(tables, clock, &transaction, &header, &detail_level); - } -} - pub fn collect_block(block: &Block, timestamp: &BlockTimestamp) -> RawBlock { let header = block.header.as_ref().unwrap(); diff --git a/blocks/evm/src/code_changes.rs b/blocks/evm/src/code_changes.rs index cbe52bc..0f218ba 100644 --- a/blocks/evm/src/code_changes.rs +++ b/blocks/evm/src/code_changes.rs @@ -1,40 +1,11 @@ -use common::blocks::insert_timestamp; use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; -use substreams::pb::substreams::Clock; -use substreams_database_change::pb::database::{table_change, DatabaseChanges, TableChange}; -use substreams_ethereum::pb::eth::v2::{Block, CodeChange}; +use substreams_ethereum::pb::eth::v2::Block; -use crate::keys::block_ordinal_keys; use crate::pb::evm::CodeChange as RawCodeChange; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L744 // DetailLevel: EXTENDED -pub fn insert_code_change_rows(row: &mut TableChange, code_change: &CodeChange) { - let address = bytes_to_hex(&code_change.address); - let old_hash = bytes_to_hex(&code_change.old_hash); - let old_code = bytes_to_hex(&code_change.old_code); - let new_hash = bytes_to_hex(&code_change.new_hash); - let new_code = bytes_to_hex(&code_change.new_code); - let ordinal = code_change.ordinal; - - row.change("address", ("", address.as_str())) - .change("old_hash", ("", old_hash.as_str())) - .change("old_code", ("", old_code.as_str())) - .change("new_hash", ("", new_hash.as_str())) - .change("new_code", ("", new_code.as_str())) - .change("ordinal", ("", ordinal.to_string().as_str())); -} - -pub fn insert_code_change(tables: &mut DatabaseChanges, clock: &Clock, code_change: &CodeChange) { - let ordinal: u64 = code_change.ordinal; - let keys = block_ordinal_keys(&clock, &ordinal); - let row = tables.push_change_composite("code_changes", keys, 0, table_change::Operation::Create); - - insert_code_change_rows(row, code_change); - insert_timestamp(row, clock, false, true); -} - pub fn collect_code_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { let mut code_changes: Vec = vec![]; diff --git a/blocks/evm/src/sinks.rs b/blocks/evm/src/events.rs similarity index 69% rename from blocks/evm/src/sinks.rs rename to blocks/evm/src/events.rs index d34bca7..c584c9f 100644 --- a/blocks/evm/src/sinks.rs +++ b/blocks/evm/src/events.rs @@ -16,16 +16,6 @@ use crate::storage_changes::collect_storage_changes; use crate::traces::collect_traces; use crate::transactions::collect_transactions; -// #[substreams::handlers::map] -// pub fn ch_out(clock: Clock, block: Block) -> Result { -// let mut tables: DatabaseChanges = DatabaseChanges::default(); - -// // TABLE::blocks -// insert_blocks(&mut tables, &clock, &block); - -// Ok(tables) -// } - #[substreams::handlers::map] pub fn map_events(clock: Clock, block: Block) -> Result { let timestamp = build_timestamp(&clock); @@ -44,12 +34,3 @@ pub fn map_events(clock: Clock, block: Block) -> Result { gas_changes: collect_gas_changes(&block, ×tamp), }) } - -// // TO-DO: Implement the `graph_out` function using EntityChanges -// #[substreams::handlers::map] -// pub fn graph_out(clock: Clock, block: Block) -> Result { -// let mut tables: DatabaseChanges = DatabaseChanges::default(); -// insert_blocks(&mut tables, &clock, &block); -// // TO-DO: Convert DatabaseChanges to EntityChanges -// Ok(tables) -// } diff --git a/blocks/evm/src/gas_changes.rs b/blocks/evm/src/gas_changes.rs index 0bce3fc..e5b3abd 100644 --- a/blocks/evm/src/gas_changes.rs +++ b/blocks/evm/src/gas_changes.rs @@ -1,10 +1,6 @@ -use common::blocks::insert_timestamp; use common::structs::BlockTimestamp; -use substreams::pb::substreams::Clock; -use substreams_database_change::pb::database::{table_change, DatabaseChanges}; -use substreams_ethereum::pb::eth::v2::{Block, GasChange}; +use substreams_ethereum::pb::eth::v2::Block; -use crate::keys::block_ordinal_keys; use crate::pb::evm::GasChange as RawGasChange; pub fn gas_change_reason_to_string(reason: i32) -> String { @@ -41,25 +37,6 @@ pub fn gas_change_reason_to_string(reason: i32) -> String { // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L726C9-L726C20 // DetailLevel: EXTENDED -pub fn insert_gas_change(tables: &mut DatabaseChanges, clock: &Clock, gas_change: &GasChange) { - let old_value = gas_change.old_value; - let new_value = gas_change.new_value; - let reason = gas_change_reason_to_string(gas_change.reason); - let reason_code = gas_change.reason; - let ordinal = gas_change.ordinal; - - let keys = block_ordinal_keys(&clock, &ordinal); - let row = tables - .push_change_composite("gas_changes", keys, 0, table_change::Operation::Create) - .change("reason", ("", reason.as_str())) - .change("reason_code", ("", reason_code.to_string().as_str())) - .change("old_value", ("", old_value.to_string().as_str())) - .change("new_value", ("", new_value.to_string().as_str())) - .change("ordinal", ("", ordinal.to_string().as_str())); - - insert_timestamp(row, clock, false, true); -} - pub fn collect_gas_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { let mut gas_changes: Vec = vec![]; diff --git a/blocks/evm/src/keys.rs b/blocks/evm/src/keys.rs deleted file mode 100644 index 4a873e8..0000000 --- a/blocks/evm/src/keys.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::collections::HashMap; - -use common::keys::blocks_keys; -use substreams::pb::substreams::Clock; - -pub fn transaction_keys(clock: &Clock, hash: &String) -> HashMap { - let mut keys = blocks_keys(clock, false); - keys.insert("hash".to_string(), hash.to_string()); - keys -} - -pub fn logs_keys(clock: &Clock, tx_hash: &String, index: &u32) -> HashMap { - let mut keys = blocks_keys(clock, false); - keys.insert("tx_hash".to_string(), tx_hash.to_string()); - keys.insert("index".to_string(), index.to_string()); - keys -} - -pub fn block_ordinal_keys(clock: &Clock, ordinal: &u64) -> HashMap { - let mut keys = blocks_keys(clock, false); - keys.insert("ordinal".to_string(), ordinal.to_string()); - keys -} - -pub fn traces_keys(clock: &Clock, tx_hash: &String, tx_index: &u64, index: &u32) -> HashMap { - let mut keys = blocks_keys(clock, false); - keys.insert("tx_hash".to_string(), tx_hash.to_string()); - keys.insert("tx_index".to_string(), tx_index.to_string()); - keys.insert("index".to_string(), index.to_string()); - keys -} diff --git a/blocks/evm/src/lib.rs b/blocks/evm/src/lib.rs index 9145fd0..a35207e 100644 --- a/blocks/evm/src/lib.rs +++ b/blocks/evm/src/lib.rs @@ -2,13 +2,11 @@ mod account_creations; mod balance_changes; mod blocks; mod code_changes; +mod events; mod gas_changes; -mod keys; mod logs; mod nonce_changes; mod pb; -mod sinks; -mod size; mod storage_changes; mod traces; mod transactions; diff --git a/blocks/evm/src/logs.rs b/blocks/evm/src/logs.rs index 7e96390..b92189d 100644 --- a/blocks/evm/src/logs.rs +++ b/blocks/evm/src/logs.rs @@ -1,48 +1,9 @@ -use common::blocks::insert_timestamp; use common::structs::BlockTimestamp; use common::utils::{bytes_to_hex, extract_topic}; -use substreams::pb::substreams::Clock; -use substreams_database_change::pb::database::{table_change, DatabaseChanges}; -use substreams_ethereum::pb::eth::v2::{Block, Log, TransactionTrace}; +use substreams_ethereum::pb::eth::v2::Block; -use crate::keys::logs_keys; use crate::pb::evm::Log as RawLog; -use crate::transactions::{insert_transaction_metadata, is_transaction_success, transaction_status_to_string}; - -// https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L512 -// DetailLevel: BASE (only successful transactions) & EXTENDED -pub fn insert_log(tables: &mut DatabaseChanges, clock: &Clock, log: &Log, transaction: &TransactionTrace) { - let index = log.index; - let block_index = log.block_index; - let tx_hash = bytes_to_hex(&transaction.hash.to_vec()); - let contract_address = bytes_to_hex(&log.address.to_vec()); // EVM Address - let topics = log.topics.clone(); - let topic0 = extract_topic(&topics, 0); - let topic1 = extract_topic(&topics, 1); - let topic2 = extract_topic(&topics, 2); - let topic3 = extract_topic(&topics, 3); - let data = bytes_to_hex(&log.data.to_vec()); - - // Missing - // - is blob gas information even available in the logs? or is it only available in the transaction receipt? - // - blob_gas_price? - // - blob_gas_used? - - let keys = logs_keys(&clock, &tx_hash, &index); - let row = tables - .push_change_composite("logs", keys, 0, table_change::Operation::Create) - .change("index", ("", index.to_string().as_str())) - .change("block_index", ("", block_index.to_string().as_str())) - .change("contract_address", ("", contract_address.as_str())) - .change("topic0", ("", topic0.as_str())) - .change("topic1", ("", topic1.as_str())) - .change("topic2", ("", topic2.as_str())) - .change("topic3", ("", topic3.as_str())) - .change("data", ("", data.as_str())); - - insert_timestamp(row, clock, false, true); - insert_transaction_metadata(row, transaction, false); -} +use crate::transactions::{is_transaction_success, transaction_status_to_string}; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L512 // DetailLevel: BASE (only successful transactions) & EXTENDED diff --git a/blocks/evm/src/nonce_changes.rs b/blocks/evm/src/nonce_changes.rs index 38b9568..61e9b5b 100644 --- a/blocks/evm/src/nonce_changes.rs +++ b/blocks/evm/src/nonce_changes.rs @@ -1,32 +1,11 @@ -use common::blocks::insert_timestamp; use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; -use substreams::pb::substreams::Clock; -use substreams_database_change::pb::database::{table_change, DatabaseChanges}; -use substreams_ethereum::pb::eth::v2::{Block, NonceChange}; +use substreams_ethereum::pb::eth::v2::Block; -use crate::keys::block_ordinal_keys; use crate::pb::evm::NonceChange as RawNonceChange; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L726C9-L726C20 // DetailLevel: EXTENDED -pub fn insert_nonce_change(tables: &mut DatabaseChanges, clock: &Clock, nonce_change: &NonceChange) { - let address = bytes_to_hex(&nonce_change.address); - let old_value = nonce_change.old_value; - let new_value = nonce_change.new_value; - let ordinal = nonce_change.ordinal; - - let keys = block_ordinal_keys(&clock, &ordinal); - let row = tables - .push_change_composite("nonce_changes", keys, 0, table_change::Operation::Create) - .change("address", ("", address.as_str())) - .change("old_value", ("", old_value.to_string().as_str())) - .change("new_value", ("", new_value.to_string().as_str())) - .change("ordinal", ("", ordinal.to_string().as_str())); - - insert_timestamp(row, clock, false, true); -} - pub fn collect_nonce_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { let mut nonce_changes: Vec = vec![]; diff --git a/blocks/evm/src/size.rs b/blocks/evm/src/size.rs deleted file mode 100644 index 4e7ab6e..0000000 --- a/blocks/evm/src/size.rs +++ /dev/null @@ -1,46 +0,0 @@ -use substreams_database_change::pb::database::TableChange; -use substreams_ethereum::pb::eth::v2::Block; - -pub fn insert_size(row: &mut TableChange, block: &Block) { - let size = block.size; - row.change("size", ("", size.to_string().as_str())); - - // transaction status counts - let all_transaction_status: Vec = block.transaction_traces.iter().map(|transaction| transaction.status).collect(); - insert_transaction_counts(row, all_transaction_status); - - // balance changes counts - let all_balance_changes_reason: Vec = block.balance_changes.iter().map(|balance_change| balance_change.reason).collect(); - insert_balance_change_counts(row, all_balance_changes_reason); -} - -pub fn insert_transaction_counts(row: &mut TableChange, all_transaction_status: Vec) { - // transaction counts - let mut total_transactions = 0; - let mut successful_transactions = 0; - let mut failed_transactions = 0; - for status in all_transaction_status { - if status == 1 { - successful_transactions += 1; - } else { - failed_transactions += 1; - } - total_transactions += 1; - } - row.change("total_transactions", ("", total_transactions.to_string().as_str())) - .change("successful_transactions", ("", successful_transactions.to_string().as_str())) - .change("failed_transactions", ("", failed_transactions.to_string().as_str())); -} - -pub fn insert_balance_change_counts(row: &mut TableChange, all_balance_changes_reason: Vec) { - // transaction counts - let total_balance_changes = all_balance_changes_reason.len(); - let mut total_withdrawals = 0; - for reason in all_balance_changes_reason { - if reason == 16 { - total_withdrawals += 1; - } - } - row.change("total_balance_changes", ("", total_balance_changes.to_string().as_str())) - .change("total_withdrawals", ("", total_withdrawals.to_string().as_str())); -} diff --git a/blocks/evm/src/storage_changes.rs b/blocks/evm/src/storage_changes.rs index c47cedc..158949c 100644 --- a/blocks/evm/src/storage_changes.rs +++ b/blocks/evm/src/storage_changes.rs @@ -1,34 +1,12 @@ -use common::blocks::insert_timestamp; use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; -use substreams::pb::substreams::Clock; -use substreams_database_change::pb::database::{table_change, DatabaseChanges}; -use substreams_ethereum::pb::eth::v2::{Block, StorageChange}; -use crate::keys::block_ordinal_keys; +use substreams_ethereum::pb::eth::v2::Block; + use crate::pb::evm::StorageChange as RawStorageChange; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L647 // DetailLevel: EXTENDED -pub fn insert_storage_change(tables: &mut DatabaseChanges, clock: &Clock, storage_change: &StorageChange) { - let address = bytes_to_hex(&storage_change.address); - let key = bytes_to_hex(&storage_change.key); - let new_value = bytes_to_hex(&storage_change.new_value); - let old_value = bytes_to_hex(&storage_change.old_value); - let ordinal = storage_change.ordinal; - - let keys = block_ordinal_keys(&clock, &ordinal); - let row = tables - .push_change_composite("storage_changes", keys, 0, table_change::Operation::Create) - .change("address", ("", address.as_str())) - .change("key", ("", key.as_str())) - .change("new_value", ("", new_value.as_str())) - .change("old_value", ("", old_value.as_str())) - .change("ordinal", ("", ordinal.to_string().as_str())); - - insert_timestamp(row, clock, false, true); -} - pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { let mut storage_changes: Vec = vec![]; diff --git a/blocks/evm/src/traces.rs b/blocks/evm/src/traces.rs index 049dd51..3bf2d9c 100644 --- a/blocks/evm/src/traces.rs +++ b/blocks/evm/src/traces.rs @@ -1,23 +1,12 @@ use common::{ - blocks::insert_timestamp, structs::BlockTimestamp, utils::{bytes_to_hex, optional_bigint_to_string}, }; -use substreams::pb::substreams::Clock; -use substreams_database_change::pb::database::{table_change, DatabaseChanges, TableChange}; -use substreams_ethereum::pb::eth::v2::{Block, Call, TransactionTrace}; +use substreams_ethereum::pb::eth::v2::Block; use crate::{ - account_creations::insert_account_creation, - balance_changes::insert_balance_change, - code_changes::insert_code_change, - gas_changes::insert_gas_change, - keys::traces_keys, - logs::insert_log, - nonce_changes::insert_nonce_change, pb::evm::Trace as RawTrace, - storage_changes::insert_storage_change, - transactions::{insert_empty_transaction_metadata, insert_transaction_metadata, is_transaction_success, transaction_status_to_string}, + transactions::{is_transaction_success, transaction_status_to_string}, }; pub fn call_types_to_string(call_type: i32) -> String { @@ -32,111 +21,6 @@ pub fn call_types_to_string(call_type: i32) -> String { } } -// https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L546 -// DetailLevel: EXTENDED -pub fn insert_trace(tables: &mut DatabaseChanges, clock: &Clock, call: &Call, transaction: &TransactionTrace) { - // transaction - let tx_index = transaction.index; - let tx_hash = bytes_to_hex(&transaction.hash); - let keys = traces_keys(&clock, &tx_hash, &tx_index.into(), &call.index); - let row = tables.push_change_composite("traces", keys, 0, table_change::Operation::Create); - insert_trace_row(row, call); - insert_timestamp(row, clock, false, true); - insert_transaction_metadata(row, transaction, true); - - // TABLE::logs - for log in call.logs.iter() { - insert_log(tables, clock, log, transaction); - } - // TABLE::balance_changes - for balance_change in call.balance_changes.iter() { - insert_balance_change(tables, clock, balance_change); - } - // TABLE::storage_changes - for storage_change in call.storage_changes.iter() { - insert_storage_change(tables, clock, &storage_change); - } - // TABLE::code_changes - for code_change in call.code_changes.iter() { - insert_code_change(tables, clock, &code_change); - } - // TABLE::account_creations - for account_creation in call.account_creations.iter() { - insert_account_creation(tables, clock, &account_creation); - } - // TABLE::nonce_changes - for nonce_change in call.nonce_changes.iter() { - insert_nonce_change(tables, clock, &nonce_change); - } - // TABLE::gas_changes - for gas_change in call.gas_changes.iter() { - insert_gas_change(tables, clock, &gas_change); - } -} - -// https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L121-L124 -// DetailLevel: EXTENDED -// System calls are introduced in Cancun, along with blobs. They are executed outside of transactions but affect the state. -pub fn insert_system_trace(tables: &mut DatabaseChanges, clock: &Clock, call: &Call) { - // system does not create any transaction, key is empty and only uses call index - let keys = traces_keys(&clock, &"".to_string(), &0, &call.index); - let row = tables.push_change_composite("traces", keys, 0, table_change::Operation::Create); - insert_trace_row(row, call); - insert_timestamp(row, clock, false, true); - insert_empty_transaction_metadata(row, true); -} - -pub fn insert_trace_row(row: &mut TableChange, call: &Call) { - // trace - let address = bytes_to_hex(&call.address); // additional `trace_address`? - let begin_ordinal = call.begin_ordinal; - let call_type = call_types_to_string(call.call_type); - let call_type_code = call.call_type; - let caller = bytes_to_hex(&call.caller); - let depth = call.depth; - let end_ordinal = call.end_ordinal; - let executed_code = call.executed_code; - let gas_consumed = call.gas_consumed; - let gas_limit = call.gas_limit; - let index = call.index; // or `sub_traces`? - let input = bytes_to_hex(&call.input); // TO-DO: fallback to 0x? - let parent_index = call.parent_index; - let state_reverted = call.state_reverted; - let status_failed = call.status_failed; - let status_reverted = call.status_reverted; - let suicide = call.suicide; // or `selfdestruct`? - let value = optional_bigint_to_string(&call.value, "0"); // UInt256 - - // not available in system traces - let failure_reason = &call.failure_reason; - let return_data = bytes_to_hex(&call.return_data); - - // missing? - // - output - // - refund_address - - row.change("address", ("", address.as_str())) - .change("begin_ordinal", ("", begin_ordinal.to_string().as_str())) - .change("call_type", ("", call_type.as_str())) - .change("call_type_code", ("", call_type_code.to_string().as_str())) - .change("caller", ("", caller.as_str())) - .change("depth", ("", depth.to_string().as_str())) - .change("end_ordinal", ("", end_ordinal.to_string().as_str())) - .change("executed_code", ("", executed_code.to_string().as_str())) - .change("gas_consumed", ("", gas_consumed.to_string().as_str())) - .change("gas_limit", ("", gas_limit.to_string().as_str())) - .change("index", ("", index.to_string().as_str())) - .change("input", ("", input.as_str())) - .change("parent_index", ("", parent_index.to_string().as_str())) - .change("state_reverted", ("", state_reverted.to_string().as_str())) - .change("status_failed", ("", status_failed.to_string().as_str())) - .change("status_reverted", ("", status_reverted.to_string().as_str())) - .change("suicide", ("", suicide.to_string().as_str())) - .change("value", ("", value.as_str())) - .change("failure_reason", ("", failure_reason.as_str())) - .change("return_data", ("", return_data.as_str())); -} - pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp, detail_level: &str) -> Vec { // Only required DetailLevel=EXTENDED if detail_level != "Extended" { @@ -145,7 +29,9 @@ pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp, detail_level: & let mut traces: Vec = vec![]; - // Collect system traces + // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L121-L124 + // DetailLevel: EXTENDED + // System calls are introduced in Cancun, along with blobs. They are executed outside of transactions but affect the state. for call in &block.system_calls { traces.push(RawTrace { block_time: Some(timestamp.time), diff --git a/blocks/evm/src/transactions.rs b/blocks/evm/src/transactions.rs index 8b42e0b..343fdd7 100644 --- a/blocks/evm/src/transactions.rs +++ b/blocks/evm/src/transactions.rs @@ -1,20 +1,11 @@ -use core::time; - -use common::blocks::insert_timestamp; use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; use common::utils::optional_bigint_to_string; -use substreams::pb::substreams::Clock; use substreams_database_change::pb::database::TableChange; -use substreams_database_change::pb::database::{table_change, DatabaseChanges}; use substreams_ethereum::pb::eth::v2::Block; -use substreams_ethereum::pb::eth::v2::BlockHeader; use substreams_ethereum::pb::eth::v2::TransactionTrace; -use crate::keys::transaction_keys; -use crate::logs::insert_log; use crate::pb::evm::Transaction as RawTransaction; -use crate::traces::insert_trace; pub fn transaction_type_to_string(r#type: i32) -> String { match r#type { @@ -48,96 +39,6 @@ pub fn is_transaction_success(status: i32) -> bool { status == 1 } -// https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L658 -// DetailLevel: BASE & EXTENDED -pub fn insert_transaction(tables: &mut DatabaseChanges, clock: &Clock, transaction: &TransactionTrace, block_header: &BlockHeader, detail_level: &String) { - let index = transaction.index; - let hash = bytes_to_hex(&transaction.hash); - let from = bytes_to_hex(&transaction.from); // EVM Address - let to = bytes_to_hex(&transaction.to); // EVM Address - let nonce = transaction.nonce; - let gas_used = transaction.gas_used; // TO-DO: rename to `gas`? https://github.com/pinax-network/substreams-raw-blocks/issues/1 - let gas_price = optional_bigint_to_string(&transaction.gas_price, "0"); // UInt256 - let gas_limit = transaction.gas_limit; - let value = optional_bigint_to_string(&transaction.value, "0"); // UInt256 - let data = bytes_to_hex(&transaction.input); // TO-DO: change to 0x? https://github.com/pinax-network/substreams-raw-blocks/issues/1 - let v = bytes_to_hex(&transaction.v); - let r = bytes_to_hex(&transaction.r); - let s = bytes_to_hex(&transaction.s); - let r#type = transaction_type_to_string(transaction.r#type); - let type_code = transaction.r#type; - let max_fee_per_gas = optional_bigint_to_string(&transaction.max_fee_per_gas, "0"); // UInt256 - let max_priority_fee_per_gas = optional_bigint_to_string(&transaction.max_priority_fee_per_gas, "0"); // UInt256 - let begin_ordinal = transaction.begin_ordinal; - let end_ordinal = transaction.end_ordinal; - let success = is_transaction_success(transaction.status); - let status = transaction_status_to_string(transaction.status); - let status_code = transaction.status; - - // transaction receipt - let receipt = transaction.receipt.clone().unwrap(); - let blob_gas_price = optional_bigint_to_string(&receipt.blob_gas_price, "0"); - let blob_gas_used = receipt.blob_gas_used(); - let cumulative_gas_used = receipt.cumulative_gas_used; - let logs_bloom = bytes_to_hex(&receipt.logs_bloom); - let state_root = bytes_to_hex(&receipt.state_root); - - // block roots - let transactions_root = bytes_to_hex(&block_header.transactions_root); - let receipts_root = bytes_to_hex(&block_header.receipt_root); - - let keys = transaction_keys(&clock, &hash); - let row = tables - .push_change_composite("transactions", keys, 0, table_change::Operation::Create) - .change("index", ("", index.to_string().as_str())) - .change("hash", ("", hash.as_str())) - .change("from", ("", from.as_str())) - .change("to", ("", to.as_str())) - .change("nonce", ("", nonce.to_string().as_str())) - .change("gas_used", ("", gas_used.to_string().as_str())) - .change("gas_price", ("", gas_price.to_string().as_str())) - .change("gas_limit", ("", gas_limit.to_string().as_str())) - .change("value", ("", value.as_str())) - .change("data", ("", data.as_str())) - .change("v", ("", v.as_str())) - .change("r", ("", r.as_str())) - .change("s", ("", s.as_str())) - .change("r", ("", r.as_str())) - .change("type", ("", r#type.as_str())) - .change("type_code", ("", type_code.to_string().as_str())) - .change("max_fee_per_gas", ("", max_fee_per_gas.as_str())) - .change("max_priority_fee_per_gas", ("", max_priority_fee_per_gas.as_str())) - .change("begin_ordinal", ("", begin_ordinal.to_string().as_str())) - .change("end_ordinal", ("", end_ordinal.to_string().as_str())) - .change("success", ("", success.to_string().as_str())) - .change("status", ("", status.as_str())) - .change("status_code", ("", status_code.to_string().as_str())) - // transaction receipt - .change("blob_gas_price", ("", blob_gas_price.as_str())) - .change("blob_gas_used", ("", blob_gas_used.to_string().as_str())) - .change("cumulative_gas_used", ("", cumulative_gas_used.to_string().as_str())) - .change("logs_bloom", ("", logs_bloom.as_str())) - .change("state_root", ("", state_root.as_str())) - // block roots - .change("transactions_root", ("", transactions_root.as_str())) - .change("receipts_root", ("", receipts_root.as_str())); - - insert_timestamp(row, clock, false, true); - - // TABLE::traces - for call in transaction.calls() { - insert_trace(tables, clock, call.call, call.transaction); - } - - // TABLE::logs - // Only required DetailLevel=BASE since traces are not available in BASE - if detail_level == "Base" { - for log in receipt.logs { - insert_log(tables, clock, &log, transaction); - } - } -} - pub fn collect_transactions(block: &Block, timestamp: &BlockTimestamp) -> Vec { let block_header = block.header.as_ref().unwrap(); From b43648f9a4d0e876557cc86c8ca472ecfea09f05 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Tue, 19 Nov 2024 10:28:54 +0100 Subject: [PATCH 11/20] rename Raw imports --- blocks/evm/README.md | 14 ++ blocks/evm/src/account_creations.rs | 10 +- blocks/evm/src/balance_changes.rs | 10 +- blocks/evm/src/blocks.rs | 6 +- blocks/evm/src/code_changes.rs | 10 +- blocks/evm/src/gas_changes.rs | 10 +- blocks/evm/src/logs.rs | 8 +- blocks/evm/src/nonce_changes.rs | 10 +- blocks/evm/src/pb/evm.rs | 356 ++++++++++++++-------------- blocks/evm/src/storage_changes.rs | 10 +- blocks/evm/src/traces.rs | 10 +- blocks/evm/src/transactions.rs | 46 +--- 12 files changed, 237 insertions(+), 263 deletions(-) diff --git a/blocks/evm/README.md b/blocks/evm/README.md index eeffcd0..2861e57 100644 --- a/blocks/evm/README.md +++ b/blocks/evm/README.md @@ -17,6 +17,8 @@ - [x] **Gas Changes** - [x] **Nonce Changes** +## Graph + ```mermaid graph TD; raw[sf.ethereum.type.v2.Block]; @@ -33,3 +35,15 @@ graph TD; extended --> gas_changes; extended --> nonce_changes; ``` + +## Modules + +```bash +Name: map_events +Initial block: 0 +Kind: map +Input: source: sf.substreams.v1.Clock +Input: source: sf.ethereum.type.v2.Block +Output Type: proto:evm.Events +Hash: e9a39ec8c7084a493d9a9f60c1f2f5d18f7505ad +``` diff --git a/blocks/evm/src/account_creations.rs b/blocks/evm/src/account_creations.rs index a935a24..9268ef7 100644 --- a/blocks/evm/src/account_creations.rs +++ b/blocks/evm/src/account_creations.rs @@ -3,17 +3,17 @@ use common::utils::bytes_to_hex; use substreams_ethereum::pb::eth::v2::Block; -use crate::pb::evm::AccountCreation as RawAccountCreation; +use crate::pb::evm::AccountCreation; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L736 // DetailLevel: EXTENDED -pub fn collect_account_creations(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut account_creations: Vec = vec![]; +pub fn collect_account_creations(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut account_creations: Vec = vec![]; // Collect account creations from system calls for call in &block.system_calls { for account_creation in &call.account_creations { - account_creations.push(RawAccountCreation { + account_creations.push(AccountCreation { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), @@ -28,7 +28,7 @@ pub fn collect_account_creations(block: &Block, timestamp: &BlockTimestamp) -> V for transaction in &block.transaction_traces { for call in &transaction.calls { for account_creation in &call.account_creations { - account_creations.push(RawAccountCreation { + account_creations.push(AccountCreation { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), diff --git a/blocks/evm/src/balance_changes.rs b/blocks/evm/src/balance_changes.rs index f306bf3..62b3ee8 100644 --- a/blocks/evm/src/balance_changes.rs +++ b/blocks/evm/src/balance_changes.rs @@ -3,7 +3,7 @@ use common::utils::optional_bigint_to_string; use common::utils::{bytes_to_hex, optional_bigint_to_decimal}; use substreams_ethereum::pb::eth::v2::Block; -use crate::pb::evm::BalanceChange as RawBalanceChange; +use crate::pb::evm::BalanceChange; pub fn balance_change_reason_to_string(reason: i32) -> String { match reason { @@ -35,14 +35,14 @@ pub fn balance_change_reason_to_string(reason: i32) -> String { // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L658 // DetailLevel: EXTENDED -pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut balance_changes: Vec = vec![]; +pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut balance_changes: Vec = vec![]; // Collect balance changes from system calls for call in &block.system_calls { for balance_change in &call.balance_changes { let amount = optional_bigint_to_decimal(balance_change.new_value.clone()) - optional_bigint_to_decimal(balance_change.old_value.clone()); - balance_changes.push(RawBalanceChange { + balance_changes.push(BalanceChange { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), @@ -63,7 +63,7 @@ pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec for call in &transaction.calls { for balance_change in &call.balance_changes { let amount = optional_bigint_to_decimal(balance_change.new_value.clone()) - optional_bigint_to_decimal(balance_change.old_value.clone()); - balance_changes.push(RawBalanceChange { + balance_changes.push(BalanceChange { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), diff --git a/blocks/evm/src/blocks.rs b/blocks/evm/src/blocks.rs index 12fa8d2..0775cec 100644 --- a/blocks/evm/src/blocks.rs +++ b/blocks/evm/src/blocks.rs @@ -4,7 +4,7 @@ use common::utils::{bytes_to_hex, optional_bigint_to_u64, optional_u64_to_string use substreams_ethereum::pb::eth::v2::Block; -use crate::pb::evm::Block as RawBlock; +use crate::pb::evm::Block as BlockHeader; pub fn block_detail_to_string(detail_level: i32) -> String { match detail_level { @@ -17,7 +17,7 @@ pub fn block_detail_to_string(detail_level: i32) -> String { // https://github.com/streamingfast/firehose-ethereum/blob/develop/proto/sf/ethereum/type/v2/type.proto // DetailLevel: BASE -pub fn collect_block(block: &Block, timestamp: &BlockTimestamp) -> RawBlock { +pub fn collect_block(block: &Block, timestamp: &BlockTimestamp) -> BlockHeader { let header = block.header.as_ref().unwrap(); let total_transactions = block.transaction_traces.len() as u64; @@ -25,7 +25,7 @@ pub fn collect_block(block: &Block, timestamp: &BlockTimestamp) -> RawBlock { let failed_transactions = total_transactions - successful_transactions; let total_withdrawals = block.balance_changes.iter().filter(|t| t.reason == 16).count() as u64; - RawBlock { + BlockHeader { time: Some(timestamp.time), number: header.number, date: timestamp.date.clone(), diff --git a/blocks/evm/src/code_changes.rs b/blocks/evm/src/code_changes.rs index 0f218ba..733898f 100644 --- a/blocks/evm/src/code_changes.rs +++ b/blocks/evm/src/code_changes.rs @@ -2,17 +2,17 @@ use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; use substreams_ethereum::pb::eth::v2::Block; -use crate::pb::evm::CodeChange as RawCodeChange; +use crate::pb::evm::CodeChange; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L744 // DetailLevel: EXTENDED -pub fn collect_code_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut code_changes: Vec = vec![]; +pub fn collect_code_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut code_changes: Vec = vec![]; // Collect code changes from system calls for call in &block.system_calls { for code_change in &call.code_changes { - code_changes.push(RawCodeChange { + code_changes.push(CodeChange { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), @@ -31,7 +31,7 @@ pub fn collect_code_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec String { match reason { @@ -37,13 +37,13 @@ pub fn gas_change_reason_to_string(reason: i32) -> String { // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L726C9-L726C20 // DetailLevel: EXTENDED -pub fn collect_gas_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut gas_changes: Vec = vec![]; +pub fn collect_gas_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut gas_changes: Vec = vec![]; // Collect gas changes from system calls for call in &block.system_calls { for gas_change in &call.gas_changes { - gas_changes.push(RawGasChange { + gas_changes.push(GasChange { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), @@ -61,7 +61,7 @@ pub fn collect_gas_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec Vec { +pub fn collect_logs(block: &Block, timestamp: &BlockTimestamp, detail_level: &str) -> Vec { // Only required DetailLevel=BASE since traces are not available in BASE if detail_level == "Base" { return vec![]; } - let mut logs: Vec = vec![]; + let mut logs: Vec = vec![]; for transaction in &block.transaction_traces { let receipt = transaction.receipt.as_ref().unwrap(); for log in &receipt.logs { - logs.push(RawLog { + logs.push(Log { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), diff --git a/blocks/evm/src/nonce_changes.rs b/blocks/evm/src/nonce_changes.rs index 61e9b5b..93296c1 100644 --- a/blocks/evm/src/nonce_changes.rs +++ b/blocks/evm/src/nonce_changes.rs @@ -2,17 +2,17 @@ use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; use substreams_ethereum::pb::eth::v2::Block; -use crate::pb::evm::NonceChange as RawNonceChange; +use crate::pb::evm::NonceChange; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L726C9-L726C20 // DetailLevel: EXTENDED -pub fn collect_nonce_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut nonce_changes: Vec = vec![]; +pub fn collect_nonce_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut nonce_changes: Vec = vec![]; // Collect nonce changes from system calls for call in &block.system_calls { for nonce_change in &call.nonce_changes { - nonce_changes.push(RawNonceChange { + nonce_changes.push(NonceChange { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), @@ -29,7 +29,7 @@ pub fn collect_nonce_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec, - #[prost(message, repeated, tag = "2")] + #[prost(message, repeated, tag="2")] pub transactions: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag = "3")] + #[prost(message, repeated, tag="3")] pub logs: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag = "4")] + #[prost(message, repeated, tag="4")] pub traces: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag = "5")] + #[prost(message, repeated, tag="5")] pub balance_changes: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag = "6")] + #[prost(message, repeated, tag="6")] pub storage_changes: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag = "7")] + #[prost(message, repeated, tag="7")] pub code_changes: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag = "8")] + #[prost(message, repeated, tag="8")] pub account_creations: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag = "9")] + #[prost(message, repeated, tag="9")] pub nonce_changes: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag = "10")] + #[prost(message, repeated, tag="10")] pub gas_changes: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Block { - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag="1")] pub time: ::core::option::Option<::prost_types::Timestamp>, - #[prost(uint64, tag = "2")] + #[prost(uint64, tag="2")] pub number: u64, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub date: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub hash: ::prost::alloc::string::String, - #[prost(string, tag = "5")] + #[prost(string, tag="5")] pub parent_hash: ::prost::alloc::string::String, - #[prost(uint64, tag = "6")] + #[prost(uint64, tag="6")] pub nonce: u64, - #[prost(string, tag = "7")] + #[prost(string, tag="7")] pub ommers_hash: ::prost::alloc::string::String, - #[prost(string, tag = "8")] + #[prost(string, tag="8")] pub logs_bloom: ::prost::alloc::string::String, - #[prost(string, tag = "9")] + #[prost(string, tag="9")] pub transactions_root: ::prost::alloc::string::String, - #[prost(string, tag = "10")] + #[prost(string, tag="10")] pub state_root: ::prost::alloc::string::String, - #[prost(string, tag = "11")] + #[prost(string, tag="11")] pub receipts_root: ::prost::alloc::string::String, - #[prost(string, tag = "12")] + #[prost(string, tag="12")] pub withdrawals_root: ::prost::alloc::string::String, - #[prost(string, tag = "13")] + #[prost(string, tag="13")] pub parent_beacon_root: ::prost::alloc::string::String, - #[prost(string, tag = "14")] + #[prost(string, tag="14")] pub miner: ::prost::alloc::string::String, - #[prost(uint64, tag = "15")] + #[prost(uint64, tag="15")] pub difficulty: u64, - #[prost(string, tag = "16")] + #[prost(string, tag="16")] pub total_difficulty: ::prost::alloc::string::String, - #[prost(string, tag = "17")] + #[prost(string, tag="17")] pub mix_hash: ::prost::alloc::string::String, - #[prost(string, tag = "18")] + #[prost(string, tag="18")] pub extra_data: ::prost::alloc::string::String, - #[prost(string, tag = "19")] + #[prost(string, tag="19")] pub extra_data_utf8: ::prost::alloc::string::String, - #[prost(uint64, tag = "20")] + #[prost(uint64, tag="20")] pub gas_limit: u64, - #[prost(uint64, tag = "21")] + #[prost(uint64, tag="21")] pub gas_used: u64, - #[prost(string, tag = "22")] + #[prost(string, tag="22")] pub base_fee_per_gas: ::prost::alloc::string::String, - #[prost(string, tag = "23")] + #[prost(string, tag="23")] pub blob_gas_used: ::prost::alloc::string::String, - #[prost(string, tag = "24")] + #[prost(string, tag="24")] pub excess_blob_gas: ::prost::alloc::string::String, - #[prost(uint64, tag = "25")] + #[prost(uint64, tag="25")] pub size: u64, - #[prost(uint64, tag = "26")] + #[prost(uint64, tag="26")] pub total_transactions: u64, - #[prost(uint64, tag = "27")] + #[prost(uint64, tag="27")] pub successful_transactions: u64, - #[prost(uint64, tag = "28")] + #[prost(uint64, tag="28")] pub failed_transactions: u64, - #[prost(uint64, tag = "29")] + #[prost(uint64, tag="29")] pub total_balance_changes: u64, - #[prost(uint64, tag = "30")] + #[prost(uint64, tag="30")] pub total_withdrawals: u64, - #[prost(string, tag = "31")] + #[prost(string, tag="31")] pub detail_level: ::prost::alloc::string::String, - #[prost(uint32, tag = "32")] + #[prost(uint32, tag="32")] pub detail_level_code: u32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Transaction { - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, - #[prost(uint64, tag = "2")] + #[prost(uint64, tag="2")] pub block_number: u64, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub block_hash: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, - #[prost(string, tag = "5")] + #[prost(string, tag="5")] pub transactions_root: ::prost::alloc::string::String, - #[prost(string, tag = "6")] + #[prost(string, tag="6")] pub receipts_root: ::prost::alloc::string::String, - #[prost(uint32, tag = "7")] + #[prost(uint32, tag="7")] pub index: u32, - #[prost(string, tag = "8")] + #[prost(string, tag="8")] pub hash: ::prost::alloc::string::String, - #[prost(string, tag = "9")] + #[prost(string, tag="9")] pub from: ::prost::alloc::string::String, - #[prost(string, tag = "10")] + #[prost(string, tag="10")] pub to: ::prost::alloc::string::String, - #[prost(uint64, tag = "11")] + #[prost(uint64, tag="11")] pub nonce: u64, - #[prost(string, tag = "12")] + #[prost(string, tag="12")] pub status: ::prost::alloc::string::String, - #[prost(uint32, tag = "13")] + #[prost(uint32, tag="13")] pub status_code: u32, - #[prost(bool, tag = "14")] + #[prost(bool, tag="14")] pub success: bool, - #[prost(string, tag = "15")] + #[prost(string, tag="15")] pub gas_price: ::prost::alloc::string::String, - #[prost(uint64, tag = "16")] + #[prost(uint64, tag="16")] pub gas_limit: u64, - #[prost(string, tag = "17")] + #[prost(string, tag="17")] pub value: ::prost::alloc::string::String, - #[prost(string, tag = "18")] + #[prost(string, tag="18")] pub data: ::prost::alloc::string::String, - #[prost(string, tag = "19")] + #[prost(string, tag="19")] pub v: ::prost::alloc::string::String, - #[prost(string, tag = "20")] + #[prost(string, tag="20")] pub r: ::prost::alloc::string::String, - #[prost(string, tag = "21")] + #[prost(string, tag="21")] pub s: ::prost::alloc::string::String, - #[prost(uint64, tag = "22")] + #[prost(uint64, tag="22")] pub gas_used: u64, - #[prost(string, tag = "23")] + #[prost(string, tag="23")] pub r#type: ::prost::alloc::string::String, - #[prost(uint32, tag = "24")] + #[prost(uint32, tag="24")] pub type_code: u32, - #[prost(string, tag = "25")] + #[prost(string, tag="25")] pub max_fee_per_gas: ::prost::alloc::string::String, - #[prost(string, tag = "26")] + #[prost(string, tag="26")] pub max_priority_fee_per_gas: ::prost::alloc::string::String, - #[prost(uint64, tag = "27")] + #[prost(uint64, tag="27")] pub begin_ordinal: u64, - #[prost(uint64, tag = "28")] + #[prost(uint64, tag="28")] pub end_ordinal: u64, - #[prost(string, tag = "29")] + #[prost(string, tag="29")] pub blob_gas_price: ::prost::alloc::string::String, - #[prost(uint64, tag = "30")] + #[prost(uint64, tag="30")] pub blob_gas_used: u64, - #[prost(uint64, tag = "31")] + #[prost(uint64, tag="31")] pub cumulative_gas_used: u64, - #[prost(string, tag = "32")] + #[prost(string, tag="32")] pub logs_bloom: ::prost::alloc::string::String, - #[prost(string, tag = "33")] + #[prost(string, tag="33")] pub state_root: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Log { - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, - #[prost(uint64, tag = "2")] + #[prost(uint64, tag="2")] pub block_number: u64, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub block_hash: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, - #[prost(string, tag = "5")] + #[prost(string, tag="5")] pub tx_hash: ::prost::alloc::string::String, - #[prost(uint32, tag = "6")] + #[prost(uint32, tag="6")] pub tx_index: u32, - #[prost(string, tag = "7")] + #[prost(string, tag="7")] pub tx_status: ::prost::alloc::string::String, - #[prost(uint32, tag = "8")] + #[prost(uint32, tag="8")] pub tx_status_code: u32, - #[prost(bool, tag = "9")] + #[prost(bool, tag="9")] pub tx_success: bool, - #[prost(string, tag = "10")] + #[prost(string, tag="10")] pub tx_from: ::prost::alloc::string::String, - #[prost(string, tag = "11")] + #[prost(string, tag="11")] pub tx_to: ::prost::alloc::string::String, - #[prost(uint32, tag = "12")] + #[prost(uint32, tag="12")] pub index: u32, - #[prost(uint32, tag = "13")] + #[prost(uint32, tag="13")] pub block_index: u32, - #[prost(string, tag = "14")] + #[prost(string, tag="14")] pub contract_address: ::prost::alloc::string::String, - #[prost(string, tag = "15")] + #[prost(string, tag="15")] pub topic0: ::prost::alloc::string::String, - #[prost(string, tag = "16")] + #[prost(string, tag="16")] pub topic1: ::prost::alloc::string::String, - #[prost(string, tag = "17")] + #[prost(string, tag="17")] pub topic2: ::prost::alloc::string::String, - #[prost(string, tag = "18")] + #[prost(string, tag="18")] pub topic3: ::prost::alloc::string::String, - #[prost(string, tag = "19")] + #[prost(string, tag="19")] pub data: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Trace { - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, - #[prost(uint64, tag = "2")] + #[prost(uint64, tag="2")] pub block_number: u64, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub block_hash: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, - #[prost(string, tag = "5")] + #[prost(string, tag="5")] pub tx_hash: ::prost::alloc::string::String, - #[prost(uint32, tag = "6")] + #[prost(uint32, tag="6")] pub tx_index: u32, - #[prost(string, tag = "7")] + #[prost(string, tag="7")] pub tx_status: ::prost::alloc::string::String, - #[prost(uint32, tag = "8")] + #[prost(uint32, tag="8")] pub tx_status_code: u32, - #[prost(bool, tag = "9")] + #[prost(bool, tag="9")] pub tx_success: bool, - #[prost(string, tag = "10")] + #[prost(string, tag="10")] pub from: ::prost::alloc::string::String, - #[prost(string, tag = "11")] + #[prost(string, tag="11")] pub to: ::prost::alloc::string::String, - #[prost(uint32, tag = "12")] + #[prost(uint32, tag="12")] pub index: u32, - #[prost(uint32, tag = "13")] + #[prost(uint32, tag="13")] pub parent_index: u32, - #[prost(uint32, tag = "14")] + #[prost(uint32, tag="14")] pub depth: u32, - #[prost(string, tag = "15")] + #[prost(string, tag="15")] pub caller: ::prost::alloc::string::String, - #[prost(string, tag = "16")] + #[prost(string, tag="16")] pub call_type: ::prost::alloc::string::String, - #[prost(uint32, tag = "17")] + #[prost(uint32, tag="17")] pub call_type_code: u32, - #[prost(string, tag = "18")] + #[prost(string, tag="18")] pub address: ::prost::alloc::string::String, - #[prost(string, tag = "19")] + #[prost(string, tag="19")] pub value: ::prost::alloc::string::String, - #[prost(uint64, tag = "20")] + #[prost(uint64, tag="20")] pub gas_limit: u64, - #[prost(uint64, tag = "21")] + #[prost(uint64, tag="21")] pub gas_consumed: u64, - #[prost(string, tag = "22")] + #[prost(string, tag="22")] pub return_data: ::prost::alloc::string::String, - #[prost(string, tag = "23")] + #[prost(string, tag="23")] pub input: ::prost::alloc::string::String, - #[prost(bool, tag = "24")] + #[prost(bool, tag="24")] pub suicide: bool, - #[prost(string, tag = "25")] + #[prost(string, tag="25")] pub failure_reason: ::prost::alloc::string::String, - #[prost(bool, tag = "26")] + #[prost(bool, tag="26")] pub state_reverted: bool, - #[prost(bool, tag = "27")] + #[prost(bool, tag="27")] pub status_reverted: bool, - #[prost(bool, tag = "28")] + #[prost(bool, tag="28")] pub status_failed: bool, - #[prost(bool, tag = "29")] + #[prost(bool, tag="29")] pub executed_code: bool, - #[prost(uint64, tag = "30")] + #[prost(uint64, tag="30")] pub begin_ordinal: u64, - #[prost(uint64, tag = "31")] + #[prost(uint64, tag="31")] pub end_ordinal: u64, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct BalanceChange { - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, - #[prost(uint64, tag = "2")] + #[prost(uint64, tag="2")] pub block_number: u64, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub block_hash: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, - #[prost(string, tag = "5")] + #[prost(string, tag="5")] pub address: ::prost::alloc::string::String, - #[prost(string, tag = "6")] + #[prost(string, tag="6")] pub new_balance: ::prost::alloc::string::String, - #[prost(string, tag = "7")] + #[prost(string, tag="7")] pub old_balance: ::prost::alloc::string::String, - #[prost(string, tag = "8")] + #[prost(string, tag="8")] pub amount: ::prost::alloc::string::String, - #[prost(uint64, tag = "9")] + #[prost(uint64, tag="9")] pub ordinal: u64, - #[prost(string, tag = "10")] + #[prost(string, tag="10")] pub reason: ::prost::alloc::string::String, - #[prost(uint32, tag = "11")] + #[prost(uint32, tag="11")] pub reason_code: u32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct StorageChange { - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, - #[prost(uint64, tag = "2")] + #[prost(uint64, tag="2")] pub block_number: u64, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub block_hash: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, - #[prost(uint64, tag = "5")] + #[prost(uint64, tag="5")] pub ordinal: u64, - #[prost(string, tag = "6")] + #[prost(string, tag="6")] pub address: ::prost::alloc::string::String, - #[prost(string, tag = "7")] + #[prost(string, tag="7")] pub key: ::prost::alloc::string::String, - #[prost(string, tag = "8")] + #[prost(string, tag="8")] pub new_value: ::prost::alloc::string::String, - #[prost(string, tag = "9")] + #[prost(string, tag="9")] pub old_value: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CodeChange { - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, - #[prost(uint64, tag = "2")] + #[prost(uint64, tag="2")] pub block_number: u64, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub block_hash: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, - #[prost(uint64, tag = "5")] + #[prost(uint64, tag="5")] pub ordinal: u64, - #[prost(string, tag = "6")] + #[prost(string, tag="6")] pub address: ::prost::alloc::string::String, - #[prost(string, tag = "7")] + #[prost(string, tag="7")] pub old_hash: ::prost::alloc::string::String, - #[prost(string, tag = "8")] + #[prost(string, tag="8")] pub old_code: ::prost::alloc::string::String, - #[prost(string, tag = "9")] + #[prost(string, tag="9")] pub new_hash: ::prost::alloc::string::String, - #[prost(string, tag = "10")] + #[prost(string, tag="10")] pub new_code: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountCreation { - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, - #[prost(uint64, tag = "2")] + #[prost(uint64, tag="2")] pub block_number: u64, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub block_hash: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, - #[prost(uint64, tag = "5")] + #[prost(uint64, tag="5")] pub ordinal: u64, - #[prost(string, tag = "6")] + #[prost(string, tag="6")] pub account: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct NonceChange { - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, - #[prost(uint64, tag = "2")] + #[prost(uint64, tag="2")] pub block_number: u64, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub block_hash: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, - #[prost(uint64, tag = "5")] + #[prost(uint64, tag="5")] pub ordinal: u64, - #[prost(string, tag = "6")] + #[prost(string, tag="6")] pub address: ::prost::alloc::string::String, - #[prost(uint64, tag = "7")] + #[prost(uint64, tag="7")] pub old_value: u64, - #[prost(uint64, tag = "8")] + #[prost(uint64, tag="8")] pub new_value: u64, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GasChange { - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, - #[prost(uint64, tag = "2")] + #[prost(uint64, tag="2")] pub block_number: u64, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub block_hash: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, - #[prost(uint64, tag = "5")] + #[prost(uint64, tag="5")] pub ordinal: u64, - #[prost(uint64, tag = "6")] + #[prost(uint64, tag="6")] pub old_value: u64, - #[prost(uint64, tag = "7")] + #[prost(uint64, tag="7")] pub new_value: u64, - #[prost(string, tag = "8")] + #[prost(string, tag="8")] pub reason: ::prost::alloc::string::String, - #[prost(uint32, tag = "9")] + #[prost(uint32, tag="9")] pub reason_code: u32, } // @@protoc_insertion_point(module) diff --git a/blocks/evm/src/storage_changes.rs b/blocks/evm/src/storage_changes.rs index 158949c..50e598f 100644 --- a/blocks/evm/src/storage_changes.rs +++ b/blocks/evm/src/storage_changes.rs @@ -3,17 +3,17 @@ use common::utils::bytes_to_hex; use substreams_ethereum::pb::eth::v2::Block; -use crate::pb::evm::StorageChange as RawStorageChange; +use crate::pb::evm::StorageChange; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L647 // DetailLevel: EXTENDED -pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut storage_changes: Vec = vec![]; +pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut storage_changes: Vec = vec![]; // Collect storage changes from system calls for call in &block.system_calls { for storage_change in &call.storage_changes { - storage_changes.push(RawStorageChange { + storage_changes.push(StorageChange { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), @@ -31,7 +31,7 @@ pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec for transaction in &block.transaction_traces { for call in &transaction.calls { for storage_change in &call.storage_changes { - storage_changes.push(RawStorageChange { + storage_changes.push(StorageChange { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), diff --git a/blocks/evm/src/traces.rs b/blocks/evm/src/traces.rs index 3bf2d9c..bd8de4d 100644 --- a/blocks/evm/src/traces.rs +++ b/blocks/evm/src/traces.rs @@ -5,7 +5,7 @@ use common::{ use substreams_ethereum::pb::eth::v2::Block; use crate::{ - pb::evm::Trace as RawTrace, + pb::evm::Trace, transactions::{is_transaction_success, transaction_status_to_string}, }; @@ -21,19 +21,19 @@ pub fn call_types_to_string(call_type: i32) -> String { } } -pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp, detail_level: &str) -> Vec { +pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp, detail_level: &str) -> Vec { // Only required DetailLevel=EXTENDED if detail_level != "Extended" { return vec![]; } - let mut traces: Vec = vec![]; + let mut traces: Vec = vec![]; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L121-L124 // DetailLevel: EXTENDED // System calls are introduced in Cancun, along with blobs. They are executed outside of transactions but affect the state. for call in &block.system_calls { - traces.push(RawTrace { + traces.push(Trace { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), @@ -73,7 +73,7 @@ pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp, detail_level: & // Collect transaction traces for transaction in &block.transaction_traces { for call in &transaction.calls { - traces.push(RawTrace { + traces.push(Trace { block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), diff --git a/blocks/evm/src/transactions.rs b/blocks/evm/src/transactions.rs index 343fdd7..f733689 100644 --- a/blocks/evm/src/transactions.rs +++ b/blocks/evm/src/transactions.rs @@ -1,11 +1,9 @@ use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; use common::utils::optional_bigint_to_string; -use substreams_database_change::pb::database::TableChange; use substreams_ethereum::pb::eth::v2::Block; -use substreams_ethereum::pb::eth::v2::TransactionTrace; -use crate::pb::evm::Transaction as RawTransaction; +use crate::pb::evm::Transaction; pub fn transaction_type_to_string(r#type: i32) -> String { match r#type { @@ -39,7 +37,7 @@ pub fn is_transaction_success(status: i32) -> bool { status == 1 } -pub fn collect_transactions(block: &Block, timestamp: &BlockTimestamp) -> Vec { +pub fn collect_transactions(block: &Block, timestamp: &BlockTimestamp) -> Vec { let block_header = block.header.as_ref().unwrap(); block @@ -47,7 +45,7 @@ pub fn collect_transactions(block: &Block, timestamp: &BlockTimestamp) -> Vec Vec Date: Tue, 19 Nov 2024 10:51:25 +0100 Subject: [PATCH 12/20] add parquet uint256 --- blocks/evm/src/pb/evm.rs | 58 ++++++++++++++++++--- blocks/evm/src/pb/mod.rs | 5 ++ blocks/evm/src/pb/parquet.rs | 80 +++++++++++++++++++++++++++++ blocks/evm/substreams.yaml | 1 + proto/evm.proto | 97 ++++++++++++++++++++++++++---------- proto/parquet/options.proto | 40 +++++++++++++++ 6 files changed, 248 insertions(+), 33 deletions(-) create mode 100644 blocks/evm/src/pb/parquet.rs create mode 100644 proto/parquet/options.proto diff --git a/blocks/evm/src/pb/evm.rs b/blocks/evm/src/pb/evm.rs index f0134ff..b337562 100644 --- a/blocks/evm/src/pb/evm.rs +++ b/blocks/evm/src/pb/evm.rs @@ -27,6 +27,7 @@ pub struct Events { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Block { + /// -- clock -- #[prost(message, optional, tag="1")] pub time: ::core::option::Option<::prost_types::Timestamp>, #[prost(uint64, tag="2")] @@ -35,6 +36,7 @@ pub struct Block { pub date: ::prost::alloc::string::String, #[prost(string, tag="4")] pub hash: ::prost::alloc::string::String, + /// -- header -- #[prost(string, tag="5")] pub parent_hash: ::prost::alloc::string::String, #[prost(uint64, tag="6")] @@ -49,8 +51,10 @@ pub struct Block { pub state_root: ::prost::alloc::string::String, #[prost(string, tag="11")] pub receipts_root: ::prost::alloc::string::String, + /// EVM Root EIP-4895 (Shangai Fork) #[prost(string, tag="12")] pub withdrawals_root: ::prost::alloc::string::String, + /// EVM Root EIP-4788 (Dencun Fork) #[prost(string, tag="13")] pub parent_beacon_root: ::prost::alloc::string::String, #[prost(string, tag="14")] @@ -69,12 +73,18 @@ pub struct Block { pub gas_limit: u64, #[prost(uint64, tag="21")] pub gas_used: u64, + /// EIP-1559 (London Fork) #[prost(string, tag="22")] pub base_fee_per_gas: ::prost::alloc::string::String, + /// EIP-4844 (Dencun Fork) #[prost(string, tag="23")] pub blob_gas_used: ::prost::alloc::string::String, + /// EIP-4844 (Dencun Fork) #[prost(string, tag="24")] pub excess_blob_gas: ::prost::alloc::string::String, + /// -- counters -- + /// + /// block size in bytes #[prost(uint64, tag="25")] pub size: u64, #[prost(uint64, tag="26")] @@ -87,6 +97,7 @@ pub struct Block { pub total_balance_changes: u64, #[prost(uint64, tag="30")] pub total_withdrawals: u64, + /// -- detail level -- #[prost(string, tag="31")] pub detail_level: ::prost::alloc::string::String, #[prost(uint32, tag="32")] @@ -95,6 +106,7 @@ pub struct Block { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Transaction { + /// -- block -- #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, #[prost(uint64, tag="2")] @@ -103,10 +115,12 @@ pub struct Transaction { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- block roots -- #[prost(string, tag="5")] pub transactions_root: ::prost::alloc::string::String, #[prost(string, tag="6")] pub receipts_root: ::prost::alloc::string::String, + /// -- transaction -- #[prost(uint32, tag="7")] pub index: u32, #[prost(string, tag="8")] @@ -139,8 +153,10 @@ pub struct Transaction { pub s: ::prost::alloc::string::String, #[prost(uint64, tag="22")] pub gas_used: u64, + /// EIP-1559 #[prost(string, tag="23")] pub r#type: ::prost::alloc::string::String, + /// EIP-1559 #[prost(uint32, tag="24")] pub type_code: u32, #[prost(string, tag="25")] @@ -151,6 +167,7 @@ pub struct Transaction { pub begin_ordinal: u64, #[prost(uint64, tag="28")] pub end_ordinal: u64, + /// -- transaction receipt -- #[prost(string, tag="29")] pub blob_gas_price: ::prost::alloc::string::String, #[prost(uint64, tag="30")] @@ -165,6 +182,7 @@ pub struct Transaction { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Log { + /// -- block -- #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, #[prost(uint64, tag="2")] @@ -173,6 +191,7 @@ pub struct Log { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- transaction -- #[prost(string, tag="5")] pub tx_hash: ::prost::alloc::string::String, #[prost(uint32, tag="6")] @@ -187,6 +206,7 @@ pub struct Log { pub tx_from: ::prost::alloc::string::String, #[prost(string, tag="11")] pub tx_to: ::prost::alloc::string::String, + /// -- logs -- #[prost(uint32, tag="12")] pub index: u32, #[prost(uint32, tag="13")] @@ -195,18 +215,19 @@ pub struct Log { pub contract_address: ::prost::alloc::string::String, #[prost(string, tag="15")] pub topic0: ::prost::alloc::string::String, - #[prost(string, tag="16")] - pub topic1: ::prost::alloc::string::String, - #[prost(string, tag="17")] - pub topic2: ::prost::alloc::string::String, - #[prost(string, tag="18")] - pub topic3: ::prost::alloc::string::String, + #[prost(string, optional, tag="16")] + pub topic1: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag="17")] + pub topic2: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag="18")] + pub topic3: ::core::option::Option<::prost::alloc::string::String>, #[prost(string, tag="19")] pub data: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Trace { + /// -- block -- #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, #[prost(uint64, tag="2")] @@ -215,6 +236,7 @@ pub struct Trace { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- transaction -- #[prost(string, tag="5")] pub tx_hash: ::prost::alloc::string::String, #[prost(uint32, tag="6")] @@ -229,6 +251,7 @@ pub struct Trace { pub from: ::prost::alloc::string::String, #[prost(string, tag="11")] pub to: ::prost::alloc::string::String, + /// -- trace -- #[prost(uint32, tag="12")] pub index: u32, #[prost(uint32, tag="13")] @@ -249,6 +272,7 @@ pub struct Trace { pub gas_limit: u64, #[prost(uint64, tag="21")] pub gas_consumed: u64, + /// Return data is set by contract calls using RETURN or REVERT. #[prost(string, tag="22")] pub return_data: ::prost::alloc::string::String, #[prost(string, tag="23")] @@ -273,6 +297,7 @@ pub struct Trace { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct BalanceChange { + /// -- block -- #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, #[prost(uint64, tag="2")] @@ -281,6 +306,7 @@ pub struct BalanceChange { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- balance change -- #[prost(string, tag="5")] pub address: ::prost::alloc::string::String, #[prost(string, tag="6")] @@ -299,6 +325,7 @@ pub struct BalanceChange { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct StorageChange { + /// -- block -- #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, #[prost(uint64, tag="2")] @@ -307,6 +334,9 @@ pub struct StorageChange { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- storage change -- + /// + /// block global ordinal #[prost(uint64, tag="5")] pub ordinal: u64, #[prost(string, tag="6")] @@ -321,6 +351,7 @@ pub struct StorageChange { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CodeChange { + /// -- block -- #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, #[prost(uint64, tag="2")] @@ -329,6 +360,9 @@ pub struct CodeChange { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- code change -- + /// + /// block global ordinal #[prost(uint64, tag="5")] pub ordinal: u64, #[prost(string, tag="6")] @@ -345,6 +379,7 @@ pub struct CodeChange { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountCreation { + /// -- block -- #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, #[prost(uint64, tag="2")] @@ -353,6 +388,9 @@ pub struct AccountCreation { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- account creation -- + /// + /// block global ordinal #[prost(uint64, tag="5")] pub ordinal: u64, #[prost(string, tag="6")] @@ -361,6 +399,7 @@ pub struct AccountCreation { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct NonceChange { + /// -- block -- #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, #[prost(uint64, tag="2")] @@ -369,6 +408,9 @@ pub struct NonceChange { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- nonce change -- + /// + /// block global ordinal #[prost(uint64, tag="5")] pub ordinal: u64, #[prost(string, tag="6")] @@ -381,6 +423,7 @@ pub struct NonceChange { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GasChange { + /// -- block -- #[prost(message, optional, tag="1")] pub block_time: ::core::option::Option<::prost_types::Timestamp>, #[prost(uint64, tag="2")] @@ -389,6 +432,9 @@ pub struct GasChange { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- gas change -- + /// + /// block global ordinal #[prost(uint64, tag="5")] pub ordinal: u64, #[prost(uint64, tag="6")] diff --git a/blocks/evm/src/pb/mod.rs b/blocks/evm/src/pb/mod.rs index 04bd25b..065e746 100644 --- a/blocks/evm/src/pb/mod.rs +++ b/blocks/evm/src/pb/mod.rs @@ -4,3 +4,8 @@ pub mod evm { include!("evm.rs"); // @@protoc_insertion_point(evm) } +// @@protoc_insertion_point(attribute:parquet) +pub mod parquet { + include!("parquet.rs"); + // @@protoc_insertion_point(parquet) +} diff --git a/blocks/evm/src/pb/parquet.rs b/blocks/evm/src/pb/parquet.rs new file mode 100644 index 0000000..81e861a --- /dev/null +++ b/blocks/evm/src/pb/parquet.rs @@ -0,0 +1,80 @@ +// @generated +// This file is @generated by prost-build. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct Column { + /// Not implemented yet but planned so we reserved the field id now + /// optional string name = 1; + #[prost(enumeration="ColumnType", optional, tag="2")] + pub r#type: ::core::option::Option, + #[prost(enumeration="Compression", optional, tag="3")] + pub compression: ::core::option::Option, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ColumnType { + UnspecifiedColumnType = 0, + Uint256 = 1, + Int256 = 2, +} +impl ColumnType { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + ColumnType::UnspecifiedColumnType => "UNSPECIFIED_COLUMN_TYPE", + ColumnType::Uint256 => "UINT256", + ColumnType::Int256 => "INT256", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "UNSPECIFIED_COLUMN_TYPE" => Some(Self::UnspecifiedColumnType), + "UINT256" => Some(Self::Uint256), + "INT256" => Some(Self::Int256), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum Compression { + Uncompressed = 0, + Snappy = 1, + Gzip = 2, + Lz4Raw = 3, + Brotli = 4, + Zstd = 5, +} +impl Compression { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Compression::Uncompressed => "UNCOMPRESSED", + Compression::Snappy => "SNAPPY", + Compression::Gzip => "GZIP", + Compression::Lz4Raw => "LZ4_RAW", + Compression::Brotli => "BROTLI", + Compression::Zstd => "ZSTD", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "UNCOMPRESSED" => Some(Self::Uncompressed), + "SNAPPY" => Some(Self::Snappy), + "GZIP" => Some(Self::Gzip), + "LZ4_RAW" => Some(Self::Lz4Raw), + "BROTLI" => Some(Self::Brotli), + "ZSTD" => Some(Self::Zstd), + _ => None, + } + } +} +// @@protoc_insertion_point(module) diff --git a/blocks/evm/substreams.yaml b/blocks/evm/substreams.yaml index 2e0f28c..0723760 100644 --- a/blocks/evm/substreams.yaml +++ b/blocks/evm/substreams.yaml @@ -13,6 +13,7 @@ binaries: protobuf: files: - evm.proto + - parquet/options.proto importPaths: - ../../proto excludePaths: diff --git a/proto/evm.proto b/proto/evm.proto index 9ac9215..5a592a2 100644 --- a/proto/evm.proto +++ b/proto/evm.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package evm; import "google/protobuf/timestamp.proto"; +import "parquet/options.proto"; message Events { repeated Block blocks = 1; @@ -19,10 +20,13 @@ message Events { message Block { + // -- clock -- google.protobuf.Timestamp time = 1; uint64 number = 2; string date = 3; string hash = 4; + + // -- header -- string parent_hash = 5; uint64 nonce = 6; string ommers_hash = 7; @@ -30,37 +34,46 @@ message Block { string transactions_root = 9; string state_root = 10; string receipts_root = 11; - string withdrawals_root = 12; - string parent_beacon_root = 13; + string withdrawals_root = 12; // EVM Root EIP-4895 (Shangai Fork) + string parent_beacon_root = 13; // EVM Root EIP-4788 (Dencun Fork) string miner = 14; - uint64 difficulty = 15; + uint64 difficulty = 15 [(parquet.column) = {type: UINT256}]; string total_difficulty = 16; string mix_hash = 17; string extra_data = 18; string extra_data_utf8 = 19; uint64 gas_limit = 20; uint64 gas_used = 21; - string base_fee_per_gas = 22; - string blob_gas_used = 23; - string excess_blob_gas = 24; - uint64 size = 25; + string base_fee_per_gas = 22; // EIP-1559 (London Fork) + string blob_gas_used = 23; // EIP-4844 (Dencun Fork) + string excess_blob_gas = 24; // EIP-4844 (Dencun Fork) + + // -- counters -- + uint64 size = 25; // block size in bytes uint64 total_transactions = 26; uint64 successful_transactions = 27; uint64 failed_transactions = 28; uint64 total_balance_changes = 29; uint64 total_withdrawals = 30; + + // -- detail level -- string detail_level = 31; uint32 detail_level_code = 32; } message Transaction { + // -- block -- google.protobuf.Timestamp block_time = 1; uint64 block_number = 2; string block_hash = 3; string block_date = 4; + + // -- block roots -- string transactions_root = 5; string receipts_root = 6; + + // -- transaction -- uint32 index = 7; string hash = 8; string from = 9; @@ -69,21 +82,23 @@ message Transaction { string status = 12; uint32 status_code = 13; bool success = 14; - string gas_price = 15; + string gas_price = 15 [(parquet.column) = {type: UINT256}]; uint64 gas_limit = 16; - string value = 17; + string value = 17 [(parquet.column) = {type: UINT256}]; string data = 18; string v = 19; string r = 20; string s = 21; uint64 gas_used = 22; - string type = 23; - uint32 type_code = 24; - string max_fee_per_gas = 25; - string max_priority_fee_per_gas = 26; + string type = 23; // EIP-1559 + uint32 type_code = 24; // EIP-1559 + string max_fee_per_gas = 25 [(parquet.column) = {type: UINT256}]; + string max_priority_fee_per_gas = 26 [(parquet.column) = {type: UINT256}]; uint64 begin_ordinal = 27; uint64 end_ordinal = 28; - string blob_gas_price = 29; + + // -- transaction receipt -- + string blob_gas_price = 29 [(parquet.column) = {type: UINT256}]; uint64 blob_gas_used = 30; uint64 cumulative_gas_used = 31; string logs_bloom = 32; @@ -91,10 +106,13 @@ message Transaction { } message Log { + // -- block -- google.protobuf.Timestamp block_time = 1; uint64 block_number = 2; string block_hash = 3; string block_date = 4; + + // -- transaction -- string tx_hash = 5; uint32 tx_index = 6; string tx_status = 7; @@ -102,21 +120,26 @@ message Log { bool tx_success = 9; string tx_from = 10; string tx_to = 11; + + // -- logs -- uint32 index = 12; uint32 block_index = 13; string contract_address = 14; string topic0 = 15; - string topic1 = 16; - string topic2 = 17; - string topic3 = 18; + optional string topic1 = 16; + optional string topic2 = 17; + optional string topic3 = 18; string data = 19; } message Trace { + // -- block -- google.protobuf.Timestamp block_time = 1; uint64 block_number = 2; string block_hash = 3; string block_date = 4; + + // -- transaction -- string tx_hash = 5; uint32 tx_index = 6; string tx_status = 7; @@ -124,6 +147,8 @@ message Trace { bool tx_success = 9; string from = 10; string to = 11; + + // -- trace -- uint32 index = 12; uint32 parent_index = 13; uint32 depth = 14; @@ -131,10 +156,10 @@ message Trace { string call_type = 16; uint32 call_type_code = 17; string address = 18; - string value = 19; + string value = 19 [(parquet.column) = {type: UINT256}]; uint64 gas_limit = 20; uint64 gas_consumed = 21; - string return_data = 22; + string return_data = 22; // Return data is set by contract calls using RETURN or REVERT. string input = 23; bool suicide = 24; string failure_reason = 25; @@ -147,25 +172,31 @@ message Trace { } message BalanceChange { + // -- block -- google.protobuf.Timestamp block_time = 1; uint64 block_number = 2; string block_hash = 3; string block_date = 4; + + // -- balance change -- string address = 5; - string new_balance = 6; - string old_balance = 7; - string amount = 8; + string new_balance = 6 [(parquet.column) = {type: UINT256}]; + string old_balance = 7 [(parquet.column) = {type: UINT256}]; + string amount = 8 [(parquet.column) = {type: INT256}]; uint64 ordinal = 9; string reason = 10; uint32 reason_code = 11; } message StorageChange { + // -- block -- google.protobuf.Timestamp block_time = 1; uint64 block_number = 2; string block_hash = 3; string block_date = 4; - uint64 ordinal = 5; + + // -- storage change -- + uint64 ordinal = 5; // block global ordinal string address = 6; string key = 7; string new_value = 8; @@ -173,11 +204,14 @@ message StorageChange { } message CodeChange { + // -- block -- google.protobuf.Timestamp block_time = 1; uint64 block_number = 2; string block_hash = 3; string block_date = 4; - uint64 ordinal = 5; + + // -- code change -- + uint64 ordinal = 5; // block global ordinal string address = 6; string old_hash = 7; string old_code = 8; @@ -186,31 +220,40 @@ message CodeChange { } message AccountCreation { + // -- block -- google.protobuf.Timestamp block_time = 1; uint64 block_number = 2; string block_hash = 3; string block_date = 4; - uint64 ordinal = 5; + + // -- account creation -- + uint64 ordinal = 5; // block global ordinal string account = 6; } message NonceChange { + // -- block -- google.protobuf.Timestamp block_time = 1; uint64 block_number = 2; string block_hash = 3; string block_date = 4; - uint64 ordinal = 5; + + // -- nonce change -- + uint64 ordinal = 5; // block global ordinal string address = 6; uint64 old_value = 7; uint64 new_value = 8; } message GasChange { + // -- block -- google.protobuf.Timestamp block_time = 1; uint64 block_number = 2; string block_hash = 3; string block_date = 4; - uint64 ordinal = 5; + + // -- gas change -- + uint64 ordinal = 5; // block global ordinal uint64 old_value = 6; uint64 new_value = 7; string reason = 8; diff --git a/proto/parquet/options.proto b/proto/parquet/options.proto new file mode 100644 index 0000000..fe13490 --- /dev/null +++ b/proto/parquet/options.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package parquet; + +option go_package = "github.com/streamingfast/substreams-sink-files/pb/parquet;pbparquet"; + +import "google/protobuf/descriptor.proto"; + +extend google.protobuf.MessageOptions { + // As long as table_name is not blank, + // a schema is generated for top-level messages in each file. + string table_name = 14756842; +} + +extend google.protobuf.FieldOptions { + bool ignored = 548935; + optional Column column = 548936; +} + +message Column { + // Not implemented yet but planned so we reserved the field id now + // optional string name = 1; + optional ColumnType type = 2; + optional Compression compression = 3; +} + +enum ColumnType { + UNSPECIFIED_COLUMN_TYPE = 0; + UINT256 = 1; + INT256 = 2; +} + +enum Compression { + UNCOMPRESSED = 0; + SNAPPY = 1; + GZIP = 2; + LZ4_RAW = 3; + BROTLI = 4; + ZSTD = 5; +} \ No newline at end of file From c25ae1ec07c3feaaebc1e032a726cba2b293a63f Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Tue, 19 Nov 2024 14:00:45 +0100 Subject: [PATCH 13/20] add creation traces --- blocks/evm/README.md | 2 ++ blocks/evm/src/account_creations.rs | 12 +++++++++ blocks/evm/src/balance_changes.rs | 6 +++++ blocks/evm/src/blocks.rs | 5 ++++ blocks/evm/src/code_changes.rs | 6 +++++ blocks/evm/src/creation_traces.rs | 39 +++++++++++++++++++++++++++++ blocks/evm/src/events.rs | 2 ++ blocks/evm/src/lib.rs | 1 + blocks/evm/src/logs.rs | 6 ++--- blocks/evm/src/pb/evm.rs | 33 +++++++++++++++++++++--- docs/google_big_query.md | 5 ++++ proto/evm.proto | 30 ++++++++++++++++++---- snowflake/ethereum/README.md | 1 + 13 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 blocks/evm/src/creation_traces.rs create mode 100644 docs/google_big_query.md diff --git a/blocks/evm/README.md b/blocks/evm/README.md index 2861e57..2a28c16 100644 --- a/blocks/evm/README.md +++ b/blocks/evm/README.md @@ -16,6 +16,7 @@ - [x] **Account Creation** - [x] **Gas Changes** - [x] **Nonce Changes** + - [x] **Creation Traces** ## Graph @@ -34,6 +35,7 @@ graph TD; extended --> account_creations; extended --> gas_changes; extended --> nonce_changes; + extended --> creation_traces; ``` ## Modules diff --git a/blocks/evm/src/account_creations.rs b/blocks/evm/src/account_creations.rs index 9268ef7..bacbd04 100644 --- a/blocks/evm/src/account_creations.rs +++ b/blocks/evm/src/account_creations.rs @@ -14,10 +14,16 @@ pub fn collect_account_creations(block: &Block, timestamp: &BlockTimestamp) -> V for call in &block.system_calls { for account_creation in &call.account_creations { account_creations.push(AccountCreation { + // block block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + + // transaction + tx_hash: Some("".to_string()), + + // account creation account: bytes_to_hex(&account_creation.account), ordinal: account_creation.ordinal, }); @@ -29,10 +35,16 @@ pub fn collect_account_creations(block: &Block, timestamp: &BlockTimestamp) -> V for call in &transaction.calls { for account_creation in &call.account_creations { account_creations.push(AccountCreation { + // block block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + + // transaction + tx_hash: Some(bytes_to_hex(&transaction.hash)), + + // account creation account: bytes_to_hex(&account_creation.account), ordinal: account_creation.ordinal, }); diff --git a/blocks/evm/src/balance_changes.rs b/blocks/evm/src/balance_changes.rs index 62b3ee8..ab45be4 100644 --- a/blocks/evm/src/balance_changes.rs +++ b/blocks/evm/src/balance_changes.rs @@ -43,10 +43,13 @@ pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec for balance_change in &call.balance_changes { let amount = optional_bigint_to_decimal(balance_change.new_value.clone()) - optional_bigint_to_decimal(balance_change.old_value.clone()); balance_changes.push(BalanceChange { + // block block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + + // balance changes address: bytes_to_hex(&balance_change.address), new_balance: optional_bigint_to_string(&balance_change.new_value, "0"), old_balance: optional_bigint_to_string(&balance_change.old_value, "0"), @@ -64,10 +67,13 @@ pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec for balance_change in &call.balance_changes { let amount = optional_bigint_to_decimal(balance_change.new_value.clone()) - optional_bigint_to_decimal(balance_change.old_value.clone()); balance_changes.push(BalanceChange { + // block block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + + // balance changes address: bytes_to_hex(&balance_change.address), new_balance: optional_bigint_to_string(&balance_change.new_value, "0"), old_balance: optional_bigint_to_string(&balance_change.old_value, "0"), diff --git a/blocks/evm/src/blocks.rs b/blocks/evm/src/blocks.rs index 0775cec..419534c 100644 --- a/blocks/evm/src/blocks.rs +++ b/blocks/evm/src/blocks.rs @@ -26,10 +26,13 @@ pub fn collect_block(block: &Block, timestamp: &BlockTimestamp) -> BlockHeader { let total_withdrawals = block.balance_changes.iter().filter(|t| t.reason == 16).count() as u64; BlockHeader { + // clock time: Some(timestamp.time), number: header.number, date: timestamp.date.clone(), hash: bytes_to_hex(&block.hash), + + // header parent_hash: bytes_to_hex(&header.parent_hash), nonce: header.nonce, ommers_hash: bytes_to_hex(&header.uncle_hash), @@ -50,6 +53,8 @@ pub fn collect_block(block: &Block, timestamp: &BlockTimestamp) -> BlockHeader { base_fee_per_gas: optional_bigint_to_string(&header.base_fee_per_gas, ""), blob_gas_used: optional_u64_to_string(&header.blob_gas_used, ""), excess_blob_gas: optional_u64_to_string(&header.excess_blob_gas, ""), + + // counters size: block.size, total_transactions: block.transaction_traces.len() as u64, successful_transactions, diff --git a/blocks/evm/src/code_changes.rs b/blocks/evm/src/code_changes.rs index 733898f..f8ff278 100644 --- a/blocks/evm/src/code_changes.rs +++ b/blocks/evm/src/code_changes.rs @@ -13,10 +13,13 @@ pub fn collect_code_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec Vec Vec { + let mut creation_traces: Vec = vec![]; + + // Collect code changes from system calls + for trace in block.transaction_traces.iter() { + for call in trace.calls.iter() { + if call.call_type() == CallType::Create { + for code in call.code_changes.iter() { + let factory = if trace.to == code.address { "".to_string() } else { bytes_to_hex(&trace.to) }; + + creation_traces.push(CreationTrace { + // block + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + + // creation trace + from: bytes_to_hex(&trace.from), + tx_hash: bytes_to_hex(&trace.hash), + address: bytes_to_hex(&code.address), + factory: factory.to_string(), + code: bytes_to_hex(&code.new_code), + }); + } + } + } + } + + creation_traces +} diff --git a/blocks/evm/src/events.rs b/blocks/evm/src/events.rs index c584c9f..80580c2 100644 --- a/blocks/evm/src/events.rs +++ b/blocks/evm/src/events.rs @@ -8,6 +8,7 @@ use crate::account_creations::collect_account_creations; use crate::balance_changes::collect_balance_changes; use crate::blocks::{block_detail_to_string, collect_block}; use crate::code_changes::collect_code_changes; +use crate::creation_traces::collect_creation_traces; use crate::gas_changes::collect_gas_changes; use crate::logs::collect_logs; use crate::nonce_changes::collect_nonce_changes; @@ -32,5 +33,6 @@ pub fn map_events(clock: Clock, block: Block) -> Result { account_creations: collect_account_creations(&block, ×tamp), nonce_changes: collect_nonce_changes(&block, ×tamp), gas_changes: collect_gas_changes(&block, ×tamp), + creation_traces: collect_creation_traces(&block, ×tamp), }) } diff --git a/blocks/evm/src/lib.rs b/blocks/evm/src/lib.rs index a35207e..99967b6 100644 --- a/blocks/evm/src/lib.rs +++ b/blocks/evm/src/lib.rs @@ -2,6 +2,7 @@ mod account_creations; mod balance_changes; mod blocks; mod code_changes; +mod creation_traces; mod events; mod gas_changes; mod logs; diff --git a/blocks/evm/src/logs.rs b/blocks/evm/src/logs.rs index 2f42414..c475109 100644 --- a/blocks/evm/src/logs.rs +++ b/blocks/evm/src/logs.rs @@ -34,9 +34,9 @@ pub fn collect_logs(block: &Block, timestamp: &BlockTimestamp, detail_level: &st block_index: log.block_index, contract_address: bytes_to_hex(&log.address), topic0: extract_topic(&log.topics, 0), - topic1: extract_topic(&log.topics, 1), - topic2: extract_topic(&log.topics, 2), - topic3: extract_topic(&log.topics, 3), + topic1: Some(extract_topic(&log.topics, 1)), + topic2: Some(extract_topic(&log.topics, 2)), + topic3: Some(extract_topic(&log.topics, 3)), data: bytes_to_hex(&log.data), }); } diff --git a/blocks/evm/src/pb/evm.rs b/blocks/evm/src/pb/evm.rs index b337562..cca45a3 100644 --- a/blocks/evm/src/pb/evm.rs +++ b/blocks/evm/src/pb/evm.rs @@ -23,6 +23,8 @@ pub struct Events { pub nonce_changes: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="10")] pub gas_changes: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="11")] + pub creation_traces: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -389,11 +391,12 @@ pub struct AccountCreation { #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, /// -- account creation -- - /// + #[prost(string, optional, tag="5")] + pub tx_hash: ::core::option::Option<::prost::alloc::string::String>, /// block global ordinal - #[prost(uint64, tag="5")] + #[prost(uint64, tag="6")] pub ordinal: u64, - #[prost(string, tag="6")] + #[prost(string, tag="7")] pub account: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] @@ -446,4 +449,28 @@ pub struct GasChange { #[prost(uint32, tag="9")] pub reason_code: u32, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CreationTrace { + /// -- block -- + #[prost(message, optional, tag="1")] + pub block_time: ::core::option::Option<::prost_types::Timestamp>, + #[prost(uint64, tag="2")] + pub block_number: u64, + #[prost(string, tag="3")] + pub block_hash: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub block_date: ::prost::alloc::string::String, + /// -- creation trace -- + #[prost(string, tag="5")] + pub tx_hash: ::prost::alloc::string::String, + #[prost(string, tag="6")] + pub address: ::prost::alloc::string::String, + #[prost(string, tag="7")] + pub from: ::prost::alloc::string::String, + #[prost(string, tag="8")] + pub factory: ::prost::alloc::string::String, + #[prost(string, tag="9")] + pub code: ::prost::alloc::string::String, +} // @@protoc_insertion_point(module) diff --git a/docs/google_big_query.md b/docs/google_big_query.md new file mode 100644 index 0000000..b4428e7 --- /dev/null +++ b/docs/google_big_query.md @@ -0,0 +1,5 @@ +## Google Big Query + + + + diff --git a/proto/evm.proto b/proto/evm.proto index 5a592a2..274988b 100644 --- a/proto/evm.proto +++ b/proto/evm.proto @@ -16,9 +16,9 @@ message Events { repeated AccountCreation account_creations = 8; repeated NonceChange nonce_changes = 9; repeated GasChange gas_changes = 10; + repeated CreationTrace creation_traces = 11; } - message Block { // -- clock -- google.protobuf.Timestamp time = 1; @@ -37,8 +37,8 @@ message Block { string withdrawals_root = 12; // EVM Root EIP-4895 (Shangai Fork) string parent_beacon_root = 13; // EVM Root EIP-4788 (Dencun Fork) string miner = 14; - uint64 difficulty = 15 [(parquet.column) = {type: UINT256}]; - string total_difficulty = 16; + uint64 difficulty = 15; + string total_difficulty = 16 [(parquet.column) = {type: UINT256}]; string mix_hash = 17; string extra_data = 18; string extra_data_utf8 = 19; @@ -226,9 +226,12 @@ message AccountCreation { string block_hash = 3; string block_date = 4; + // -- transaction -- + optional string tx_hash = 5; + // -- account creation -- - uint64 ordinal = 5; // block global ordinal - string account = 6; + uint64 ordinal = 6; // block global ordinal + string account = 7; } message NonceChange { @@ -259,3 +262,20 @@ message GasChange { string reason = 8; uint32 reason_code = 9; } + +message CreationTrace { + // -- block -- + google.protobuf.Timestamp block_time = 1; + uint64 block_number = 2; + string block_hash = 3; + string block_date = 4; + + // -- transaction -- + string tx_hash = 5; + + // -- creation trace -- + string address = 6; + string from = 7; + string factory = 8; + string code = 9; +} \ No newline at end of file diff --git a/snowflake/ethereum/README.md b/snowflake/ethereum/README.md index 1911a67..8780510 100644 --- a/snowflake/ethereum/README.md +++ b/snowflake/ethereum/README.md @@ -14,6 +14,7 @@ Try 30 day limited trial access to the dataset on the [Snowflake Data Marketplac | `logs` | Event logs from smart contracts. | | `storage_changes` | Changes in smart contract storage. | | `code_changes` | Smart contract code updates. | +| `creation_traces` | Contract creation transactions. | ## Sample SQL Queries From 70fcc0a9e890aead3c2c70b0e93aa4dca09c2142 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Tue, 19 Nov 2024 14:24:28 +0100 Subject: [PATCH 14/20] add tx_hash --- blocks/evm/src/balance_changes.rs | 6 ++++++ blocks/evm/src/events.rs | 11 +++++------ blocks/evm/src/gas_changes.rs | 6 ++++++ blocks/evm/src/logs.rs | 10 +++++++++- blocks/evm/src/nonce_changes.rs | 6 ++++++ blocks/evm/src/pb/evm.rs | 24 +++++++++++++++--------- blocks/evm/src/storage_changes.rs | 6 ++++++ blocks/evm/src/traces.rs | 10 +++++++++- blocks/evm/src/transactions.rs | 5 +++++ common/src/utils.rs | 6 ++++++ proto/evm.proto | 17 ++++++++++------- 11 files changed, 83 insertions(+), 24 deletions(-) diff --git a/blocks/evm/src/balance_changes.rs b/blocks/evm/src/balance_changes.rs index ab45be4..1e482ea 100644 --- a/blocks/evm/src/balance_changes.rs +++ b/blocks/evm/src/balance_changes.rs @@ -49,6 +49,9 @@ pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + // transaction + tx_hash: Some("".to_string()), + // balance changes address: bytes_to_hex(&balance_change.address), new_balance: optional_bigint_to_string(&balance_change.new_value, "0"), @@ -73,6 +76,9 @@ pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + // transaction + tx_hash: Some(bytes_to_hex(&transaction.hash)), + // balance changes address: bytes_to_hex(&balance_change.address), new_balance: optional_bigint_to_string(&balance_change.new_value, "0"), diff --git a/blocks/evm/src/events.rs b/blocks/evm/src/events.rs index 80580c2..89976a5 100644 --- a/blocks/evm/src/events.rs +++ b/blocks/evm/src/events.rs @@ -1,4 +1,4 @@ -use common::utils::build_timestamp; +use common::utils::build_timestamp_with_prefix; use substreams::errors::Error; use substreams::pb::substreams::Clock; @@ -6,7 +6,7 @@ use substreams_ethereum::pb::eth::v2::Block; use crate::account_creations::collect_account_creations; use crate::balance_changes::collect_balance_changes; -use crate::blocks::{block_detail_to_string, collect_block}; +use crate::blocks::collect_block; use crate::code_changes::collect_code_changes; use crate::creation_traces::collect_creation_traces; use crate::gas_changes::collect_gas_changes; @@ -19,14 +19,13 @@ use crate::transactions::collect_transactions; #[substreams::handlers::map] pub fn map_events(clock: Clock, block: Block) -> Result { - let timestamp = build_timestamp(&clock); - let detail_level = block_detail_to_string(block.detail_level); + let timestamp = build_timestamp_with_prefix(&clock); Ok(Events { blocks: vec![collect_block(&block, ×tamp)], transactions: collect_transactions(&block, ×tamp), - logs: collect_logs(&block, ×tamp, &detail_level), - traces: collect_traces(&block, ×tamp, &detail_level), + logs: collect_logs(&block, ×tamp), + traces: collect_traces(&block, ×tamp), balance_changes: collect_balance_changes(&block, ×tamp), storage_changes: collect_storage_changes(&block, ×tamp), code_changes: collect_code_changes(&block, ×tamp), diff --git a/blocks/evm/src/gas_changes.rs b/blocks/evm/src/gas_changes.rs index ea9a4f1..9a172b1 100644 --- a/blocks/evm/src/gas_changes.rs +++ b/blocks/evm/src/gas_changes.rs @@ -44,10 +44,13 @@ pub fn collect_gas_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec Vec Vec { +pub fn collect_logs(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let detail_level = block_detail_to_string(block.detail_level); + // Only required DetailLevel=BASE since traces are not available in BASE if detail_level == "Base" { return vec![]; @@ -19,10 +22,13 @@ pub fn collect_logs(block: &Block, timestamp: &BlockTimestamp, detail_level: &st let receipt = transaction.receipt.as_ref().unwrap(); for log in &receipt.logs { logs.push(Log { + // block block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + + // transaction tx_hash: bytes_to_hex(&transaction.hash), tx_index: transaction.index, tx_status: transaction_status_to_string(transaction.status), @@ -30,6 +36,8 @@ pub fn collect_logs(block: &Block, timestamp: &BlockTimestamp, detail_level: &st tx_success: is_transaction_success(transaction.status), tx_from: bytes_to_hex(&transaction.from), tx_to: bytes_to_hex(&transaction.to), + + // log index: log.index, block_index: log.block_index, contract_address: bytes_to_hex(&log.address), diff --git a/blocks/evm/src/nonce_changes.rs b/blocks/evm/src/nonce_changes.rs index 93296c1..2e6a337 100644 --- a/blocks/evm/src/nonce_changes.rs +++ b/blocks/evm/src/nonce_changes.rs @@ -13,10 +13,13 @@ pub fn collect_nonce_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec Vec, /// -- balance change -- - #[prost(string, tag="5")] - pub address: ::prost::alloc::string::String, #[prost(string, tag="6")] - pub new_balance: ::prost::alloc::string::String, + pub address: ::prost::alloc::string::String, #[prost(string, tag="7")] - pub old_balance: ::prost::alloc::string::String, + pub new_balance: ::prost::alloc::string::String, #[prost(string, tag="8")] + pub old_balance: ::prost::alloc::string::String, + #[prost(string, tag="9")] pub amount: ::prost::alloc::string::String, - #[prost(uint64, tag="9")] + #[prost(uint64, tag="10")] pub ordinal: u64, - #[prost(string, tag="10")] + #[prost(string, tag="11")] pub reason: ::prost::alloc::string::String, - #[prost(uint32, tag="11")] + #[prost(uint32, tag="12")] pub reason_code: u32, } #[allow(clippy::derive_partial_eq_without_eq)] @@ -390,9 +393,11 @@ pub struct AccountCreation { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, - /// -- account creation -- + /// -- transaction -- #[prost(string, optional, tag="5")] pub tx_hash: ::core::option::Option<::prost::alloc::string::String>, + /// -- account creation -- + /// /// block global ordinal #[prost(uint64, tag="6")] pub ordinal: u64, @@ -461,9 +466,10 @@ pub struct CreationTrace { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, - /// -- creation trace -- + /// -- transaction -- #[prost(string, tag="5")] pub tx_hash: ::prost::alloc::string::String, + /// -- creation trace -- #[prost(string, tag="6")] pub address: ::prost::alloc::string::String, #[prost(string, tag="7")] diff --git a/blocks/evm/src/storage_changes.rs b/blocks/evm/src/storage_changes.rs index 50e598f..710b93b 100644 --- a/blocks/evm/src/storage_changes.rs +++ b/blocks/evm/src/storage_changes.rs @@ -14,10 +14,13 @@ pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec for call in &block.system_calls { for storage_change in &call.storage_changes { storage_changes.push(StorageChange { + // block block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + + // storage changes address: bytes_to_hex(&storage_change.address), key: bytes_to_hex(&storage_change.key), new_value: bytes_to_hex(&storage_change.new_value), @@ -32,10 +35,13 @@ pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec for call in &transaction.calls { for storage_change in &call.storage_changes { storage_changes.push(StorageChange { + // block block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + + // storage changes address: bytes_to_hex(&storage_change.address), key: bytes_to_hex(&storage_change.key), new_value: bytes_to_hex(&storage_change.new_value), diff --git a/blocks/evm/src/traces.rs b/blocks/evm/src/traces.rs index bd8de4d..7fa8ef1 100644 --- a/blocks/evm/src/traces.rs +++ b/blocks/evm/src/traces.rs @@ -5,6 +5,7 @@ use common::{ use substreams_ethereum::pb::eth::v2::Block; use crate::{ + blocks::block_detail_to_string, pb::evm::Trace, transactions::{is_transaction_success, transaction_status_to_string}, }; @@ -21,7 +22,9 @@ pub fn call_types_to_string(call_type: i32) -> String { } } -pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp, detail_level: &str) -> Vec { +pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let detail_level = block_detail_to_string(block.detail_level); + // Only required DetailLevel=EXTENDED if detail_level != "Extended" { return vec![]; @@ -34,10 +37,13 @@ pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp, detail_level: & // System calls are introduced in Cancun, along with blobs. They are executed outside of transactions but affect the state. for call in &block.system_calls { traces.push(Trace { + // block block_time: Some(timestamp.time), block_number: timestamp.number, block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + + // transaction // As this is a system call, tx_hash is empty // tx_index, tx_status, tx_status_code, tx_success are irrelevant as well tx_hash: String::new(), @@ -45,6 +51,8 @@ pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp, detail_level: & tx_status: transaction_status_to_string(1), tx_status_code: 1, tx_success: true, + + // trace from: bytes_to_hex(&call.caller), to: bytes_to_hex(&call.address), index: call.index, diff --git a/blocks/evm/src/transactions.rs b/blocks/evm/src/transactions.rs index f733689..c584db7 100644 --- a/blocks/evm/src/transactions.rs +++ b/blocks/evm/src/transactions.rs @@ -46,12 +46,17 @@ pub fn collect_transactions(block: &Block, timestamp: &BlockTimestamp) -> Vec BlockTimestamp { } } +pub fn build_timestamp_with_prefix(clock: &Clock) -> BlockTimestamp { + let mut data = build_timestamp(clock); + data.hash = add_prefix_to_hex(&data.hash); + data +} + #[cfg(test)] mod tests { use super::*; diff --git a/proto/evm.proto b/proto/evm.proto index 274988b..46251bb 100644 --- a/proto/evm.proto +++ b/proto/evm.proto @@ -178,14 +178,17 @@ message BalanceChange { string block_hash = 3; string block_date = 4; + // -- transaction -- + optional string tx_hash = 5; + // -- balance change -- - string address = 5; - string new_balance = 6 [(parquet.column) = {type: UINT256}]; - string old_balance = 7 [(parquet.column) = {type: UINT256}]; - string amount = 8 [(parquet.column) = {type: INT256}]; - uint64 ordinal = 9; - string reason = 10; - uint32 reason_code = 11; + string address = 6; + string new_balance = 7 [(parquet.column) = {type: UINT256}]; + string old_balance = 8 [(parquet.column) = {type: UINT256}]; + string amount = 9 [(parquet.column) = {type: INT256}]; + uint64 ordinal = 10; + string reason = 11; + uint32 reason_code = 12; } message StorageChange { From 25e69be2f9aef958fac6cabbac3aa4d789932661 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Tue, 19 Nov 2024 14:41:55 +0100 Subject: [PATCH 15/20] add tx_hash to all tables --- blocks/evm/src/code_changes.rs | 6 ++++ blocks/evm/src/creation_traces.rs | 4 ++- blocks/evm/src/gas_changes.rs | 8 ++++- blocks/evm/src/nonce_changes.rs | 6 ++++ blocks/evm/src/pb/evm.rs | 52 +++++++++++++++++++------------ blocks/evm/src/storage_changes.rs | 6 ++++ blocks/evm/src/traces.rs | 4 +-- proto/evm.proto | 52 +++++++++++++++++++------------ 8 files changed, 93 insertions(+), 45 deletions(-) diff --git a/blocks/evm/src/code_changes.rs b/blocks/evm/src/code_changes.rs index f8ff278..9f142cc 100644 --- a/blocks/evm/src/code_changes.rs +++ b/blocks/evm/src/code_changes.rs @@ -19,6 +19,9 @@ pub fn collect_code_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec Vec Vec block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + // transaction + tx_hash: bytes_to_hex(&trace.hash), + // creation trace from: bytes_to_hex(&trace.from), - tx_hash: bytes_to_hex(&trace.hash), address: bytes_to_hex(&code.address), factory: factory.to_string(), code: bytes_to_hex(&code.new_code), diff --git a/blocks/evm/src/gas_changes.rs b/blocks/evm/src/gas_changes.rs index 9a172b1..be1577d 100644 --- a/blocks/evm/src/gas_changes.rs +++ b/blocks/evm/src/gas_changes.rs @@ -1,4 +1,4 @@ -use common::structs::BlockTimestamp; +use common::{structs::BlockTimestamp, utils::bytes_to_hex}; use substreams_ethereum::pb::eth::v2::Block; use crate::pb::evm::GasChange; @@ -50,6 +50,9 @@ pub fn collect_gas_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec Vec Vec Vec, /// -- storage change -- /// /// block global ordinal - #[prost(uint64, tag="5")] + #[prost(uint64, tag="6")] pub ordinal: u64, - #[prost(string, tag="6")] - pub address: ::prost::alloc::string::String, #[prost(string, tag="7")] - pub key: ::prost::alloc::string::String, + pub address: ::prost::alloc::string::String, #[prost(string, tag="8")] - pub new_value: ::prost::alloc::string::String, + pub key: ::prost::alloc::string::String, #[prost(string, tag="9")] + pub new_value: ::prost::alloc::string::String, + #[prost(string, tag="10")] pub old_value: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] @@ -365,20 +368,23 @@ pub struct CodeChange { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- transaction -- + #[prost(string, optional, tag="5")] + pub tx_hash: ::core::option::Option<::prost::alloc::string::String>, /// -- code change -- /// /// block global ordinal - #[prost(uint64, tag="5")] + #[prost(uint64, tag="6")] pub ordinal: u64, - #[prost(string, tag="6")] - pub address: ::prost::alloc::string::String, #[prost(string, tag="7")] - pub old_hash: ::prost::alloc::string::String, + pub address: ::prost::alloc::string::String, #[prost(string, tag="8")] - pub old_code: ::prost::alloc::string::String, + pub old_hash: ::prost::alloc::string::String, #[prost(string, tag="9")] - pub new_hash: ::prost::alloc::string::String, + pub old_code: ::prost::alloc::string::String, #[prost(string, tag="10")] + pub new_hash: ::prost::alloc::string::String, + #[prost(string, tag="11")] pub new_code: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] @@ -416,16 +422,19 @@ pub struct NonceChange { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- transaction -- + #[prost(string, optional, tag="5")] + pub tx_hash: ::core::option::Option<::prost::alloc::string::String>, /// -- nonce change -- /// /// block global ordinal - #[prost(uint64, tag="5")] + #[prost(uint64, tag="6")] pub ordinal: u64, - #[prost(string, tag="6")] + #[prost(string, tag="7")] pub address: ::prost::alloc::string::String, - #[prost(uint64, tag="7")] - pub old_value: u64, #[prost(uint64, tag="8")] + pub old_value: u64, + #[prost(uint64, tag="9")] pub new_value: u64, } #[allow(clippy::derive_partial_eq_without_eq)] @@ -440,18 +449,21 @@ pub struct GasChange { pub block_hash: ::prost::alloc::string::String, #[prost(string, tag="4")] pub block_date: ::prost::alloc::string::String, + /// -- transaction -- + #[prost(string, optional, tag="5")] + pub tx_hash: ::core::option::Option<::prost::alloc::string::String>, /// -- gas change -- /// /// block global ordinal - #[prost(uint64, tag="5")] - pub ordinal: u64, #[prost(uint64, tag="6")] - pub old_value: u64, + pub ordinal: u64, #[prost(uint64, tag="7")] + pub old_value: u64, + #[prost(uint64, tag="8")] pub new_value: u64, - #[prost(string, tag="8")] + #[prost(string, tag="9")] pub reason: ::prost::alloc::string::String, - #[prost(uint32, tag="9")] + #[prost(uint32, tag="10")] pub reason_code: u32, } #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/blocks/evm/src/storage_changes.rs b/blocks/evm/src/storage_changes.rs index 710b93b..9117e7d 100644 --- a/blocks/evm/src/storage_changes.rs +++ b/blocks/evm/src/storage_changes.rs @@ -20,6 +20,9 @@ pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + // transaction + tx_hash: Some(String::new()), + // storage changes address: bytes_to_hex(&storage_change.address), key: bytes_to_hex(&storage_change.key), @@ -41,6 +44,9 @@ pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec block_hash: timestamp.hash.clone(), block_date: timestamp.date.clone(), + // transaction + tx_hash: Some(bytes_to_hex(&transaction.hash)), + // storage changes address: bytes_to_hex(&storage_change.address), key: bytes_to_hex(&storage_change.key), diff --git a/blocks/evm/src/traces.rs b/blocks/evm/src/traces.rs index 7fa8ef1..e53e859 100644 --- a/blocks/evm/src/traces.rs +++ b/blocks/evm/src/traces.rs @@ -44,9 +44,7 @@ pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp) -> Vec { block_date: timestamp.date.clone(), // transaction - // As this is a system call, tx_hash is empty - // tx_index, tx_status, tx_status_code, tx_success are irrelevant as well - tx_hash: String::new(), + tx_hash: String::new(), // As this is a system call, tx_hash is empty tx_index: 0, tx_status: transaction_status_to_string(1), tx_status_code: 1, diff --git a/proto/evm.proto b/proto/evm.proto index 46251bb..5bb1322 100644 --- a/proto/evm.proto +++ b/proto/evm.proto @@ -198,12 +198,15 @@ message StorageChange { string block_hash = 3; string block_date = 4; + // -- transaction -- + optional string tx_hash = 5; + // -- storage change -- - uint64 ordinal = 5; // block global ordinal - string address = 6; - string key = 7; - string new_value = 8; - string old_value = 9; + uint64 ordinal = 6; // block global ordinal + string address = 7; + string key = 8; + string new_value = 9; + string old_value = 10; } message CodeChange { @@ -213,13 +216,16 @@ message CodeChange { string block_hash = 3; string block_date = 4; + // -- transaction -- + optional string tx_hash = 5; + // -- code change -- - uint64 ordinal = 5; // block global ordinal - string address = 6; - string old_hash = 7; - string old_code = 8; - string new_hash = 9; - string new_code = 10; + uint64 ordinal = 6; // block global ordinal + string address = 7; + string old_hash = 8; + string old_code = 9; + string new_hash = 10; + string new_code = 11; } message AccountCreation { @@ -244,11 +250,14 @@ message NonceChange { string block_hash = 3; string block_date = 4; + // -- transaction -- + optional string tx_hash = 5; + // -- nonce change -- - uint64 ordinal = 5; // block global ordinal - string address = 6; - uint64 old_value = 7; - uint64 new_value = 8; + uint64 ordinal = 6; // block global ordinal + string address = 7; + uint64 old_value = 8; + uint64 new_value = 9; } message GasChange { @@ -258,12 +267,15 @@ message GasChange { string block_hash = 3; string block_date = 4; + // -- transaction -- + optional string tx_hash = 5; + // -- gas change -- - uint64 ordinal = 5; // block global ordinal - uint64 old_value = 6; - uint64 new_value = 7; - string reason = 8; - uint32 reason_code = 9; + uint64 ordinal = 6; // block global ordinal + uint64 old_value = 7; + uint64 new_value = 8; + string reason = 9; + uint32 reason_code = 10; } message CreationTrace { From 5a57ef1db7e12658ccdb60c517556323f73ffe36 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Tue, 19 Nov 2024 15:31:06 +0100 Subject: [PATCH 16/20] Refactored parse event --- blocks/evm/src/account_creations.rs | 55 +++++------- blocks/evm/src/balance_changes.rs | 73 ++++++--------- blocks/evm/src/code_changes.rs | 67 ++++++-------- blocks/evm/src/events.rs | 1 + blocks/evm/src/gas_changes.rs | 64 ++++++------- blocks/evm/src/logs.rs | 84 ++++++++++-------- blocks/evm/src/nonce_changes.rs | 61 ++++++------- blocks/evm/src/traces.rs | 133 ++++++++++------------------ 8 files changed, 226 insertions(+), 312 deletions(-) diff --git a/blocks/evm/src/account_creations.rs b/blocks/evm/src/account_creations.rs index bacbd04..22107e8 100644 --- a/blocks/evm/src/account_creations.rs +++ b/blocks/evm/src/account_creations.rs @@ -1,32 +1,19 @@ use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; -use substreams_ethereum::pb::eth::v2::Block; +use substreams_ethereum::pb::eth::v2::{AccountCreation, Block, TransactionTrace}; -use crate::pb::evm::AccountCreation; +use crate::pb::evm::AccountCreation as AccountCreationEvent; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L736 // DetailLevel: EXTENDED -pub fn collect_account_creations(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut account_creations: Vec = vec![]; +pub fn collect_account_creations(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut account_creations: Vec = vec![]; // Collect account creations from system calls for call in &block.system_calls { for account_creation in &call.account_creations { - account_creations.push(AccountCreation { - // block - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - - // transaction - tx_hash: Some("".to_string()), - - // account creation - account: bytes_to_hex(&account_creation.account), - ordinal: account_creation.ordinal, - }); + account_creations.push(parse_account_creation(account_creation, &TransactionTrace::default(), timestamp)); } } @@ -34,23 +21,27 @@ pub fn collect_account_creations(block: &Block, timestamp: &BlockTimestamp) -> V for transaction in &block.transaction_traces { for call in &transaction.calls { for account_creation in &call.account_creations { - account_creations.push(AccountCreation { - // block - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - - // transaction - tx_hash: Some(bytes_to_hex(&transaction.hash)), - - // account creation - account: bytes_to_hex(&account_creation.account), - ordinal: account_creation.ordinal, - }); + account_creations.push(parse_account_creation(account_creation, transaction, timestamp)); } } } account_creations } + +pub fn parse_account_creation(account_creation: &AccountCreation, transaction: &TransactionTrace, timestamp: &BlockTimestamp) -> AccountCreationEvent { + AccountCreationEvent { + // block + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + + // transaction + tx_hash: Some(bytes_to_hex(&transaction.hash)), + + // account creation + account: bytes_to_hex(&account_creation.account), + ordinal: account_creation.ordinal, + } +} diff --git a/blocks/evm/src/balance_changes.rs b/blocks/evm/src/balance_changes.rs index 1e482ea..106eed8 100644 --- a/blocks/evm/src/balance_changes.rs +++ b/blocks/evm/src/balance_changes.rs @@ -1,9 +1,9 @@ use common::structs::BlockTimestamp; use common::utils::optional_bigint_to_string; use common::utils::{bytes_to_hex, optional_bigint_to_decimal}; -use substreams_ethereum::pb::eth::v2::Block; +use substreams_ethereum::pb::eth::v2::{BalanceChange, Block, TransactionTrace}; -use crate::pb::evm::BalanceChange; +use crate::pb::evm::BalanceChange as BalanceChangeEvent; pub fn balance_change_reason_to_string(reason: i32) -> String { match reason { @@ -35,32 +35,13 @@ pub fn balance_change_reason_to_string(reason: i32) -> String { // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L658 // DetailLevel: EXTENDED -pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut balance_changes: Vec = vec![]; +pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut balance_changes: Vec = vec![]; // Collect balance changes from system calls for call in &block.system_calls { for balance_change in &call.balance_changes { - let amount = optional_bigint_to_decimal(balance_change.new_value.clone()) - optional_bigint_to_decimal(balance_change.old_value.clone()); - balance_changes.push(BalanceChange { - // block - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - - // transaction - tx_hash: Some("".to_string()), - - // balance changes - address: bytes_to_hex(&balance_change.address), - new_balance: optional_bigint_to_string(&balance_change.new_value, "0"), - old_balance: optional_bigint_to_string(&balance_change.old_value, "0"), - amount: amount.to_string(), - ordinal: balance_change.ordinal, - reason: balance_change_reason_to_string(balance_change.reason), - reason_code: balance_change.reason as u32, - }); + balance_changes.push(parse_balance_change(balance_change, &TransactionTrace::default(), timestamp)); } } @@ -68,29 +49,33 @@ pub fn collect_balance_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec for transaction in &block.transaction_traces { for call in &transaction.calls { for balance_change in &call.balance_changes { - let amount = optional_bigint_to_decimal(balance_change.new_value.clone()) - optional_bigint_to_decimal(balance_change.old_value.clone()); - balance_changes.push(BalanceChange { - // block - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - - // transaction - tx_hash: Some(bytes_to_hex(&transaction.hash)), - - // balance changes - address: bytes_to_hex(&balance_change.address), - new_balance: optional_bigint_to_string(&balance_change.new_value, "0"), - old_balance: optional_bigint_to_string(&balance_change.old_value, "0"), - amount: amount.to_string(), - ordinal: balance_change.ordinal, - reason: balance_change_reason_to_string(balance_change.reason), - reason_code: balance_change.reason as u32, - }); + balance_changes.push(parse_balance_change(balance_change, transaction, timestamp)); } } } balance_changes } + +pub fn parse_balance_change(balance_change: &BalanceChange, transaction: &TransactionTrace, timestamp: &BlockTimestamp) -> BalanceChangeEvent { + let amount = optional_bigint_to_decimal(balance_change.new_value.clone()) - optional_bigint_to_decimal(balance_change.old_value.clone()); + BalanceChangeEvent { + // block + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + + // transaction + tx_hash: Some(bytes_to_hex(&transaction.hash)), + + // balance changes + address: bytes_to_hex(&balance_change.address), + new_balance: optional_bigint_to_string(&balance_change.new_value, "0"), + old_balance: optional_bigint_to_string(&balance_change.old_value, "0"), + amount: amount.to_string(), + ordinal: balance_change.ordinal, + reason: balance_change_reason_to_string(balance_change.reason), + reason_code: balance_change.reason as u32, + } +} diff --git a/blocks/evm/src/code_changes.rs b/blocks/evm/src/code_changes.rs index 9f142cc..91cc54f 100644 --- a/blocks/evm/src/code_changes.rs +++ b/blocks/evm/src/code_changes.rs @@ -1,35 +1,18 @@ use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; -use substreams_ethereum::pb::eth::v2::Block; +use substreams_ethereum::pb::eth::v2::{Block, CodeChange, TransactionTrace}; -use crate::pb::evm::CodeChange; +use crate::pb::evm::CodeChange as CodeChangeEvent; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L744 // DetailLevel: EXTENDED -pub fn collect_code_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut code_changes: Vec = vec![]; +pub fn collect_code_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut code_changes: Vec = vec![]; // Collect code changes from system calls for call in &block.system_calls { for code_change in &call.code_changes { - code_changes.push(CodeChange { - // block - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - - // transaction - tx_hash: Some(String::new()), - - // code changes - address: bytes_to_hex(&code_change.address), - old_hash: bytes_to_hex(&code_change.old_hash), - old_code: bytes_to_hex(&code_change.old_code), - new_hash: bytes_to_hex(&code_change.new_hash), - new_code: bytes_to_hex(&code_change.new_code), - ordinal: code_change.ordinal, - }); + code_changes.push(parse_code_change(code_change, &TransactionTrace::default(), timestamp)); } } @@ -37,27 +20,31 @@ pub fn collect_code_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec CodeChangeEvent { + CodeChangeEvent { + // block + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + + // transaction + tx_hash: Some(bytes_to_hex(&transaction.hash)), + + // code changes + address: bytes_to_hex(&code_change.address), + old_hash: bytes_to_hex(&code_change.old_hash), + old_code: bytes_to_hex(&code_change.old_code), + new_hash: bytes_to_hex(&code_change.new_hash), + new_code: bytes_to_hex(&code_change.new_code), + ordinal: code_change.ordinal, + } +} diff --git a/blocks/evm/src/events.rs b/blocks/evm/src/events.rs index 89976a5..1aa1529 100644 --- a/blocks/evm/src/events.rs +++ b/blocks/evm/src/events.rs @@ -20,6 +20,7 @@ use crate::transactions::collect_transactions; #[substreams::handlers::map] pub fn map_events(clock: Clock, block: Block) -> Result { let timestamp = build_timestamp_with_prefix(&clock); + let mut events = Events::default(); Ok(Events { blocks: vec![collect_block(&block, ×tamp)], diff --git a/blocks/evm/src/gas_changes.rs b/blocks/evm/src/gas_changes.rs index be1577d..22787fb 100644 --- a/blocks/evm/src/gas_changes.rs +++ b/blocks/evm/src/gas_changes.rs @@ -1,7 +1,7 @@ use common::{structs::BlockTimestamp, utils::bytes_to_hex}; -use substreams_ethereum::pb::eth::v2::Block; +use substreams_ethereum::pb::eth::v2::{Block, GasChange, TransactionTrace}; -use crate::pb::evm::GasChange; +use crate::pb::evm::GasChange as GasChangeEvent; pub fn gas_change_reason_to_string(reason: i32) -> String { match reason { @@ -37,29 +37,13 @@ pub fn gas_change_reason_to_string(reason: i32) -> String { // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L726C9-L726C20 // DetailLevel: EXTENDED -pub fn collect_gas_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut gas_changes: Vec = vec![]; +pub fn collect_gas_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut gas_changes: Vec = vec![]; // Collect gas changes from system calls for call in &block.system_calls { for gas_change in &call.gas_changes { - gas_changes.push(GasChange { - // block - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - - // transaction - tx_hash: Some(String::new()), - - // gas changes - old_value: gas_change.old_value, - new_value: gas_change.new_value, - reason: gas_change_reason_to_string(gas_change.reason), - reason_code: gas_change.reason as u32, - ordinal: gas_change.ordinal, - }); + gas_changes.push(parse_gas_changes(gas_change, &TransactionTrace::default(), timestamp)); } } @@ -67,26 +51,30 @@ pub fn collect_gas_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec GasChangeEvent { + GasChangeEvent { + // block + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + + // transaction + tx_hash: Some(bytes_to_hex(&transaction.hash)), + + // gas changes + old_value: gas_change.old_value, + new_value: gas_change.new_value, + reason: gas_change_reason_to_string(gas_change.reason), + reason_code: gas_change.reason as u32, + ordinal: gas_change.ordinal, + } +} diff --git a/blocks/evm/src/logs.rs b/blocks/evm/src/logs.rs index 4ec5f62..286b176 100644 --- a/blocks/evm/src/logs.rs +++ b/blocks/evm/src/logs.rs @@ -1,54 +1,62 @@ use common::structs::BlockTimestamp; use common::utils::{bytes_to_hex, extract_topic}; -use substreams_ethereum::pb::eth::v2::Block; +use substreams_ethereum::pb::eth::v2::{Block, Log, TransactionTrace}; use crate::blocks::block_detail_to_string; -use crate::pb::evm::Log; +use crate::pb::evm::Log as LogEvent; use crate::transactions::{is_transaction_success, transaction_status_to_string}; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L512 // DetailLevel: BASE (only successful transactions) & EXTENDED -pub fn collect_logs(block: &Block, timestamp: &BlockTimestamp) -> Vec { +pub fn collect_logs(block: &Block, timestamp: &BlockTimestamp) -> Vec { let detail_level = block_detail_to_string(block.detail_level); + let mut logs: Vec = vec![]; // Only required DetailLevel=BASE since traces are not available in BASE if detail_level == "Base" { - return vec![]; - } - - let mut logs: Vec = vec![]; - - for transaction in &block.transaction_traces { - let receipt = transaction.receipt.as_ref().unwrap(); - for log in &receipt.logs { - logs.push(Log { - // block - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - - // transaction - tx_hash: bytes_to_hex(&transaction.hash), - tx_index: transaction.index, - tx_status: transaction_status_to_string(transaction.status), - tx_status_code: transaction.status as u32, - tx_success: is_transaction_success(transaction.status), - tx_from: bytes_to_hex(&transaction.from), - tx_to: bytes_to_hex(&transaction.to), - - // log - index: log.index, - block_index: log.block_index, - contract_address: bytes_to_hex(&log.address), - topic0: extract_topic(&log.topics, 0), - topic1: Some(extract_topic(&log.topics, 1)), - topic2: Some(extract_topic(&log.topics, 2)), - topic3: Some(extract_topic(&log.topics, 3)), - data: bytes_to_hex(&log.data), - }); + for transaction in &block.transaction_traces { + let receipt = transaction.receipt.as_ref().unwrap(); + for log in &receipt.logs { + logs.push(parse_log(&log, &transaction, ×tamp)); + } + } + } else if detail_level == "Extended" { + for transaction in &block.transaction_traces { + for call in transaction.calls() { + for log in call.call.logs.iter() { + logs.push(parse_log(&log, &transaction, ×tamp)); + } + } } } - logs } + +pub fn parse_log(log: &Log, transaction: &TransactionTrace, timestamp: &BlockTimestamp) -> LogEvent { + LogEvent { + // block + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + + // transaction + tx_hash: bytes_to_hex(&transaction.hash), + tx_index: transaction.index, + tx_status: transaction_status_to_string(transaction.status), + tx_status_code: transaction.status as u32, + tx_success: is_transaction_success(transaction.status), + tx_from: bytes_to_hex(&transaction.from), + tx_to: bytes_to_hex(&transaction.to), + + // log + index: log.index, + block_index: log.block_index, + contract_address: bytes_to_hex(&log.address), + topic0: extract_topic(&log.topics, 0), + topic1: Some(extract_topic(&log.topics, 1)), + topic2: Some(extract_topic(&log.topics, 2)), + topic3: Some(extract_topic(&log.topics, 3)), + data: bytes_to_hex(&log.data), + } +} diff --git a/blocks/evm/src/nonce_changes.rs b/blocks/evm/src/nonce_changes.rs index d3c13ea..98f928b 100644 --- a/blocks/evm/src/nonce_changes.rs +++ b/blocks/evm/src/nonce_changes.rs @@ -1,33 +1,18 @@ use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; -use substreams_ethereum::pb::eth::v2::Block; +use substreams_ethereum::pb::eth::v2::{Block, NonceChange, TransactionTrace}; -use crate::pb::evm::NonceChange; +use crate::pb::evm::NonceChange as NonceChangeEvent; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L726C9-L726C20 // DetailLevel: EXTENDED -pub fn collect_nonce_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut nonce_changes: Vec = vec![]; +pub fn collect_nonce_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut nonce_changes: Vec = vec![]; // Collect nonce changes from system calls for call in &block.system_calls { for nonce_change in &call.nonce_changes { - nonce_changes.push(NonceChange { - // block - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - - // transaction - tx_hash: Some(String::new()), - - // nonce changes - address: bytes_to_hex(&nonce_change.address), - old_value: nonce_change.old_value, - new_value: nonce_change.new_value, - ordinal: nonce_change.ordinal, - }); + nonce_changes.push(parse_nonce_change(nonce_change, &TransactionTrace::default(), timestamp)); } } @@ -35,25 +20,29 @@ pub fn collect_nonce_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec NonceChangeEvent { + NonceChangeEvent { + // block + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + + // transaction + tx_hash: Some(bytes_to_hex(&transaction.hash)), + + // nonce changes + address: bytes_to_hex(&nonce_change.address), + old_value: nonce_change.old_value, + new_value: nonce_change.new_value, + ordinal: nonce_change.ordinal, + } +} diff --git a/blocks/evm/src/traces.rs b/blocks/evm/src/traces.rs index e53e859..6c0d866 100644 --- a/blocks/evm/src/traces.rs +++ b/blocks/evm/src/traces.rs @@ -2,11 +2,10 @@ use common::{ structs::BlockTimestamp, utils::{bytes_to_hex, optional_bigint_to_string}, }; -use substreams_ethereum::pb::eth::v2::Block; +use substreams_ethereum::pb::eth::v2::{Block, Call, TransactionTrace}; use crate::{ - blocks::block_detail_to_string, - pb::evm::Trace, + pb::evm::Trace as TraceEvent, transactions::{is_transaction_success, transaction_status_to_string}, }; @@ -22,98 +21,64 @@ pub fn call_types_to_string(call_type: i32) -> String { } } -pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let detail_level = block_detail_to_string(block.detail_level); - - // Only required DetailLevel=EXTENDED - if detail_level != "Extended" { - return vec![]; - } - - let mut traces: Vec = vec![]; +pub fn collect_traces(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut traces: Vec = vec![]; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L121-L124 // DetailLevel: EXTENDED // System calls are introduced in Cancun, along with blobs. They are executed outside of transactions but affect the state. for call in &block.system_calls { - traces.push(Trace { - // block - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - - // transaction - tx_hash: String::new(), // As this is a system call, tx_hash is empty - tx_index: 0, - tx_status: transaction_status_to_string(1), - tx_status_code: 1, - tx_success: true, - - // trace - from: bytes_to_hex(&call.caller), - to: bytes_to_hex(&call.address), - index: call.index, - parent_index: call.parent_index, - depth: call.depth, - caller: bytes_to_hex(&call.caller), - call_type: call_types_to_string(call.call_type), - call_type_code: call.call_type as u32, - address: bytes_to_hex(&call.address), - value: optional_bigint_to_string(&call.value, "0"), - gas_limit: call.gas_limit, - gas_consumed: call.gas_consumed, - input: bytes_to_hex(&call.input), - return_data: bytes_to_hex(&call.return_data), - failure_reason: call.failure_reason.clone(), - begin_ordinal: call.begin_ordinal, - end_ordinal: call.end_ordinal, - executed_code: call.executed_code, - state_reverted: call.state_reverted, - status_failed: call.status_failed, - status_reverted: call.status_reverted, - suicide: call.suicide, - }); + traces.push(parse_traces(call, &TransactionTrace::default(), timestamp)); } // Collect transaction traces for transaction in &block.transaction_traces { for call in &transaction.calls { - traces.push(Trace { - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - tx_hash: bytes_to_hex(&transaction.hash), - tx_index: transaction.index, - tx_status: transaction_status_to_string(transaction.status), - tx_status_code: transaction.status as u32, - tx_success: is_transaction_success(transaction.status), - from: bytes_to_hex(&transaction.from), - to: bytes_to_hex(&transaction.to), - index: call.index, - parent_index: call.parent_index, - depth: call.depth, - caller: bytes_to_hex(&call.caller), - call_type: call_types_to_string(call.call_type), - call_type_code: call.call_type as u32, - address: bytes_to_hex(&call.address), - value: optional_bigint_to_string(&call.value, "0"), - gas_limit: call.gas_limit, - gas_consumed: call.gas_consumed, - input: bytes_to_hex(&call.input), - return_data: bytes_to_hex(&call.return_data), - failure_reason: call.failure_reason.clone(), - begin_ordinal: call.begin_ordinal, - end_ordinal: call.end_ordinal, - executed_code: call.executed_code, - state_reverted: call.state_reverted, - status_failed: call.status_failed, - status_reverted: call.status_reverted, - suicide: call.suicide, - }); + traces.push(parse_traces(call, transaction, timestamp)); } } - traces } + +pub fn parse_traces(call: &Call, transaction: &TransactionTrace, timestamp: &BlockTimestamp) -> TraceEvent { + let is_system_call = transaction.hash.is_empty(); + + TraceEvent { + // block + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + + // transaction + tx_hash: bytes_to_hex(&transaction.hash), + tx_index: transaction.index, + tx_status: transaction_status_to_string(if is_system_call { 1 } else { transaction.status }), + tx_status_code: if is_system_call { 1 } else { transaction.status } as u32, + tx_success: is_transaction_success(if is_system_call { 1 } else { transaction.status }), + + // trace + from: bytes_to_hex(&call.caller), + to: bytes_to_hex(&call.address), + index: call.index, + parent_index: call.parent_index, + depth: call.depth, + caller: bytes_to_hex(&call.caller), + call_type: call_types_to_string(call.call_type), + call_type_code: call.call_type as u32, + address: bytes_to_hex(&call.address), + value: optional_bigint_to_string(&call.value, "0"), + gas_limit: call.gas_limit, + gas_consumed: call.gas_consumed, + input: bytes_to_hex(&call.input), + return_data: bytes_to_hex(&call.return_data), + failure_reason: call.failure_reason.clone(), + begin_ordinal: call.begin_ordinal, + end_ordinal: call.end_ordinal, + executed_code: call.executed_code, + state_reverted: call.state_reverted, + status_failed: call.status_failed, + status_reverted: call.status_reverted, + suicide: call.suicide, + } +} From 7230bda3fa69b5df503f3f20e5c4fc30c1a06995 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Tue, 19 Nov 2024 15:36:05 +0100 Subject: [PATCH 17/20] refactor storage changes --- blocks/evm/src/storage_changes.rs | 65 +++++++++++++------------------ 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/blocks/evm/src/storage_changes.rs b/blocks/evm/src/storage_changes.rs index 9117e7d..4e9432f 100644 --- a/blocks/evm/src/storage_changes.rs +++ b/blocks/evm/src/storage_changes.rs @@ -1,35 +1,19 @@ use common::structs::BlockTimestamp; use common::utils::bytes_to_hex; -use substreams_ethereum::pb::eth::v2::Block; +use substreams_ethereum::pb::eth::v2::{Block, StorageChange, TransactionTrace}; -use crate::pb::evm::StorageChange; +use crate::pb::evm::StorageChange as StorageChangeEvent; // https://github.com/streamingfast/firehose-ethereum/blob/1bcb32a8eb3e43347972b6b5c9b1fcc4a08c751e/proto/sf/ethereum/type/v2/type.proto#L647 // DetailLevel: EXTENDED -pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { - let mut storage_changes: Vec = vec![]; +pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec { + let mut storage_changes: Vec = vec![]; // Collect storage changes from system calls for call in &block.system_calls { for storage_change in &call.storage_changes { - storage_changes.push(StorageChange { - // block - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - - // transaction - tx_hash: Some(String::new()), - - // storage changes - address: bytes_to_hex(&storage_change.address), - key: bytes_to_hex(&storage_change.key), - new_value: bytes_to_hex(&storage_change.new_value), - old_value: bytes_to_hex(&storage_change.old_value), - ordinal: storage_change.ordinal, - }); + storage_changes.push(parse_storage_change(storage_change, transaction, timestamp)); } } @@ -37,26 +21,29 @@ pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec for transaction in &block.transaction_traces { for call in &transaction.calls { for storage_change in &call.storage_changes { - storage_changes.push(StorageChange { - // block - block_time: Some(timestamp.time), - block_number: timestamp.number, - block_hash: timestamp.hash.clone(), - block_date: timestamp.date.clone(), - - // transaction - tx_hash: Some(bytes_to_hex(&transaction.hash)), - - // storage changes - address: bytes_to_hex(&storage_change.address), - key: bytes_to_hex(&storage_change.key), - new_value: bytes_to_hex(&storage_change.new_value), - old_value: bytes_to_hex(&storage_change.old_value), - ordinal: storage_change.ordinal, - }); - } + storage_changes.push(parse_storage_change(storage_change, transaction, timestamp)); } } storage_changes } + +pub fn parse_storage_change(storage_change: &StorageChange, transaction: &TransactionTrace, timestamp: &BlockTimestamp) -> StorageChangeEvent { + StorageChangeEvent { + // block + block_time: Some(timestamp.time), + block_number: timestamp.number, + block_hash: timestamp.hash.clone(), + block_date: timestamp.date.clone(), + + // transaction + tx_hash: Some(bytes_to_hex(&transaction.hash)), + + // storage changes + address: bytes_to_hex(&storage_change.address), + key: bytes_to_hex(&storage_change.key), + new_value: bytes_to_hex(&storage_change.new_value), + old_value: bytes_to_hex(&storage_change.old_value), + ordinal: storage_change.ordinal, + } +} From c103b467d1b60d8eb2b8c33bde241f4857dcc830 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Tue, 19 Nov 2024 15:36:59 +0100 Subject: [PATCH 18/20] fix storage changes --- blocks/evm/src/events.rs | 1 - blocks/evm/src/storage_changes.rs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blocks/evm/src/events.rs b/blocks/evm/src/events.rs index 1aa1529..89976a5 100644 --- a/blocks/evm/src/events.rs +++ b/blocks/evm/src/events.rs @@ -20,7 +20,6 @@ use crate::transactions::collect_transactions; #[substreams::handlers::map] pub fn map_events(clock: Clock, block: Block) -> Result { let timestamp = build_timestamp_with_prefix(&clock); - let mut events = Events::default(); Ok(Events { blocks: vec![collect_block(&block, ×tamp)], diff --git a/blocks/evm/src/storage_changes.rs b/blocks/evm/src/storage_changes.rs index 4e9432f..99abdc7 100644 --- a/blocks/evm/src/storage_changes.rs +++ b/blocks/evm/src/storage_changes.rs @@ -13,7 +13,7 @@ pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec // Collect storage changes from system calls for call in &block.system_calls { for storage_change in &call.storage_changes { - storage_changes.push(parse_storage_change(storage_change, transaction, timestamp)); + storage_changes.push(parse_storage_change(storage_change, &TransactionTrace::default(), timestamp)); } } @@ -22,6 +22,7 @@ pub fn collect_storage_changes(block: &Block, timestamp: &BlockTimestamp) -> Vec for call in &transaction.calls { for storage_change in &call.storage_changes { storage_changes.push(parse_storage_change(storage_change, transaction, timestamp)); + } } } From 1f76712f4cc3fd58626188e0983ccfccf83a2a41 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Tue, 19 Nov 2024 21:26:33 +0100 Subject: [PATCH 19/20] update readme --- blocks/evm/README.md | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/blocks/evm/README.md b/blocks/evm/README.md index 2a28c16..bc91aca 100644 --- a/blocks/evm/README.md +++ b/blocks/evm/README.md @@ -22,20 +22,9 @@ ```mermaid graph TD; - raw[sf.ethereum.type.v2.Block]; - raw --> base(BASE) - raw --> extended(EXTENDED); - base --> blocks; - base --> logs; - base --> transactions; - extended --> traces; - extended --> balance_changes; - extended --> storage_changes; - extended --> code_changes; - extended --> account_creations; - extended --> gas_changes; - extended --> nonce_changes; - extended --> creation_traces; + map_events[map: map_events]; + sf.substreams.v1.Clock[source: sf.substreams.v1.Clock] --> map_events; + sf.ethereum.type.v2.Block[source: sf.ethereum.type.v2.Block] --> map_events; ``` ## Modules @@ -47,5 +36,5 @@ Kind: map Input: source: sf.substreams.v1.Clock Input: source: sf.ethereum.type.v2.Block Output Type: proto:evm.Events -Hash: e9a39ec8c7084a493d9a9f60c1f2f5d18f7505ad +Hash: ec09630461ab227a2e7448038cebaa91b49400bf ``` From 67c8ff3cac81932fc8acb4e7bcebc05485ef136c Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Tue, 19 Nov 2024 21:28:43 +0100 Subject: [PATCH 20/20] update logo --- blocks/evm/logo.png | Bin 23508 -> 86648 bytes blocks/evm/substreams.yaml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/blocks/evm/logo.png b/blocks/evm/logo.png index 4c4766ee1444716397cd01c2204f5bc667bba7cc..a15a39cc6539cda69562888974b0d2873657c950 100644 GIT binary patch literal 86648 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%0AtV zIE-nHM<)m@8r2O|r1$9Z-4OA$TuzGiBpW;5n+b~-8m$tb?9h3_Zr>$l=e+i`!d}i| z&B&p-M$((@@$hTl0u9%F9U=W^9wd4S?658Dd{hs-)4t*nLX*5U90syI$EBl{6T=e; zk`XvkOqxBwaJOg9X;0#;K57|&KvST_%3~+#wnXP64Sv++zcrW^_6c9y?U3O13P4)6 zvWP1(%o7}2l3>7{31EQm0=OhG8ou@L zN_}{OSEU(8F9gj+hqIO(11N8My1eekq#u1)<+Rbm=-KVEvEK2qFdLAvgaXkul+nFn zWTHqz7$&TS5Uih{$Gln^t?J2LQO)1O)}1YemiCqd8YW0|L5Y56@>z z8Oqyj4m)o_Qek0sW|i6sQzi8zKb%LzV8Khri;cM%0h8cW>Opq1cIwl2xo6@dyZmE# zW?=W?QiJNJIM3gP=Q^wa;76zCDQXzr@E0Q>UjV!JQoFR~`uZDzQ~qjjmEZZ4Uf3T7 zDjKl-vJ-Th7ScHKbL;Zet&JLR)DTCb1p+PW2O zX8_g2H$*WkVgB{pL4&dO_!X<;@8iuPoBs}b%=`=(#i{U11if2Zizgz+s^53;7ihb& zF6}P`b^WJmwpA=5re}vg0}{@x0o+;UbNFEWwFJM$Q8^TdmSvX-qAO4+U$6NIh;aG4 zLOn94I4r3gEh@6ehDLEjJL9H1!g*D{qBih2l)6JhK!Ji8$AoSZ^CK_g3o214)yz9R z2t%>UAL>S$;iTgj@+i+7i7@QFT8}#`ahD143K|R|41ACmhw6TWu+Ld3X_F;~zkXvHsL4RVl*Wbx7br|?fW(nDq^F&tYe!PVNq;%&(lOoz!BfO=N z_tPbjqa`Luv_Ncc(OSA&^PX`}xdavs?CubbvTs)7nX&49!qP`5zcS`l6|h46+dbhe z=P*Y3Pt4d^%OC;2qaX%?jZ9?}=R1y$) zfyAG1VGg^d^0i!^=e~WuaZ&RmjY)DVnLUgZ0#bDdD*OGeIf$d)2G1*E(&aX)!lLLD-F#ew+SZy#*OU%- zQ&jY^52yUuAKGw~k95+q$p2<`Ojg-wj@-eIaLhR`wGQ#};7AFxjE~v|f{+hKR^gcO zbCQMZ5O>+5)m1}K7>LCweSl^~R5x(y6=6l+bGm>|%&Z6u2~MiD<}rPWzHT5r82T{e zH9j*##mU=!dxoTA4PtY{$?8D3Pq4OEv&}q)DX;ECdua2$`j1Zo=w+PmxVbQY{s zQ+>KG_Kh6!pPeWS4@l_H&`C64ZDtXk6&Z6N9RH=xLLLz)%>$AB0g#y0Vbi-aZhmir zp2QO-25L8?fT^inH(3}_5fHo6yZTwS z9o3g6m4Vlp^LMRLD}-oada1b$M7Z`eswo`^04tBY@7Lhi^v*1KZvvCQX_dt*eLj;! zVGZxU%M>lK*37BuzcsM15y7yKX^sJ>I|rXV@pg1h-)ZDke@?em316949h83^Vwh}$ z>NZ7oGjenf)$>8wT<%&Y3?O^ayBtZ+o-wI$0TO4%>_18p^b#>=Ig9-bD|b9(j7{jgqkF z3U9$1*qLJpbIa(_(T5{fXY=lEAu5(kT-q&X2=@ym!q+J*RL`1LAUDC5q=JiHEc8^P zT!D%vEr?u_mUWlSrb~4}lL0wcflu7wDG=LbWIkte93oUSe)T^Oawzc1w9F}byAbVu zye%dg+WAj5H#4)f*<1%^c5R=gKS%wt?D72k@@wpKoZXYfB>)6e&3w6=L+*_L$7=|r8Z&p^(GxUZS|i=t@2a8=Xm#O9Nfic*@x zWn#V?Ra1%Ncf3}$hIE8`+14Wq?xP8T#*Xcn=T0}(0^7<$HU9jsafocb4k&FdfAa{B z#OLz90IE98C~lWgeds3q`6%f5X5ShuywOX=X_%{`X}oCu#mld08YV^RXuY!89x+D$ zxteBcWE_Xpr6<%S(aELIqEB1P#dOs4zX&twe^2eTI#AFUKyA0pHtkoTuVV!QaRT&u zhDBCr5B<#*mGG36p6x{*pKbB^&CL#9fF8#PAY1k{$_Z48dWMt2t|fk!I*v*pFam-Q zBpUqdpwae|t-&BFZ1_Y^lZgA2EjF|s8(YtOh`RIci1r>(*=peUiyPbZh9~@S;79n+jX7p1^uXpb#%{(EKiT1492i&3N21O_ zSu16pj`8_DGuV5nj-})pqPffLn$q(-V`KXE?oVw#BC9@1ykOhrcU5ag-LBj%_1SDN z=chRi$9+V3$Ll9+_o6@z2rNr{ic%sEt0V96uvVb!R;lTGl63AZj@s|lu@uFwihvVP z2}?*3&M>|`P}cNJJXp$vy*2!QFRcvukt43ilYn-6#=GRZorFw&O&)xy8qie{MW8^k zLHn=I=5P{WQ_+tSu972u?H=xWS~|gh9(;wXS!x|W9*%h};tV3nne{1VGviKRJi8}) z)00G-{7-M)<%v$XDs5=l*;&V))k{0tAa0Inn5;bC>u2KA+GkmWCyCZ%%c^}HU71w; z9E7cyWs2rxzEEoy=Am3^#sAgV|5PjsKcF)wDy?agt^EUnp9G<{Zi4Kp7fny;+03fW zP%UpaEF_L5hKGwXV9}EWdy+slrrulq@|EZ>N=xr{`E+}IE;A|p{d+wOtM%Ei_rSBl zVAQ6CiGU~N>G(j?=$8@E4T44(1Rge%H7I2HF#eOeyL!EdURLR6HSUj3{|tqGy2)Y* z$oJWnmG~d^!Xn!y_k`cfyKb@*jUO5|K2_^E98~L5$w0msDPg6PNBN7aTW#2u`>vC8 z*D-9V%kpRg{McUJ9Q?W-BLQujTIMaU7$6bsHhN;d%=l0}v*066l*ORo=uruHmkN*&dq|~?qvc6(a_U9aE zz^Rb-bl{>^1k}FZmnUNBAQ}SN(g=nyhpq2AlbLa}{3dx8byBUtaQFF8B8jbg2NxM_1(NViFO*c6uEphAi~EX1}@b@pY2v z3q}Ic!;dK9g9NPwud}0QHVTE((&>_51ADKY0?s*0a3rHNbOsX*TusY6+~>m6aswcZ zy%@yontgi^65dDJAH$F6MC(Q4O&c??<#|^f>6)aKh$4F_D@ghqJxnuf;7cJnIoZ33O%;c|-e#UiXe}-FFzU`Pj=N+ScADKNcTl8Xa0g*j(JEO*dP* zx8QQQ1O$6ccsFZ79+Qp__H3hBL1;oKqiA{gr#_{`r(XirRB;p^=rovm+z$j4x{?cn zNnVV^4f4Ve8dgMQK_l^PV=_qv<&pnbVcsN1!C%1Q$e`@ayGKcWmaxOJ+8=ih7X&ZE zGY+bpE9Q2Ts{M{&`Dxjohrthy=-#?Ikuvcs@oSTa;0PHsy4uaYdRo78&k&fMts0^_ zs9N}`eZ%SY`Mo^!P){|nP3Xg!?|^~_xofxpgukaKIN+Pp?fFdDc;$;Gq5JS0TaNTN zDDr7I9aD}gT0b)rYSCMHn$^5-nxeg@+Wifr|w9K3)R*$)eQnilgg(! zUaP0|MS1IDoJTMG^Xo)gv3g#<*E~K7CkM_=@Ty(G!@Bj>1rHf7%^;6_i$n0CRUR%AG5%T}~I}b@esG_x4!Ua}Qadr5#2F`7qQ;8{f zy$1D*HeLt^f;CQb@l<`G1#5%pp$GPZ<_zr`F)8iH?3Jr*tzYM2qaGU2yIaJHLxty5 zi7Z(sh?bR@i;3!Cf#pYP-Ob^9@{A^0Rg&ESyB=>HjG+mL??7xUWqE;U61A4w1Bb9; zs{K$)q~q}a%?d98IvWea)+p^L#7|_~_Q!>Px>DP*eB04yP0}t3Sx1XFvT{?7B>c|&!nk9W;$<}{ z7i$!E%`b|?M7HkNG$3gEw4Hm~wFpmjG0(nW@G$#JLM(Ez=SRQ3eP(ewQBC6&DpuH^ zt;+X}5EbB9e)LxVc=`jlfu=Dm$#Kz=;YJ?_I$9{3PMC!{p(}sRi^cJ<_hvh z^H~93ndwlHzlP7qPl*Z|NTQ zCxM>VLVYyZ7S24q z5`v}h*PPi2SI>1o*gG^#-uqI~5pe%p zHdsnNV~=v2E^d>#<7?Pqz5zHqeUTOqN|HAq#qMgzdFiTwOy-A!I@y`t{WV^kXKBW+ z1`+`kdZ{N~Nf;Dy;R0jkpNMgKdY{YS5Dh2!_xL=u9^t85)NZxre!;}Av01;ig6r;h z=`!p_ux5wKK79YBp1t>vTZ`Y$O?o8@ajfoi+z7@0JjgqOXuz{Nf080qYrgPK*}N+; z&!Fs6>bodzW%~CYYa30p$M2rGRRjU=E=e>0N!)8ZX}r3M9xaym)bk64NZxw3VQgph z`h9W`SQR#=sI6VBOrjxM>btmDL$`qyumPaK1RWirgf#m@&qoP*S$gD8YaHy2Sq35|4Qc;Red}z>4W0IPFL@Tnq0dbaaZfb%_ z!0FN^m^D6SG311PxwxMMa%oE|6cNt)HN||JD@T-RHIPhrbQK8~gzGS*ker8z?f82$ z>&8|_7RQ|;xBMnss-oI2-M{=NT48ntf8)ex|5cP2q_L zZD~0bj5*rWoXA+4tI6$PTVQ$+dW#^pGaZP;1l$i4p0>KwohE4Mgn(&*^a$J>i6(HC z9B}^Up(s=U!xHs0mVt#^SfT%1X3n5Iu5avv^jg_{8H5?y@3Q>s{h%y_L_R@_!+KzP zPE52ZviAy>!vJM@+WtU+S6W>NsmkkNS){PBj72+WF9y?g+Kl8{eks)aqT*M(7=37P zuBJxhQlp#5mJ}6QSeU|<;Th50Fwh6{0lsu`MVhE0wt{3JjgHLnajaYL8;~C6OaKd$ zE)H!H8`k?WK9fZofV9VhC@wEX;U;WM-cm0<{<8 zb9yTi<{61*#G_t_~FvynTB8Ke$zEhFARK< zsxb~iS6Gg97OPUJv6_w#{#~_cj>sRtd=ULu#T+O#ni48J5-)zM#+1t*TI%Is_Ne)K zs-jlY=kFVt{cnIOm%>ls0{LO#;yQhV(Ykv|5`nPpdk$Q*VWKj&QjML|ANQuq@MpRm zFM2%AcFP+N@tClGV>5X3Z!)&w494_YVQE?8)4G#X4r<^KTO`8EZ;0HpHs0mcynOM3 zdL;kK8!C|1%Bx=ACBe!)QsyrZ&7`v4XN82xt{$Np{;Z25Sxb#uymv({=b9sMG?_L7 zj_`{=yGMlxVZ;ItmhqR0Tv39-IIrU<(?<|oSnmyFF9=}*l>Pw|7j<6wqj}${B(LWZ zw;%I|DAgFyCkSC&zVvSR)9sXORA89Eu(H$NZurfRGzR7kjN2E`UJ2f&A9p6H8RxH< zS50qfCVE1aUW5+4xbs!(6#96En;Q!PwZvMMBV#H(0eB<~&N6LKeLjUm9ry%0JN`x> zi`@SCHU0qa3PfWf#4c;%sJ_K^n;lO<%*D0~rOx?sou^N6UTIkG*R1t`H)5R@2<OMA9- zb71}wRH&MYeBpj^=@}hDO=b@qPMk;X;_MWDl+t>4Lp&+Ry#5CVp%1;<)(rWwWBw?5 zDTHA9>K=T7j{kHD*I_}-Nt9cnWlI(MohkY~0tu~akx}{WurF(Y9AcQ>J0+U+sN|VJ zCPaT0dF}q0nQ1=!;yTEaLqjDvo=#IZmkyF=lY}2O!ijv&dR*e%uy4)mFC|ySrIi&j z$ENr2@DBHutOIHBXu z3bQPLu@}~p64l-31&}RdpqKoCePO^G;Pfyx2~Av3@Y`j02%UaKW!uWV9X(t1zAhbs ztJr?Tn!&inu}HiYSV=)3x~%x39Wy80TVzKpG(KZiYrrk3u~E&|!GWIa#aKf$d4WbP zuT6~#KWzqs2vj0Y*<{ypELrvCh3=|!D|OA+Y5tZf zUS9#Vz#c4YY`NJ-5PW*Wb#&JNOXjsM%}S%fwb@h>1O!9A9761DVb8#+-? z>>MW7Uy}OhEhrK?Xb!&QaIdgrXtWKB1#@io9+g1P>EY3m91fV5a;;#hS)HDCNxbe! z#(%Li=AP*xHSNzgRG=jz~+5gp_HX4l1ffyBnlKV)Mm8yHEODWQ=Q9!|X zbJdXc6zG(`r0vxJf;Q;1i$uAq)Tyt?sq_w#go5hnVm?QC)|gs+r}RZn(=j7$uF+GLmH{>4;7I0Vk1raKw7?m6 zzNWidhEl)nykIHx(k+JD9Z2Ogm@!4;H}$um*5jzfvpa4!#A|JHz0VTqgFV&x1w;^BNq)lPNT5LEbj8ZbMituF?G;N% zpg}<4{I0SwX2zdJ&28Dky02<2SDO4_V52^uHw<>tmKjux7tR1q+dF;SbD`f7{FCrA z@t9+0>BK;nw$((8B*B}N8izB5^nIpMT}AY|{-{_wT6v8QOtRscFT z4CPO_{5oTD(-OZaXzzp*!?1Tsr}u14^fLZ)>JhtxQ(s6cRfYKSM#JW+`%Ghbd9~A8 zpPablSuXJ$g}81+g(it73anMEBlzkF-fEUhoF5KEzKkGL1N3YRF_9zh)2IFdG^Ekx2bV+ z@$J{|jarV07NAa+Bx39D-06ge4BYC#BS}vijyGF3vRHF~4ODJUzeANoeD?8Z%wSW4 z!XlWu-EqzRgoV)%>^k?p-SL={+n(fPAx2TMSBQ?vYdgpo9L@7J?2ltKyUq}N;g+5@ zM$Ce`2JS9$x(eLKV|(%*l+IO*#ABbPcjRcxWr;9V@I^LOJ`oWyc%tKU3nE@NWEAxi0aI=!u{iZSDt0bLhk%vr(XnUq;>3 z`RPi9`%xehd{lZ7X64O(v*=R!!i0gC&`+2;3en!|@-O9Z+(XmLR`8_?r#&JETK^!R zkRp${-u1+5TlvH7jlVU-$y0DhFcf7Xr9L;=6GexU-wY|lfzY@xO7r5_ZW;tfNxPZQZv^{q14QtvEteozdS zTBKH5+Q^9RUWDq)80fnC)H$q}FxY1$SB<52_HDR03sp{Ny7#}u%7Ob@ej~jZwIP6R zV@*z;Sz4x};unG#u3@=i;q26|IL1im>hq-RzgJ-Nqr!QDdUBFvuztsmy&9|7%ry35!UpSmtR<9cClpVB&o0}KmB@7kqsds;@bMPs?&^8R&I7<#F` zX*4dJu^7Efzbenam7Q$I|KH&4^L_Ymrx^TqK8`)hE@ww zRG#T_(=WwbkCq)}Wg|tDyG|9qFk)!=`Q=?PA&W9IF=Fi@#xd&u z*lMw80SSgy1rYOvmby{h^7@$z0=prXFMz{+LIw4IjbjnU_OptM{>kzL;d`sN4wBeD zo}cf2xxtsOn=Ps=m`AAyUJ-Cb%kil`hKa7q+ItH>+4U`_X?78lzW!@Y?=JLbeP$TM zy#0&sc|9dF_IeL8nK|1;6BKOoV=4r+&vjz1DjpKC(9lq#(|zZ5%a(37xI@XU4MCj?AHg=ui28cJ(FVdwkoZj*GgS?Un4s75 zWI}e66PU&89|*KG@ocexZ~ zWNRsb2!$HqFNxi;{`{}lm&7{>!NhTn`8Ld&M7&gRw8n=NFwRx#mHHUWgr2>chM>g{ zv^`>UAkzT&JFmo^ezM%!L2!?VXH)c)*GdPLD5HMO{jXSwlatdiOdKI5*BS*o-V)pNoZ^QF(ZaLf-LE~)eeo!Cg2|R zBb5H6v+wL*YIYO}Jnqo04zn_L?r2c6^*! z2|6M=Fc+l@R)#TRp=uv3{Uk(!I9niG#F-qABl@=avq>@F6P$0f9lMO6QgjQX!}DosOA9h5lH?=_N>b z6DBAh;$_C#!+RYgezNFQ9^G(dcoK7^VKIH`GCzGwL!-iWWd4%W{So)#M0>3~4}cjn$7sSN5UJ9greZl*)8ZddT0 z#u=1iB1iw>f%DsFK~1Q6dx@hM%BCqoejhHG{%c!eIDyt_l2qB3<~yD1wJ0N_-idZ` z@;UkF@PdHq+$Tib!nEX(Sgmmfo5_LujBSMrch70S@mXQkJdiv+5jo-R*hzCf?evAN zsekX1)6RpJPt6$|fxO%^bJ{7>X%i^(onT{`^Ri)joPA=)eV^qcPPCu{(dn9=@C&d0 z@W?YGTRq>UYO?r~Co*el%R*zS-@=FJOnH&xyP9g+Vj!@;Mt;+H6UV5RLLGX{-Y zv2Rhhef-6f^q0SN#_68S(aTV4#Lryf$3dgH81I*o4m+iowUsfMkvlTTtPhbltuEwa zO)tV)5f6g+7l41soC)-6p9%!A7nJ?=k}Oo~8`LUeYb!Au?p?RNOClr+5Z7+IdXH|6s#t~zPUz-RQN9g~}|U6Ng-w(br! z7q@UKLcqKI_vp&V2b^t*fU%j3V(h}O2*Z>*U%!DyA5%gxous>%kXS_Q(Gss>(JW;( z?6|s;l|V;1x-pGH;*w1`6yDRrZ~T7^xBV#?c*M!Tom&=GpgLsj^rx*i4~#zara{kj z$GE(7w-v2y)4afjw5t7~n3Zn@i$1deJIW}(nd+}37H5sR(ig6LhH(P#3_95ADwGx! zQ*HK<3j#)Vi0jq`(zu71l0z{-a`H2|dSYlBg;{XcJZEf(<~iCvs62)z1;64(waFovUb)tP*!43>g=3W(6CGELKD{a8RT! z6rD77cww%cEqo-H_uOmAsn5sw1f$q2JR6@f*o#JVs*TcauBH~oc>)>w2Q>Lh(jZ-g zFlumStfS_Zx8s;+t0?Hekyb?i1L_!yy+Vt40uR{AcL;@(`^h?V=SgEoh&v}!@f*9k z1A%_ULn&#E@xXk=GTvGy)TcdhAuSw!k zdUuwhw;K!FTyue`ovAJCh^S%De$HLkvbniBkHpk=w$a|RcitRKec?TeakIjTt)r#5 z0iA#-iOKergoE)?8Civ0_KIkeWgT|$8;<$z(ed%64FY->2!>u?rx#v?Y;dthZYq|e z!^;8_R_XB0mqfd_j=yRGWD~r*G$D}=$Bi1d4qCNZD_O1Q;~E($ zi_LAMegpr{P+`}xG!@^s)3uup*{QkC8|Z7;bw+!Jp+w3$1b|PU$%;>wMoJyxcwvY_ z;y_FYbw0{Q3KM!MWWmv6$0C-8tUzO9zpj+v2~c5BQFhjn#jfhZdOcs}@nV@?aB`GI z9veJ!6fBwA8AYc%#u^jDvOOx%6m$!R=$KeS7go?cS#padF@00M@10zSM1zn8&rDA($!Nd5{5^i4M$je?xT0 zfbDaSdWVPmaBF^xz&oUS@WZ1OTPrgn*o085Zwhud5n##qL$E7W8_thF#YNEV)+y;B z^?r|(N(eR@Ql?p>5wZ^;CM0Yv(MF#OJhf}QQ)R<=Q-vpMJnZe4@*0Cq?5+D1|9VY; zKfoWYh!Yn!o(;74aEba7Qo=Wv0F3NrvU~>0?2sBvXK#G~I&h3f^$S`7;m2pXatew3 zLDSHbj!w(~vzkiOfkL&zSDlI0B+|z4i`eg^vLsTR3_aE08t&nVF^~!mDz=tnuFu1~ zC|~(kZj|s%k2${OH>cV1W{mD4oprbI(V!~q2XEaP5&WE6rJR%@Amty|ljn3aQZMy+e-*cN zUZh8s(s~rHMZ=Idh;g!-6?ikE$Y&Og$eM>)}I{- zop_EF{_@0NkL}yVTD}@`L@c$PqE3h`*_SQV37w@fA!Kv3IU45U4jv9Go*8iYYsOgn z1bF(nah@C1`pKaZo&yzl+|ZyZ{k45wjSM;9Gkd&DgtyMe4x?^q(2~^i;Zc)g}ZXRy;#e&*D80gBK<+SC4k>A z17!_&#Wmzg<#3#aFhbI`p`1poo4*+2sa&3r_ve9dKi&sn_k=?4=YGWStx5I8L@i^m zUo$tg1568Yvd~i@$+?t22Xu1wqsfSjbci9#7U5tmtsy2A#`0$Qak)W>@s}6&HveJZ z|8A?$1~?)DYOO}O>TpcyARmHKuj2M^sM(89^D41il9Eahb&Im6Ws#{!H!wusb-H_J z6$!-anL0n({^2DrW*NH^#6<`ib>vhzbg4;vKh*r({u!a zngdt?NpkOva-l<}{w&tk(WXX+ah5i~T9*6_T8mY5=j@d;h`LvQ`j1_?BD+d$mWa|W zg_TW8#|$&8H<>mvJgk$;2;i$;n2h|R6yA}rhKR9_$Lo0x&rIY$@p%}i+Y?X zFy!eLL0zsX0{#YZf0@`5#PDAImO>&y!1Z4f`bXIRs= zgYf3xGT8bbD;Bm)XvQ9?6>}io0MMWFw!h9xiDbIt_j1Z;0mbI=E@jq5oVzm4Y#ARR zq@VEXe7#Os;y{IF)@#UAq*`FeluI1Rz-jbW@l@mXo^EuiXnwMapmUd(&c9Ml}Cio&upK1qRm}- z`eavy8jD_fBtb;n&m#Q3>>5>+#?MGLh0IS)OLI06#Qy$>(ndl#l8n9G;G9KE2i~zi ze|+V4{lrKtx!EHL5MQX`K0D@%pKE6p3 zxqzer^=|hnf(Ohb7#Qxo`edi9G@)07qu7|`-_3Ne*=^VQa5UlohIk0V1p~B+urrxwN?F*wM+22(=}ut@nxQEe#r*@d zN+tQ`EtNQGS$^hOwrHL3ZsR3e4yE;v(Wv8N_$GgHmEc~|>rxBzRsY!G2gHa_cdwTc zc{-&S52aaqo>_Lgb>C??5B7Zq>eCeGi}~KU+cDwZ-+~C>RDb_a|5e(l;xOIvsxGQ0 z)a(+sU*<81_p55P8@_pAB;bWyVOH&4=^r5TWsKIV#PoswB!xCsH zX|wdLMr!KOw5_RDUay;jZipxP^3>3;V~C;SgX9 zqO#5PpvGElQWu4rfxg-KyrxRpkJn26e}mVc5MBIF_Hg&t?;xA}u}^jn4#@y7w5yj( J6C&XBzX2;;Wyt^l literal 23508 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 diff --git a/blocks/evm/substreams.yaml b/blocks/evm/substreams.yaml index 0723760..4dde592 100644 --- a/blocks/evm/substreams.yaml +++ b/blocks/evm/substreams.yaml @@ -1,7 +1,7 @@ specVersion: v0.1.0 package: name: raw_blocks_evm - version: v0.6.0 + version: v1.0.0 url: https://github.com/pinax-network/substreams-raw-blocks image: logo.png