diff --git a/Cargo.lock b/Cargo.lock index 0851dc587..8f87fc1ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "86b8f9420f797f2d9e935edf629310eb938a0d839f984e25327f3c7eed22300c" dependencies = [ "memchr", ] @@ -474,6 +474,7 @@ dependencies = [ "hyper", "lazy_static", "rand 0.8.5", + "regex", "reqwest", "rocket", "schemars 0.8.12", @@ -2220,7 +2221,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -2985,13 +2986,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata 0.3.6", + "regex-syntax 0.7.4", ] [[package]] @@ -3000,7 +3002,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax", + "regex-syntax 0.6.28", +] + +[[package]] +name = "regex-automata" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.4", ] [[package]] @@ -3009,6 +3022,12 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + [[package]] name = "remove_dir_all" version = "0.5.3" diff --git a/components/chainhook-cli/src/cli/mod.rs b/components/chainhook-cli/src/cli/mod.rs index c03199fcf..28eb37783 100644 --- a/components/chainhook-cli/src/cli/mod.rs +++ b/components/chainhook-cli/src/cli/mod.rs @@ -339,7 +339,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { start_block: Some(34239), end_block: Some(50000), blocks: None, - predicate: StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate { + predicate: StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: "ST1SVA0SST0EDT4MFYGWGP6GNSXMMQJDVP1G8QTTC.arkadiko-freddie-v1-1".into(), contains: "vault".into(), }), @@ -355,7 +355,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { start_block: Some(34239), end_block: Some(50000), blocks: None, - predicate: StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate { + predicate: StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: "SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.arkadiko-freddie-v1-1".into(), contains: "vault".into(), }), diff --git a/components/chainhook-cli/src/service/tests/mod.rs b/components/chainhook-cli/src/service/tests/mod.rs index 8fbe52f46..a8d9cfcb4 100644 --- a/components/chainhook-cli/src/service/tests/mod.rs +++ b/components/chainhook-cli/src/service/tests/mod.rs @@ -308,6 +308,7 @@ async fn it_handles_stacks_predicates_with_network(network: &str) { #[test_case(json!({"scope":"print_event","contract_identifier": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09", "contains": "*"}); "with scope print_event wildcard conatins")] #[test_case(json!({"scope":"print_event","contract_identifier": "*", "contains": "vault"}); "with scope print_event wildcard contract_identifier")] #[test_case(json!({"scope":"print_event", "contract_identifier": "*", "contains": "*"}); "with scope print_event wildcard both fields")] +#[test_case(json!({"scope":"print_event", "contract_identifier": "*", "matches_regex": "(some)|(value)"}); "with scope print_event and matching_rule regex")] #[test_case(json!({"scope":"ft_event","asset_identifier": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.cbtc-token::cbtc","actions": ["burn"]}); "with scope ft_event")] #[test_case(json!({"scope":"nft_event","asset_identifier": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.monkey-sip09::monkeys","actions": ["mint", "transfer", "burn"]}); "with scope nft_event")] #[test_case(json!({"scope":"stx_event","actions": ["transfer", "lock"]}); "with scope stx_event")] diff --git a/components/chainhook-sdk/Cargo.toml b/components/chainhook-sdk/Cargo.toml index 58c7d201d..923946362 100644 --- a/components/chainhook-sdk/Cargo.toml +++ b/components/chainhook-sdk/Cargo.toml @@ -41,6 +41,7 @@ zeromq = { version = "0.3.3", default-features = false, features = ["tokio-runti dashmap = "5.4.0" fxhash = "0.2.1" lazy_static = "1.4.0" +regex = "1.9.3" [dev-dependencies] test-case = "3.1.0" diff --git a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs index 680be7e34..2cb5c4585 100644 --- a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs @@ -2,13 +2,14 @@ use crate::utils::{AbstractStacksBlock, Context}; use super::types::{ BlockIdentifierIndexRule, ExactMatchingRule, HookAction, StacksChainhookSpecification, - StacksContractDeploymentPredicate, StacksPredicate, + StacksContractDeploymentPredicate, StacksPredicate, StacksPrintEventBasedPredicate, }; use chainhook_types::{ BlockIdentifier, StacksChainEvent, StacksTransactionData, StacksTransactionEvent, StacksTransactionKind, TransactionIdentifier, }; use hiro_system_kit::slog; +use regex::Regex; use reqwest::{Client, Method}; use serde_json::Value as JsonValue; use stacks_rpc_client::clarity::stacks_common::codec::StacksMessageCodec; @@ -406,16 +407,47 @@ pub fn evaluate_stacks_predicate_on_transaction<'a>( match event { StacksTransactionEvent::SmartContractEvent(actual) => { if actual.topic == "print" { - if expected_event.contract_identifier == actual.contract_identifier - || expected_event.contract_identifier == "*" - { - if expected_event.contains == "*" { - return true; + match expected_event { + StacksPrintEventBasedPredicate::Contains { + contract_identifier, + contains, + } => { + if contract_identifier == &actual.contract_identifier + || contract_identifier == "*" + { + if contains == "*" { + return true; + } + let value = format!( + "{}", + expect_decoded_clarity_value(&actual.hex_value) + ); + if value.contains(contains) { + return true; + } + } } - let value = - format!("{}", expect_decoded_clarity_value(&actual.hex_value)); - if value.contains(&expected_event.contains) { - return true; + StacksPrintEventBasedPredicate::MatchesRegex { + contract_identifier, + regex, + } => { + if contract_identifier == &actual.contract_identifier + || contract_identifier == "*" + { + if let Ok(regex) = Regex::new(regex) { + let value = format!( + "{}", + expect_decoded_clarity_value(&actual.hex_value) + ); + if regex.is_match(&value) { + return true; + } + } else { + ctx.try_log(|logger| { + slog::error!(logger, "unable to parse print_event matching rule as regex") + }); + } + } } } } diff --git a/components/chainhook-sdk/src/chainhooks/tests/mod.rs b/components/chainhook-sdk/src/chainhooks/tests/mod.rs index 82e2cb2b7..a3a458d93 100644 --- a/components/chainhook-sdk/src/chainhooks/tests/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/tests/mod.rs @@ -227,7 +227,7 @@ pub mod fixtures; // PrintEvent predicate tests #[test_case( vec![vec![get_test_event_by_type("smart_contract_print_event")]], - StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate { + StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: "ST3AXH4EBHD63FCFPTZ8GR29TNTVWDYPGY0KDY5E5.loan-data".to_string(), contains: "some-value".to_string() }), @@ -236,7 +236,7 @@ pub mod fixtures; )] #[test_case( vec![vec![get_test_event_by_type("smart_contract_not_print_event")]], - StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate { + StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: "ST3AXH4EBHD63FCFPTZ8GR29TNTVWDYPGY0KDY5E5.loan-data".to_string(), contains: "some-value".to_string(), }), @@ -245,7 +245,7 @@ pub mod fixtures; )] #[test_case( vec![vec![get_test_event_by_type("smart_contract_print_event")]], - StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate { + StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: "wront-id".to_string(), contains: "some-value".to_string(), }), @@ -254,7 +254,7 @@ pub mod fixtures; )] #[test_case( vec![vec![get_test_event_by_type("smart_contract_print_event")]], - StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate { + StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: "ST3AXH4EBHD63FCFPTZ8GR29TNTVWDYPGY0KDY5E5.loan-data".to_string(), contains: "wrong-value".to_string(), @@ -264,7 +264,7 @@ pub mod fixtures; )] #[test_case( vec![vec![get_test_event_by_type("smart_contract_print_event")]], - StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate { + StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: "*".to_string(), contains: "some-value".to_string(), }), @@ -273,7 +273,7 @@ pub mod fixtures; )] #[test_case( vec![vec![get_test_event_by_type("smart_contract_print_event")]], - StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate { + StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: "ST3AXH4EBHD63FCFPTZ8GR29TNTVWDYPGY0KDY5E5.loan-data".to_string(), contains: "*".to_string(), }), @@ -282,13 +282,41 @@ pub mod fixtures; )] #[test_case( vec![vec![get_test_event_by_type("smart_contract_print_event")], vec![get_test_event_by_type("smart_contract_print_event_empty")]], - StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate { + StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::Contains { contract_identifier: "*".to_string(), contains: "*".to_string(), }), 2; "PrintEvent predicate contract_identifier wildcard and contains wildcard matches all values on all print events" )] +#[test_case( + vec![vec![get_test_event_by_type("smart_contract_print_event")]], + StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::MatchesRegex { + contract_identifier: "ST3AXH4EBHD63FCFPTZ8GR29TNTVWDYPGY0KDY5E5.loan-data".to_string(), + regex: "(some)|(value)".to_string(), + }), + 1; + "PrintEvent predicate matches contract_identifier and regex" +)] +#[test_case( + vec![vec![get_test_event_by_type("smart_contract_print_event")]], + StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::MatchesRegex { + contract_identifier: "*".to_string(), + regex: "(some)|(value)".to_string(), + }), + 1; + "PrintEvent predicate contract_identifier wildcard checks all print events for match with regex" +)] +#[test_case( + vec![vec![get_test_event_by_type("smart_contract_print_event")]], + StacksPredicate::PrintEvent(StacksPrintEventBasedPredicate::MatchesRegex { + contract_identifier: "*".to_string(), + regex: "[".to_string(), + }), + 0 + ; + "PrintEvent predicate does not match invalid regex" +)] fn test_stacks_predicates( blocks_with_events: Vec>, predicate: StacksPredicate, diff --git a/components/chainhook-sdk/src/chainhooks/types.rs b/components/chainhook-sdk/src/chainhooks/types.rs index 0a1c179a5..12e903fb3 100644 --- a/components/chainhook-sdk/src/chainhooks/types.rs +++ b/components/chainhook-sdk/src/chainhooks/types.rs @@ -776,9 +776,17 @@ pub enum StacksTrait { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct StacksPrintEventBasedPredicate { - pub contract_identifier: String, - pub contains: String, +#[serde(untagged)] +pub enum StacksPrintEventBasedPredicate { + Contains { + contract_identifier: String, + contains: String, + }, + MatchesRegex { + contract_identifier: String, + #[serde(rename = "matches_regex")] + regex: String, + }, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] diff --git a/docs/chainhook-openapi.json b/docs/chainhook-openapi.json index d09ef8f19..8f0c72e9d 100644 --- a/docs/chainhook-openapi.json +++ b/docs/chainhook-openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "chainhook", - "version": "0.17.0" + "version": "1.0.0" }, "paths": { "/ping": { @@ -997,9 +997,39 @@ }, { "type": "object", + "anyOf": [ + { + "type": "object", + "required": [ + "contains", + "contract_identifier" + ], + "properties": { + "contract_identifier": { + "type": "string" + }, + "contains": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "contract_identifier", + "matches_regex" + ], + "properties": { + "contract_identifier": { + "type": "string" + }, + "matches_regex": { + "type": "string" + } + } + } + ], "required": [ - "contains", - "contract_identifier", "scope" ], "properties": { @@ -1008,12 +1038,6 @@ "enum": [ "print_event" ] - }, - "contract_identifier": { - "type": "string" - }, - "contains": { - "type": "string" } } },