diff --git a/components/chainhook-cli/src/config/mod.rs b/components/chainhook-cli/src/config/mod.rs index cba31806e..80ad2215b 100644 --- a/components/chainhook-cli/src/config/mod.rs +++ b/components/chainhook-cli/src/config/mod.rs @@ -357,7 +357,7 @@ impl Config { (false, true, false, _) => Config::testnet_default(), (false, false, true, _) => Config::mainnet_default(), (false, false, false, Some(config_path)) => Config::from_file_path(config_path)?, - _ => Err("Invalid combination of arguments".to_string())?, + _ => Err("Must include environment flag (for example --devnet)".to_string())?, }; Ok(config) } diff --git a/components/chainhook-cli/src/config/tests/mod.rs b/components/chainhook-cli/src/config/tests/mod.rs index fa48fe655..c63eb2a13 100644 --- a/components/chainhook-cli/src/config/tests/mod.rs +++ b/components/chainhook-cli/src/config/tests/mod.rs @@ -178,5 +178,5 @@ fn it_has_default_config_for_each_network() { let config = Config::default(false, false, false, &Some(path)).unwrap(); assert_eq!(config.network.bitcoin_network, BitcoinNetwork::Regtest); assert_eq!(config.network.stacks_network, StacksNetwork::Devnet); - Config::default(true, true, false, &None).expect_err("expected invalid combination error"); + Config::default(true, true, false, &None).expect_err("expected missing environment flag error"); } diff --git a/components/chainhook-cli/src/service/tests/helpers/mock_stacks_node.rs b/components/chainhook-cli/src/service/tests/helpers/mock_stacks_node.rs index 883853689..c07b65475 100644 --- a/components/chainhook-cli/src/service/tests/helpers/mock_stacks_node.rs +++ b/components/chainhook-cli/src/service/tests/helpers/mock_stacks_node.rs @@ -1,7 +1,9 @@ use crate::scan::stacks::{Record, RecordKind}; use crate::service::tests::helpers::mock_bitcoin_rpc::TipData; use chainhook_sdk::indexer::bitcoin::NewBitcoinBlock; -use chainhook_sdk::indexer::stacks::{NewBlock, NewEvent, NewTransaction, RewardSet, RewardSetSigner}; +use chainhook_sdk::indexer::stacks::{ + NewBlock, NewEvent, NewTransaction, RewardSet, RewardSetSigner, +}; use chainhook_sdk::types::{ FTBurnEventData, FTMintEventData, FTTransferEventData, NFTBurnEventData, NFTMintEventData, NFTTransferEventData, STXBurnEventData, STXLockEventData, STXMintEventData, @@ -87,6 +89,13 @@ fn create_stacks_new_event( } else { None }; + let tenure_change_event = if let StacksTransactionEventPayload::TenureChangeEvent(data) = &event + { + event_type = "tenure_change_event".to_string(); + Some(serde_json::to_value(data).unwrap()) + } else { + None + }; let contract_event = if let StacksTransactionEventPayload::SmartContractEvent(data) = &event { event_type = "smart_contract_print_event".to_string(); Some(serde_json::to_value(data).unwrap()) @@ -112,6 +121,7 @@ fn create_stacks_new_event( data_map_insert_event: None, data_map_update_event: None, data_map_delete_event: None, + tenure_change_event, contract_event, } } @@ -336,12 +346,7 @@ pub async fn mine_stacks_block( .map_err(|e| format!("failed to send new_block request: {}", e))? .text() .await - .map_err(|e| { - format!( - "failed to parse response for new_block request: {}", - e - ) - })?; + .map_err(|e| format!("failed to parse response for new_block request: {}", e))?; Ok(()) } @@ -414,12 +419,7 @@ async fn call_new_burn_block( .map_err(|e| format!("failed to send new_burn_block request: {}", e))? .text() .await - .map_err(|e| { - format!( - "failed to parse response for new_burn_block request: {}", - e - ) - })?; + .map_err(|e| format!("failed to parse response for new_burn_block request: {}", e))?; Ok(()) } diff --git a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs index 1b8e5bfa9..9b402722c 100644 --- a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs @@ -1,11 +1,11 @@ use crate::observer::EventObserverConfig; use crate::utils::{AbstractStacksBlock, Context, MAX_BLOCK_HEIGHTS_ENTRIES}; +use super::types::validate_txid; use super::types::{ append_error_context, BlockIdentifierIndexRule, ChainhookInstance, ExactMatchingRule, HookAction, }; -use super::types::validate_txid; use chainhook_types::{ BlockIdentifier, StacksChainEvent, StacksNetwork, StacksTransactionData, StacksTransactionEvent, StacksTransactionEventPayload, StacksTransactionKind, @@ -258,6 +258,7 @@ pub enum StacksPredicate { FtEvent(StacksFtEventBasedPredicate), NftEvent(StacksNftEventBasedPredicate), StxEvent(StacksStxEventBasedPredicate), + TenureChange(TenureChangeBasedPredicate), Txid(ExactMatchingRule), } @@ -299,6 +300,7 @@ impl StacksPredicate { StacksPredicate::FtEvent(_) => {} StacksPredicate::NftEvent(_) => {} StacksPredicate::StxEvent(_) => {} + StacksPredicate::TenureChange(_) => {} StacksPredicate::Txid(ExactMatchingRule::Equals(txid)) => { if let Err(e) = validate_txid(txid) { return Err(append_error_context( @@ -452,6 +454,12 @@ pub struct StacksStxEventBasedPredicate { pub actions: Vec, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct TenureChangeBasedPredicate { + pub actions: Vec, +} + #[derive(Clone)] pub struct StacksTriggerChainhook<'a> { pub chainhook: &'a StacksChainhookInstance, @@ -484,17 +492,13 @@ pub struct StacksChainhookOccurrencePayload { } impl StacksChainhookOccurrencePayload { - pub fn from_trigger( - trigger: StacksTriggerChainhook<'_>, - ) -> StacksChainhookOccurrencePayload { + pub fn from_trigger(trigger: StacksTriggerChainhook<'_>) -> StacksChainhookOccurrencePayload { StacksChainhookOccurrencePayload { apply: trigger .apply .into_iter() .map(|(transactions, block)| { - let transactions = transactions - .into_iter().cloned() - .collect::>(); + let transactions = transactions.into_iter().cloned().collect::>(); StacksApplyTransactionPayload { block_identifier: block.get_identifier().clone(), transactions, @@ -505,9 +509,7 @@ impl StacksChainhookOccurrencePayload { .rollback .into_iter() .map(|(transactions, block)| { - let transactions = transactions - .into_iter().cloned() - .collect::>(); + let transactions = transactions.into_iter().cloned().collect::>(); StacksRollbackTransactionPayload { block_identifier: block.get_identifier().clone(), transactions, @@ -790,6 +792,7 @@ pub fn evaluate_stacks_predicate_on_block<'a>( | StacksPredicate::FtEvent(_) | StacksPredicate::NftEvent(_) | StacksPredicate::StxEvent(_) + | StacksPredicate::TenureChange(_) | StacksPredicate::PrintEvent(_) | StacksPredicate::Txid(_) => unreachable!(), } @@ -816,7 +819,7 @@ pub fn evaluate_stacks_predicate_on_transaction<'a>( _ => false, }, StacksPredicate::ContractDeployment(StacksContractDeploymentPredicate::ImplementTrait( - stacks_trait, + _stacks_trait, )) => match &transaction.metadata.kind { StacksTransactionKind::ContractDeployment(_actual_deployment) => { ctx.try_log(|logger| { @@ -922,6 +925,18 @@ pub fn evaluate_stacks_predicate_on_transaction<'a>( } false } + StacksPredicate::TenureChange(_expected_event) => { + for event in transaction.metadata.receipt.events.iter() { + if matches!( + &event.event_payload, + StacksTransactionEventPayload::TenureChangeEvent(_) + ) { + return true; + } + } + false + } + StacksPredicate::StxEvent(expected_event) => { let expecting_mint = expected_event.actions.contains(&"mint".to_string()); let expecting_transfer = expected_event.actions.contains(&"transfer".to_string()); @@ -949,7 +964,9 @@ pub fn evaluate_stacks_predicate_on_transaction<'a>( } StacksPredicate::PrintEvent(expected_event) => { for event in transaction.metadata.receipt.events.iter() { - if let StacksTransactionEventPayload::SmartContractEvent(actual) = &event.event_payload { + if let StacksTransactionEventPayload::SmartContractEvent(actual) = + &event.event_payload + { if actual.topic == "print" { match expected_event { StacksPrintEventBasedPredicate::Contains { @@ -1212,6 +1229,17 @@ pub fn serialized_event_with_decoded_clarity_value( "position": event.position }) } + StacksTransactionEventPayload::TenureChangeEvent(_payload) => { + json!({ + "type": "TenureChangeEvent", + // "data": { + // "contract_identifier": payload.contract_identifier, + // "topic": payload.topic, + // "value": serialized_decoded_clarity_value(&payload.tenure_change, ctx), + // }, + "position": event.position + }) + } } } @@ -1235,7 +1263,7 @@ pub fn serialized_decoded_clarity_value(hex_value: &str, ctx: &Context) -> serde Ok(bytes) => bytes, _ => return json!(hex_value.to_string()), }; - + match ClarityValue::consensus_deserialize(&mut Cursor::new(&value_bytes)) { Ok(value) => serialize_to_json(&value), Err(e) => { diff --git a/components/chainhook-sdk/src/indexer/stacks/mod.rs b/components/chainhook-sdk/src/indexer/stacks/mod.rs index 2e54a7992..5237311f1 100644 --- a/components/chainhook-sdk/src/indexer/stacks/mod.rs +++ b/components/chainhook-sdk/src/indexer/stacks/mod.rs @@ -41,10 +41,10 @@ pub struct NewBlock { #[serde(skip_serializing_if = "Option::is_none")] pub block_time: Option, - + #[serde(skip_serializing_if = "Option::is_none")] pub signer_bitvec: Option, - + #[serde(skip_serializing_if = "Option::is_none")] pub signer_signature: Option>, @@ -147,6 +147,7 @@ pub struct NewEvent { pub data_map_insert_event: Option, pub data_map_update_event: Option, pub data_map_delete_event: Option, + pub tenure_change_event: Option, pub contract_event: Option, } @@ -278,6 +279,15 @@ impl NewEvent { index: self.event_index, }, }); + } else if let Some(ref event_data) = self.tenure_change_event { + let data: TenureChangeEventData = + serde_json::from_value(event_data.clone()).expect("Unable to decode event_data"); + return Ok(StacksTransactionEvent { + event_payload: StacksTransactionEventPayload::TenureChangeEvent(data.clone()), + position: StacksTransactionEventPosition { + index: self.event_index, + }, + }); } else if let Some(ref event_data) = self.contract_event { let data: SmartContractEventData = serde_json::from_value(event_data.clone()).expect("Unable to decode event_data"); @@ -387,8 +397,7 @@ pub fn standardize_stacks_block( } return Err(format!( "unable to standardize block #{} ({})", - block.block_height, - e + block.block_height, e )); } }; @@ -629,7 +638,6 @@ pub fn get_value_description(raw_value: &str, ctx: &Context) -> String { _ => return raw_value.to_string(), }; - match ClarityValue::consensus_deserialize(&mut Cursor::new(&value_bytes)) { Ok(value) => format!("{}", value), Err(e) => { @@ -703,12 +711,12 @@ pub fn get_tx_description( if let ClarityValue::Tuple(outter) = *data.data { if let Some(ClarityValue::Tuple(inner)) = outter.data_map.get("data") { if let ( - Some(ClarityValue::Principal(stacking_address)), - Some(ClarityValue::UInt(amount_ustx)), - Some(ClarityValue::Principal(delegate)), - Some(ClarityValue::Optional(pox_addr)), - Some(ClarityValue::Optional(unlock_burn_height)), - ) = ( + Some(ClarityValue::Principal(stacking_address)), + Some(ClarityValue::UInt(amount_ustx)), + Some(ClarityValue::Principal(delegate)), + Some(ClarityValue::Optional(pox_addr)), + Some(ClarityValue::Optional(unlock_burn_height)), + ) = ( &outter.data_map.get("stacker"), &inner.data_map.get("amount-ustx"), &inner.data_map.get("delegate-to"), @@ -728,17 +736,13 @@ pub fn get_tx_description( Some(value) => match &**value { ClarityValue::Tuple(address_comps) => { match ( - &address_comps - .data_map - .get("version"), + &address_comps.data_map.get("version"), &address_comps .data_map .get("hashbytes"), ) { ( - Some(ClarityValue::UInt( - _version, - )), + Some(ClarityValue::UInt(_version)), Some(ClarityValue::Sequence( SequenceData::Buffer( _hashbytes, @@ -763,14 +767,7 @@ pub fn get_tx_description( }, }), ); - return Ok(( - description, - tx_type, - 0, - 0, - "".to_string(), - None, - )); + return Ok((description, tx_type, 0, 0, "".to_string(), None)); } } } @@ -1324,6 +1321,7 @@ pub fn get_standardized_stacks_receipt( StacksTransactionEventPayload::DataMapInsertEvent(_data) => {} StacksTransactionEventPayload::DataMapUpdateEvent(_data) => {} StacksTransactionEventPayload::DataMapDeleteEvent(_data) => {} + StacksTransactionEventPayload::TenureChangeEvent(_data) => {} StacksTransactionEventPayload::SmartContractEvent(data) => { mutated_contracts_radius.insert(data.contract_identifier.clone()); } diff --git a/components/chainhook-sdk/src/indexer/stacks/tests.rs b/components/chainhook-sdk/src/indexer/stacks/tests.rs index 51d4d2a5c..849e08f5c 100644 --- a/components/chainhook-sdk/src/indexer/stacks/tests.rs +++ b/components/chainhook-sdk/src/indexer/stacks/tests.rs @@ -3,6 +3,7 @@ use chainhook_types::{ FTBurnEventData, FTMintEventData, FTTransferEventData, NFTBurnEventData, NFTMintEventData, NFTTransferEventData, STXBurnEventData, STXLockEventData, STXMintEventData, STXTransferEventData, SmartContractEventData, StacksTransactionEventPayload, + TenureChangeEventData, }; use crate::indexer::tests::helpers::stacks_events::create_new_event_from_stacks_event; @@ -213,10 +214,10 @@ fn test_stacks_vector_040() { process_stacks_blocks_and_check_expectations((helpers::stacks_shapes::get_vector_040(), None)); } -// #[test] -// fn test_stacks_vector_041() { -// process_stacks_blocks_and_check_expectations((helpers::shapes::get_vector_041(), None)); -// } +#[test] +fn test_stacks_vector_041() { + process_stacks_blocks_and_check_expectations((helpers::stacks_shapes::get_vector_041(), None)); +} #[test] fn test_stacks_vector_042() { @@ -363,6 +364,15 @@ fn test_stacks_vector_055() { topic: "print".to_string(), hex_value: String::new(), }); "smart_contract_print_event")] +#[test_case(StacksTransactionEventPayload::TenureChangeEvent(TenureChangeEventData { + tenure_consensus_hash: String::new(), + prev_tenure_consensus_hash: String::new(), + burn_view_consensus_hash: String::new(), + previous_tenure_end: String::new(), + previous_tenure_blocks: "9".to_string(), + cause: "block_found".to_string(), + pubkey_hash: String::new(), +}); "tenure_change_event")] fn new_events_can_be_converted_into_chainhook_event(original_event: StacksTransactionEventPayload) { let new_event = create_new_event_from_stacks_event(original_event.clone()); let event = new_event.into_chainhook_event().unwrap(); @@ -392,6 +402,7 @@ fn into_chainhook_event_rejects_invalid_missing_event() { data_map_insert_event: None, data_map_update_event: None, data_map_delete_event: None, + tenure_change_event: None, contract_event: None, }; new_event diff --git a/components/chainhook-sdk/src/indexer/tests/helpers/stacks_events.rs b/components/chainhook-sdk/src/indexer/tests/helpers/stacks_events.rs index 4ca977bc5..cc9ede7c2 100644 --- a/components/chainhook-sdk/src/indexer/tests/helpers/stacks_events.rs +++ b/components/chainhook-sdk/src/indexer/tests/helpers/stacks_events.rs @@ -91,6 +91,13 @@ pub fn create_new_event_from_stacks_event(event: StacksTransactionEventPayload) } else { None }; + let tenure_change_event = if let StacksTransactionEventPayload::TenureChangeEvent(data) = &event + { + event_type = "tenure_change".to_string(); + Some(serde_json::to_value(data).unwrap()) + } else { + None + }; let contract_event = if let StacksTransactionEventPayload::SmartContractEvent(data) = &event { event_type = "smart_contract_print_event".to_string(); Some(serde_json::to_value(data).unwrap()) @@ -116,6 +123,7 @@ pub fn create_new_event_from_stacks_event(event: StacksTransactionEventPayload) data_map_insert_event, data_map_update_event, data_map_delete_event, + tenure_change_event, contract_event, } } diff --git a/components/chainhook-types-rs/src/events.rs b/components/chainhook-types-rs/src/events.rs index 02179b51b..a9a4b6e88 100644 --- a/components/chainhook-types-rs/src/events.rs +++ b/components/chainhook-types-rs/src/events.rs @@ -121,6 +121,17 @@ pub struct SmartContractEventData { pub hex_value: String, } +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct TenureChangeEventData { + pub tenure_consensus_hash: String, + pub prev_tenure_consensus_hash: String, + pub burn_view_consensus_hash: String, + pub previous_tenure_end: String, + pub previous_tenure_blocks: String, + pub cause: String, + pub pubkey_hash: String, +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(tag = "type", content = "data")] pub enum StacksTransactionEventPayload { @@ -139,6 +150,7 @@ pub enum StacksTransactionEventPayload { DataMapUpdateEvent(DataMapUpdateEventData), DataMapDeleteEvent(DataMapDeleteEventData), SmartContractEvent(SmartContractEventData), + TenureChangeEvent(TenureChangeEventData), } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] diff --git a/docs/chainhook-openapi.json b/docs/chainhook-openapi.json index 989f3c230..7d535bfe8 100644 --- a/docs/chainhook-openapi.json +++ b/docs/chainhook-openapi.json @@ -1180,6 +1180,27 @@ } } }, + { + "type": "object", + "required": [ + "actions", + "scope" + ], + "properties": { + "scope": { + "type": "string", + "enum": [ + "tenure_change" + ] + }, + "actions": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, { "type": "object", "oneOf": [