diff --git a/substreams/Cargo.lock b/substreams/Cargo.lock index 1e3fcce..5db62d5 100644 --- a/substreams/Cargo.lock +++ b/substreams/Cargo.lock @@ -1682,21 +1682,6 @@ dependencies = [ "substreams-ethereum", ] -[[package]] -name = "tycho-substreams" -version = "0.2.1" -dependencies = [ - "ethabi 18.0.0", - "hex", - "itertools 0.12.1", - "num-bigint", - "prost 0.11.9", - "serde", - "serde_json", - "substreams", - "substreams-ethereum", -] - [[package]] name = "tycho-substreams" version = "0.2.1" @@ -1729,6 +1714,22 @@ dependencies = [ "substreams-ethereum", ] +[[package]] +name = "tycho-substreams" +version = "0.2.2" +dependencies = [ + "ethabi 18.0.0", + "hex", + "itertools 0.12.1", + "num-bigint", + "prost 0.11.9", + "rstest", + "serde", + "serde_json", + "substreams", + "substreams-ethereum", +] + [[package]] name = "typenum" version = "1.17.0" diff --git a/substreams/crates/tycho-substreams/Cargo.toml b/substreams/crates/tycho-substreams/Cargo.toml index f63e755..89c8d20 100644 --- a/substreams/crates/tycho-substreams/Cargo.toml +++ b/substreams/crates/tycho-substreams/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-substreams" -version = "0.2.1" +version = "0.2.2" edition = "2021" description = "Tycho substreams development kit, contains tycho-indexer block changes model and helper functions for common indexing tasks." repository = "https://github.com/propeller-heads/tycho-protocol-sdk/tree/main/substreams/crates/tycho-substreams" @@ -21,3 +21,6 @@ ethabi = "18.0.0" num-bigint = "0.4.4" serde = "1.0.204" serde_json = "1.0.120" + +[dev-dependencies] +rstest = "0.24.0" diff --git a/substreams/crates/tycho-substreams/src/lib.rs b/substreams/crates/tycho-substreams/src/lib.rs index 54fc71f..4da9a02 100644 --- a/substreams/crates/tycho-substreams/src/lib.rs +++ b/substreams/crates/tycho-substreams/src/lib.rs @@ -4,6 +4,7 @@ pub mod balances; pub mod block_storage; pub mod contract; pub mod entrypoint; +#[cfg(test)] mod mock_store; pub mod models; pub mod pb; diff --git a/substreams/crates/tycho-substreams/src/models.rs b/substreams/crates/tycho-substreams/src/models.rs index 96e0fc5..dbf05ea 100644 --- a/substreams/crates/tycho-substreams/src/models.rs +++ b/substreams/crates/tycho-substreams/src/models.rs @@ -427,24 +427,33 @@ impl InterimEntityChanges { } pub fn set_attribute(&mut self, attr: &Attribute) { - // Add any attribute creation to the map - if attr.change == i32::from(ChangeType::Creation) { + // If the attribute is created in this transaction, add it to the set of created + // attributes. + // Note: If it's already present in `self.attributes` it means this is not a real + // creation (it can be a deletion -> creation sequence for example), in that case we don't + // want to mark it as created. + if attr.change == i32::from(ChangeType::Creation) && + !self.attributes.contains_key(&attr.name) + { self.created_attributes .insert(attr.name.clone()); } + // If a freshly created attribute is deleted, remove the creation and don't emit the + // deletion. if attr.change == i32::from(ChangeType::Deletion) && self.created_attributes .contains(&attr.name) { - // If a freshly created attribute is deleted, remove the creation. self.attributes.remove(&attr.name); - } else { - self.attributes - .entry(attr.name.clone()) - .and_modify(|existing| *existing = attr.clone()) - .or_insert(attr.clone()); + return; } + + // Otherwise, add the attribute to the map. + self.attributes + .entry(attr.name.clone()) + .and_modify(|existing| *existing = attr.clone()) + .or_insert(attr.clone()); } } @@ -606,12 +615,130 @@ impl TransactionChanges { #[cfg(test)] mod test { + use rstest::rstest; use substreams_ethereum::pb::eth::v2::StorageChange; - use crate::models::{Attribute, ChangeType, EntityChanges}; + use crate::models::{Attribute, ChangeType, EntityChanges, Transaction, TransactionChanges}; use super::{InterimContractChange, TransactionChangesBuilder}; + fn create_attribute_change(value: u8, change_type: ChangeType) -> EntityChanges { + EntityChanges { + component_id: "component".to_string(), + attributes: vec![Attribute { + name: "attribute".to_string(), + value: vec![value], + change: change_type.into(), + }], + } + } + + fn create_transaction_changes( + attribute_value: u8, + change_type: ChangeType, + ) -> Option { + Some(TransactionChanges { + tx: Some(Transaction { + hash: [].to_vec(), + from: [].to_vec(), + to: [].to_vec(), + index: 0, + }), + contract_changes: Default::default(), + component_changes: Default::default(), + balance_changes: Default::default(), + entity_changes: vec![EntityChanges { + component_id: "component".to_string(), + attributes: vec![Attribute { + name: "attribute".to_string(), + value: vec![attribute_value], + change: change_type.into(), + }], + }], + entrypoints: Default::default(), + entrypoint_params: Default::default(), + }) + } + + #[rstest] + #[case::deletion_creation_deletion( + vec![ + (0, ChangeType::Deletion), + (1, ChangeType::Creation), + (0, ChangeType::Deletion), + ], + create_transaction_changes(0, ChangeType::Deletion) + )] + #[case::creation_deletion_creation( + vec![ + (1, ChangeType::Creation), + (0, ChangeType::Deletion), + (2, ChangeType::Creation), + ], + create_transaction_changes(2, ChangeType::Creation) + )] + #[case::creation_deletion( + vec![ + (1, ChangeType::Creation), + (0, ChangeType::Deletion), + ], + None + )] + #[case::deletion_creation_deletion_creation( + vec![ + (0, ChangeType::Deletion), + (1, ChangeType::Creation), + (2, ChangeType::Deletion), + (3, ChangeType::Creation), + ], + create_transaction_changes(3, ChangeType::Creation) + )] + #[case::creation_deletion_creation_deletion( + vec![ + (1, ChangeType::Creation), + (0, ChangeType::Deletion), + (2, ChangeType::Creation), + (3, ChangeType::Deletion), + ], + None + )] + #[case::creation_update( + vec![ + (1, ChangeType::Creation), + (2, ChangeType::Update), + ], + create_transaction_changes(2, ChangeType::Update) + )] + #[case::creation_update_deletion( + vec![ + (1, ChangeType::Creation), + (2, ChangeType::Update), + (3, ChangeType::Deletion), + ], + None + )] + #[case::deletion_update( + vec![ + (0, ChangeType::Deletion), + (1, ChangeType::Update), + ], + create_transaction_changes(1, ChangeType::Update) + )] + fn test_attribute_sequences( + #[case] changes: Vec<(u8, ChangeType)>, + #[case] expected: Option, + ) { + let mut builder = TransactionChangesBuilder::new(&Transaction::default()); + + for (value, change_type) in changes { + let change = create_attribute_change(value, change_type); + builder.add_entity_change(&change); + } + + let tx_changes = builder.build(); + assert_eq!(tx_changes, expected); + } + #[test] fn test_transaction_changes_builder_ignored_contract_changes() { let mut builder = TransactionChangesBuilder::new(&super::Transaction::default());