Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow matching with regex for stacks print_event #380

Merged
merged 3 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions components/chainhook-cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}),
Expand All @@ -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(),
}),
Expand Down
1 change: 1 addition & 0 deletions components/chainhook-cli/src/service/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
1 change: 1 addition & 0 deletions components/chainhook-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
52 changes: 42 additions & 10 deletions components/chainhook-sdk/src/chainhooks/stacks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")
});
}
}
}
}
}
Expand Down
42 changes: 35 additions & 7 deletions components/chainhook-sdk/src/chainhooks/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}),
Expand All @@ -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(),
}),
Expand All @@ -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(),
}),
Expand All @@ -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(),
Expand All @@ -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(),
}),
Expand All @@ -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(),
}),
Expand All @@ -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<Vec<StacksTransactionEvent>>,
predicate: StacksPredicate,
Expand Down
14 changes: 11 additions & 3 deletions components/chainhook-sdk/src/chainhooks/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't like that we have to duplicate the definition of contract_identifier here. If anyone knows a way around this, that would be sick. That's what lead me down the path of the breaking change PR (#379), but I guess it's probably worth it to not break the API so soon after releasing v1.

Copy link
Contributor

@lgalabru lgalabru Aug 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. After toying with serde, it does not look like something supported - which sounds reasonable, as this could lead to one json payload resulting to multiple different struct.
If we really wanted to avoid the repetition, we could move to something like this:

struct Predicate {
   contract_identifier: T,
   contains: Option<U>,
   matches: Option<V>
}

and associate a method to that struct to make it more usable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that is an option.

In this case I guess we could treat this as an "OR", so we match any that contain or regex match if both values are provided. I think it maybe sets a weird precedence for how we do this in the future - it would feel clunky to have a predicate type with many Options that are only really intended to be used alone, but the user can specify all of them. And if we ever need to make them mutually exclusive (you can have fieldA OR fieldB specified in the predicate, but not both), we wouldn't be able to represent this in an openapi spec.

All of that makes me lean toward the current solution, but no particularly strongly. Do you have a preference, @lgalabru?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of that makes me lean toward the current solution

Agreed!

Contains {
contract_identifier: String,
contains: String,
},
MatchesRegex {
contract_identifier: String,
#[serde(rename = "matches_regex")]
regex: String,
},
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
Expand Down
42 changes: 33 additions & 9 deletions docs/chainhook-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"openapi": "3.0.0",
"info": {
"title": "chainhook",
"version": "0.17.0"
"version": "1.0.0"
},
"paths": {
"/ping": {
Expand Down Expand Up @@ -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": {
Expand All @@ -1008,12 +1038,6 @@
"enum": [
"print_event"
]
},
"contract_identifier": {
"type": "string"
},
"contains": {
"type": "string"
}
}
},
Expand Down