From 3c347a23b4f0edb5fc8944f7707b8201b8f09418 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Mon, 1 Jul 2024 11:23:58 -0400 Subject: [PATCH] feat: improve chainhook-sdk interface (#608) ### Description The goal of this PR is to make it much easier to use the Chainhook SDK. Previously, there were many fields that are rarely needed for the average user which had to be set when configuring the SDK. Many of these fields had confusing names that made it difficult to know how they were used in the SDK. Additionally, many of these fields were only needed for observing stacks events, but bitcoin-only users had to specify them anyways. This has a few major changes to the Chainhook SDK: - Removing some unused fields from the event observer config (`cache_path`, `data_handler_tx`, and`ingestion_port`) (694fb4dfd6dd1e066ebaddac41e2324ebd2d3c41) - Renames `display_logs` -> `display_stacks_ingestion_logs` (694fb4dfd6dd1e066ebaddac41e2324ebd2d3c41) - Renames `EventObserverConfigOverrides` -> `StacksEventObserverConfigBuilder` (9da117847fb028e80c722b58678aa6291f68f649) - Renames `ingestion_port` -> `chainhook_stacks_block_ingestion_port` for `StacksEventObserverConfigBuilder` (4e997fc0225e51b1876a4182d2c73b76d2e48dfa) - Adds a `BitcoinEventObserverConfigBuilder` (fc67dff52da16ecb0c3da287afe732aa06f22a21) - Renames some very confusingly named structs (5a4cb39c7a294b3a5768d8ee9378ebe2bfbe578e): - `*ChainhookFullSpecification` => `*ChainhookSpecificationNetworkMap` - `*ChainhookNetworkSpecification` => `*ChainhookSpecification` - `*ChainhookSpecification` => `*ChainhookInstance` - refactor: moves stacks/bitcoin specific types to their respective types folder (83e833626950a8097aa14e54115ba6db6591d268) - adds helpers for registering chainhooks (4debc28103762f94c5eef9cb15cdfedd6d0c32ed) - renames `ChainhookConfig` -> `ChainhookStore` (c54b6e7f171f09a3a058b397b4131fcf8d9dca7e) - add `EventObserverBuilder` to make a clean interface for starting an event observer (fe04dd9aca396eb939c50748b8824849ea05fb8a) - add a bunch of rsdoc comments with examples #### Breaking change? This will break some aspects of the Chainhook SDK. It should be a simple upgrade: - If you're building any of the above structs directly, rename the fields accordingly - If you're using `::new(..)` to build any of the above structs with fields that are removed, you may need to remove some fields - You can probably remove a good bit of code by using the builders ### Example New code example to start a bitcoin event observer: ```rust fn start_observer(ctx: &Context) -> Result<(), String> { let json_predicate = std::fs::read_to_string("./predicate.json").expect("Unable to read file"); let hook_instance: BitcoinChainhookInstance = serde_json::from_str(&json_predicate).expect("unable to parse chainhook spec"); let config = BitcoinEventObserverConfigBuilder::new() .rpc_username("regtest") .rpc_password("test1235") .rpc_url("http://0.0.0.0:8332") .finish()? .register_bitcoin_chainhook_instance(hook_instance)? .to_owned(); let (observer_commands_tx, observer_commands_rx) = channel(); EventObserverBuilder::new(config, &observer_commands_tx, observer_commands_rx, &ctx) .start() .map_err(|e| format!("event observer error: {}", e.to_string())) } ```
Previous usage of starting a bitcoin observer ```rust let json_predicate = std::fs::read_to_string("./predicate.json").expect("Unable to read file"); let hook_spec: BitcoinChainhookFullSpecification = serde_json::from_str(&json_predicate).expect("unable to parse chainhook spec"); let bitcoin_network = BitcoinNetwork::Regtest; let stacks_network = chainhook_sdk::types::StacksNetwork::Mainnet; let mut bitcoin_hook_spec = hook_spec .into_selected_network_specification(&bitcoin_network) .expect("unable to parse bitcoin spec"); bitcoin_hook_spec.enabled = true; let mut chainhook_config = ChainhookConfig::new(); chainhook_config .register_specification(ChainhookSpecification::Bitcoin(bitcoin_hook_spec)) .expect("failed to register chainhook spec"); let config = EventObserverConfig { chainhook_config: Some(chainhook_config), bitcoin_rpc_proxy_enabled: false, ingestion_port: 0, bitcoind_rpc_username: "regtest".to_string(), bitcoind_rpc_password: "test1235".to_string(), bitcoind_rpc_url: "http://0.0.0.0:8332".to_string(), bitcoin_block_signaling: BitcoinBlockSignaling::ZeroMQ("tcp://0.0.0.0:18543".to_string()), display_logs: true, cache_path: String::new(), bitcoin_network: bitcoin_network, stacks_network: stacks_network, data_handler_tx: None, prometheus_monitoring_port: None, }; let (observer_commands_tx, observer_commands_rx) = channel(); // set up context to configure how we display logs from the event observer let logger = hiro_system_kit::log::setup_logger(); let _guard = hiro_system_kit::log::setup_global_logger(logger.clone()); let ctx = chainhook_sdk::utils::Context { logger: Some(logger), tracer: false, }; let moved_ctx = ctx.clone(); let _ = hiro_system_kit::thread_named("Chainhook event observer") .spawn(move || { let future = start_bitcoin_event_observer( config, observer_commands_tx, observer_commands_rx, None, None, moved_ctx, ); match hiro_system_kit::nestable_block_on(future) { Ok(_) => {} Err(e) => { println!("{}", e) } } }) .expect("unable to spawn thread"); ```
Fixes #598 --- components/chainhook-cli/src/cli/mod.rs | 79 +-- components/chainhook-cli/src/config/mod.rs | 8 +- components/chainhook-cli/src/scan/bitcoin.rs | 6 +- components/chainhook-cli/src/scan/stacks.rs | 10 +- .../chainhook-cli/src/service/http_api.rs | 24 +- components/chainhook-cli/src/service/mod.rs | 34 +- .../chainhook-cli/src/service/runloops.rs | 13 +- .../src/service/tests/helpers/mock_service.rs | 15 +- .../chainhook-cli/src/service/tests/mod.rs | 6 +- .../src/service/tests/observer_tests.rs | 10 +- .../src/service/tests/runloop_tests.rs | 11 +- .../src/chainhooks/bitcoin/mod.rs | 348 ++++++++++- .../src/chainhooks/bitcoin/tests.rs | 6 +- .../src/chainhooks/stacks/mod.rs | 234 +++++++- .../chainhook-sdk/src/chainhooks/tests/mod.rs | 28 +- .../chainhook-sdk/src/chainhooks/types.rs | 559 ++---------------- .../chainhook-sdk/src/indexer/bitcoin/mod.rs | 8 +- components/chainhook-sdk/src/observer/mod.rs | 538 +++++++++++++---- .../chainhook-sdk/src/observer/tests/mod.rs | 97 +-- components/chainhook-types-rs/src/bitcoin.rs | 2 +- components/chainhook-types-rs/src/rosetta.rs | 4 +- docs/chainhook-openapi.json | 26 +- 22 files changed, 1213 insertions(+), 853 deletions(-) diff --git a/components/chainhook-cli/src/cli/mod.rs b/components/chainhook-cli/src/cli/mod.rs index 387b6dac8..6859d8fa7 100644 --- a/components/chainhook-cli/src/cli/mod.rs +++ b/components/chainhook-cli/src/cli/mod.rs @@ -14,13 +14,16 @@ use crate::storage::{ is_stacks_block_present, open_readonly_stacks_db_conn, open_readwrite_stacks_db_conn, set_last_confirmed_insert_key, }; - -use chainhook_sdk::chainhooks::types::{ - BitcoinChainhookFullSpecification, BitcoinChainhookNetworkSpecification, BitcoinPredicateType, - ChainhookFullSpecification, FileHook, HookAction, InscriptionFeedData, OrdinalOperations, - StacksChainhookFullSpecification, StacksChainhookNetworkSpecification, StacksPredicate, - StacksPrintEventBasedPredicate, -}; +use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookSpecification; +use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookSpecificationNetworkMap; +use chainhook_sdk::chainhooks::bitcoin::BitcoinPredicateType; +use chainhook_sdk::chainhooks::bitcoin::InscriptionFeedData; +use chainhook_sdk::chainhooks::bitcoin::OrdinalOperations; +use chainhook_sdk::chainhooks::stacks::StacksChainhookSpecification; +use chainhook_sdk::chainhooks::stacks::StacksChainhookSpecificationNetworkMap; +use chainhook_sdk::chainhooks::stacks::StacksPredicate; +use chainhook_sdk::chainhooks::stacks::StacksPrintEventBasedPredicate; +use chainhook_sdk::chainhooks::types::{ChainhookSpecificationNetworkMap, FileHook, HookAction}; use chainhook_sdk::types::{BitcoinNetwork, BlockIdentifier, StacksNetwork}; use chainhook_sdk::utils::{BlockHeights, Context}; use clap::{Parser, Subcommand}; @@ -351,7 +354,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { .predicates_paths .iter() .map(|p| load_predicate_from_path(p)) - .collect::, _>>()?; + .collect::, _>>()?; info!(ctx.expect_logger(), "Starting service...",); @@ -384,7 +387,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { (true, false) => { let mut networks = BTreeMap::new(); - networks.insert(StacksNetwork::Testnet, StacksChainhookNetworkSpecification { + networks.insert(StacksNetwork::Testnet, StacksChainhookSpecification { start_block: Some(34239), end_block: Some(50000), blocks: None, @@ -401,7 +404,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { }) }); - networks.insert(StacksNetwork::Mainnet, StacksChainhookNetworkSpecification { + networks.insert(StacksNetwork::Mainnet, StacksChainhookSpecification { start_block: Some(34239), end_block: Some(50000), blocks: None, @@ -418,20 +421,22 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { }) }); - ChainhookFullSpecification::Stacks(StacksChainhookFullSpecification { - uuid: id.to_string(), - owner_uuid: None, - name: "Hello world".into(), - version: 1, - networks, - }) + ChainhookSpecificationNetworkMap::Stacks( + StacksChainhookSpecificationNetworkMap { + uuid: id.to_string(), + owner_uuid: None, + name: "Hello world".into(), + version: 1, + networks, + }, + ) } (false, true) => { let mut networks = BTreeMap::new(); networks.insert( BitcoinNetwork::Mainnet, - BitcoinChainhookNetworkSpecification { + BitcoinChainhookSpecification { start_block: Some(767430), end_block: Some(767430), blocks: None, @@ -451,13 +456,15 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { }, ); - ChainhookFullSpecification::Bitcoin(BitcoinChainhookFullSpecification { - uuid: id.to_string(), - owner_uuid: None, - name: "Hello world".into(), - version: 1, - networks, - }) + ChainhookSpecificationNetworkMap::Bitcoin( + BitcoinChainhookSpecificationNetworkMap { + uuid: id.to_string(), + owner_uuid: None, + name: "Hello world".into(), + version: 1, + networks, + }, + ) } _ => { return Err("command `predicates new` should either provide the flag --stacks or --bitcoin".into()); @@ -500,9 +507,9 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { Config::default(false, cmd.testnet, cmd.mainnet, &cmd.config_path)?; let predicate = load_predicate_from_path(&cmd.predicate_path)?; match predicate { - ChainhookFullSpecification::Bitcoin(predicate) => { + ChainhookSpecificationNetworkMap::Bitcoin(predicate) => { let predicate_spec = match predicate - .into_selected_network_specification(&config.network.bitcoin_network) + .into_specification_for_network(&config.network.bitcoin_network) { Ok(predicate) => predicate, Err(e) => { @@ -522,9 +529,9 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { ) .await?; } - ChainhookFullSpecification::Stacks(predicate) => { + ChainhookSpecificationNetworkMap::Stacks(predicate) => { let predicate_spec = match predicate - .into_selected_network_specification(&config.network.stacks_network) + .into_specification_for_network(&config.network.stacks_network) { Ok(predicate) => predicate, Err(e) => { @@ -569,13 +576,13 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { } PredicatesCommand::Check(cmd) => { let config = Config::default(false, cmd.testnet, cmd.mainnet, &cmd.config_path)?; - let predicate: ChainhookFullSpecification = + let predicate: ChainhookSpecificationNetworkMap = load_predicate_from_path(&cmd.predicate_path)?; match predicate { - ChainhookFullSpecification::Bitcoin(predicate) => { + ChainhookSpecificationNetworkMap::Bitcoin(predicate) => { let _ = match predicate - .into_selected_network_specification(&config.network.bitcoin_network) + .into_specification_for_network(&config.network.bitcoin_network) { Ok(predicate) => predicate, Err(e) => { @@ -586,9 +593,9 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { } }; } - ChainhookFullSpecification::Stacks(predicate) => { + ChainhookSpecificationNetworkMap::Stacks(predicate) => { let _ = match predicate - .into_selected_network_specification(&config.network.stacks_network) + .into_specification_for_network(&config.network.stacks_network) { Ok(predicate) => predicate, Err(e) => { @@ -866,7 +873,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { pub fn load_predicate_from_path( predicate_path: &str, -) -> Result { +) -> Result { let file = std::fs::File::open(&predicate_path) .map_err(|e| format!("unable to read file {}\n{:?}", predicate_path, e))?; let mut file_reader = BufReader::new(file); @@ -874,7 +881,7 @@ pub fn load_predicate_from_path( file_reader .read_to_end(&mut file_buffer) .map_err(|e| format!("unable to read file {}\n{:?}", predicate_path, e))?; - let predicate: ChainhookFullSpecification = serde_json::from_slice(&file_buffer) + let predicate: ChainhookSpecificationNetworkMap = serde_json::from_slice(&file_buffer) .map_err(|e| format!("unable to parse json file {}\n{:?}", predicate_path, e))?; Ok(predicate) } diff --git a/components/chainhook-cli/src/config/mod.rs b/components/chainhook-cli/src/config/mod.rs index 812d76021..7c3c1f8eb 100644 --- a/components/chainhook-cli/src/config/mod.rs +++ b/components/chainhook-cli/src/config/mod.rs @@ -1,6 +1,7 @@ pub mod file; pub mod generator; +use chainhook_sdk::chainhooks::types::ChainhookStore; pub use chainhook_sdk::indexer::IndexerConfig; use chainhook_sdk::observer::EventObserverConfig; use chainhook_sdk::types::{ @@ -114,17 +115,14 @@ impl Config { pub fn get_event_observer_config(&self) -> EventObserverConfig { EventObserverConfig { bitcoin_rpc_proxy_enabled: true, - chainhook_config: None, - ingestion_port: DEFAULT_INGESTION_PORT, + registered_chainhooks: ChainhookStore::new(), bitcoind_rpc_username: self.network.bitcoind_rpc_username.clone(), bitcoind_rpc_password: self.network.bitcoind_rpc_password.clone(), bitcoind_rpc_url: self.network.bitcoind_rpc_url.clone(), bitcoin_block_signaling: self.network.bitcoin_block_signaling.clone(), - display_logs: false, - cache_path: self.storage.working_dir.clone(), + display_stacks_ingestion_logs: false, bitcoin_network: self.network.bitcoin_network.clone(), stacks_network: self.network.stacks_network.clone(), - data_handler_tx: None, prometheus_monitoring_port: self.monitoring.prometheus_monitoring_port, } } diff --git a/components/chainhook-cli/src/scan/bitcoin.rs b/components/chainhook-cli/src/scan/bitcoin.rs index 0f80e7f54..3437af311 100644 --- a/components/chainhook-cli/src/scan/bitcoin.rs +++ b/components/chainhook-cli/src/scan/bitcoin.rs @@ -10,7 +10,7 @@ use chainhook_sdk::chainhooks::bitcoin::{ evaluate_bitcoin_chainhooks_on_chain_event, handle_bitcoin_hook_action, BitcoinChainhookOccurrence, BitcoinTriggerChainhook, }; -use chainhook_sdk::chainhooks::types::BitcoinChainhookSpecification; +use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookInstance; use chainhook_sdk::indexer; use chainhook_sdk::indexer::bitcoin::{ build_http_client, download_and_parse_block_with_retry, retrieve_block_hash_with_retry, @@ -27,7 +27,7 @@ use std::sync::{Arc, RwLock}; use super::common::PredicateScanResult; pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate( - predicate_spec: &BitcoinChainhookSpecification, + predicate_spec: &BitcoinChainhookInstance, unfinished_scan_data: Option, config: &Config, kill_signal: Option>>, @@ -265,7 +265,7 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate( pub async fn process_block_with_predicates( block: BitcoinBlockData, - predicates: &Vec<&BitcoinChainhookSpecification>, + predicates: &Vec<&BitcoinChainhookInstance>, event_observer_config: &EventObserverConfig, ctx: &Context, ) -> Result { diff --git a/components/chainhook-cli/src/scan/stacks.rs b/components/chainhook-cli/src/scan/stacks.rs index 5212af0ec..55e44df9a 100644 --- a/components/chainhook-cli/src/scan/stacks.rs +++ b/components/chainhook-cli/src/scan/stacks.rs @@ -24,9 +24,9 @@ use chainhook_sdk::{ utils::Context, }; use chainhook_sdk::{ - chainhooks::{ - stacks::{handle_stacks_hook_action, StacksChainhookOccurrence, StacksTriggerChainhook}, - types::StacksChainhookSpecification, + chainhooks::stacks::{ + handle_stacks_hook_action, StacksChainhookInstance, StacksChainhookOccurrence, + StacksTriggerChainhook, }, utils::{file_append, send_request, AbstractStacksBlock}, }; @@ -171,7 +171,7 @@ pub async fn get_canonical_fork_from_tsv( } pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( - predicate_spec: &StacksChainhookSpecification, + predicate_spec: &StacksChainhookInstance, unfinished_scan_data: Option, stacks_db_conn: &DB, config: &Config, @@ -441,7 +441,7 @@ pub async fn scan_stacks_chainstate_via_rocksdb_using_predicate( } pub async fn scan_stacks_chainstate_via_csv_using_predicate( - predicate_spec: &StacksChainhookSpecification, + predicate_spec: &StacksChainhookInstance, config: &mut Config, ctx: &Context, ) -> Result { diff --git a/components/chainhook-cli/src/service/http_api.rs b/components/chainhook-cli/src/service/http_api.rs index 460b2f9a1..b3519d421 100644 --- a/components/chainhook-cli/src/service/http_api.rs +++ b/components/chainhook-cli/src/service/http_api.rs @@ -5,7 +5,7 @@ use std::{ }; use chainhook_sdk::{ - chainhooks::types::{ChainhookFullSpecification, ChainhookSpecification}, + chainhooks::types::{ChainhookSpecificationNetworkMap, ChainhookInstance}, observer::ObserverCommand, utils::Context, }; @@ -120,7 +120,7 @@ fn handle_get_predicates( #[openapi(tag = "Managing Predicates")] #[post("/v1/chainhooks", format = "application/json", data = "")] fn handle_create_predicate( - predicate: Result, rocket::serde::json::Error>, + predicate: Result, rocket::serde::json::Error>, api_config: &State, background_job_tx: &State>>>, ctx: &State, @@ -149,7 +149,7 @@ fn handle_create_predicate( if let Ok(mut predicates_db_conn) = open_readwrite_predicates_db_conn(api_config) { match get_entry_from_predicates_db( - &ChainhookSpecification::either_stx_or_btc_key(&predicate_uuid), + &ChainhookInstance::either_stx_or_btc_key(&predicate_uuid), &mut predicates_db_conn, &ctx, ) { @@ -195,7 +195,7 @@ fn handle_get_predicate( match open_readwrite_predicates_db_conn(api_config) { Ok(mut predicates_db_conn) => { let (predicate, status) = match get_entry_from_predicates_db( - &ChainhookSpecification::either_stx_or_btc_key(&predicate_uuid), + &ChainhookInstance::either_stx_or_btc_key(&predicate_uuid), &mut predicates_db_conn, &ctx, ) { @@ -281,7 +281,7 @@ pub fn get_entry_from_predicates_db( predicate_key: &str, predicate_db_conn: &mut Connection, _ctx: &Context, -) -> Result, String> { +) -> Result, String> { let entry: HashMap = predicate_db_conn.hgetall(predicate_key).map_err(|e| { format!( "unable to load chainhook associated with key {}: {}", @@ -295,7 +295,7 @@ pub fn get_entry_from_predicates_db( Some(payload) => payload, }; - let spec = ChainhookSpecification::deserialize_specification(&encoded_spec)?; + let spec = ChainhookInstance::deserialize_specification(&encoded_spec)?; let encoded_status = match entry.get("status") { None => Err(format!( @@ -313,9 +313,9 @@ pub fn get_entry_from_predicates_db( pub fn get_entries_from_predicates_db( predicate_db_conn: &mut Connection, ctx: &Context, -) -> Result, String> { +) -> Result, String> { let chainhooks_to_load: Vec = predicate_db_conn - .scan_match(ChainhookSpecification::either_stx_or_btc_key("*")) + .scan_match(ChainhookInstance::either_stx_or_btc_key("*")) .map_err(|e| format!("unable to connect to redis: {}", e.to_string()))? .into_iter() .collect(); @@ -349,7 +349,7 @@ pub fn get_entries_from_predicates_db( pub fn load_predicates_from_redis( config: &crate::config::Config, ctx: &Context, -) -> Result, String> { +) -> Result, String> { let redis_uri: &str = config.expected_api_database_uri(); let client = redis::Client::open(redis_uri) .map_err(|e| format!("unable to connect to redis: {}", e.to_string()))?; @@ -378,11 +378,11 @@ pub fn get_routes_spec() -> (Vec, OpenApi) { } fn serialized_predicate_with_status( - predicate: &ChainhookSpecification, + predicate: &ChainhookInstance, status: &PredicateStatus, ) -> JsonValue { match (predicate, status) { - (ChainhookSpecification::Stacks(spec), status) => json!({ + (ChainhookInstance::Stacks(spec), status) => json!({ "chain": "stacks", "uuid": spec.uuid, "network": spec.network, @@ -390,7 +390,7 @@ fn serialized_predicate_with_status( "status": status, "enabled": spec.enabled, }), - (ChainhookSpecification::Bitcoin(spec), status) => json!({ + (ChainhookInstance::Bitcoin(spec), status) => json!({ "chain": "bitcoin", "uuid": spec.uuid, "network": spec.network, diff --git a/components/chainhook-cli/src/service/mod.rs b/components/chainhook-cli/src/service/mod.rs index e62f76bb8..7019b4e2e 100644 --- a/components/chainhook-cli/src/service/mod.rs +++ b/components/chainhook-cli/src/service/mod.rs @@ -11,9 +11,9 @@ use crate::storage::{ open_readwrite_stacks_db_conn, }; -use chainhook_sdk::chainhooks::types::{ChainhookConfig, ChainhookFullSpecification}; +use chainhook_sdk::chainhooks::types::{ChainhookSpecificationNetworkMap, ChainhookStore}; -use chainhook_sdk::chainhooks::types::ChainhookSpecification; +use chainhook_sdk::chainhooks::types::ChainhookInstance; use chainhook_sdk::observer::{ start_event_observer, HookExpirationData, ObserverCommand, ObserverEvent, PredicateDeregisteredEvent, PredicateEvaluationReport, PredicateInterruptedData, @@ -41,10 +41,10 @@ impl Service { pub async fn run( &mut self, - predicates_from_startup: Vec, + predicates_from_startup: Vec, observer_commands_tx_rx: Option<(Sender, Receiver)>, ) -> Result<(), String> { - let mut chainhook_config = ChainhookConfig::new(); + let mut chainhook_store = ChainhookStore::new(); // store all predicates from Redis that were in the process of scanning when // chainhook was shutdown - we need to resume where we left off @@ -89,7 +89,7 @@ impl Service { continue; } } - match chainhook_config.register_specification(predicate) { + match chainhook_store.register_instance(predicate) { Ok(_) => { debug!( self.ctx.expect_logger(), @@ -115,7 +115,7 @@ impl Service { if let Ok(mut predicates_db_conn) = open_readwrite_predicates_db_conn(api_config) { let uuid = predicate.get_uuid(); match get_entry_from_predicates_db( - &ChainhookSpecification::either_stx_or_btc_key(&uuid), + &ChainhookInstance::either_stx_or_btc_key(&uuid), &mut predicates_db_conn, &self.ctx, ) { @@ -130,7 +130,7 @@ impl Service { } }; } - match chainhook_config.register_full_specification( + match chainhook_store.register_instance_from_network_map( ( &self.config.network.bitcoin_network, &self.config.network.stacks_network, @@ -161,7 +161,7 @@ impl Service { // let (ordinal_indexer_command_tx, ordinal_indexer_command_rx) = channel(); let mut event_observer_config = self.config.get_event_observer_config(); - event_observer_config.chainhook_config = Some(chainhook_config); + event_observer_config.registered_chainhooks = chainhook_store; // Download and ingest a Stacks dump if self.config.rely_on_remote_stacks_tsv() { @@ -305,13 +305,13 @@ impl Service { for predicate_with_last_scanned_block in leftover_scans { match predicate_with_last_scanned_block { - (ChainhookSpecification::Stacks(spec), last_scanned_block) => { + (ChainhookInstance::Stacks(spec), last_scanned_block) => { let _ = stacks_scan_op_tx.send(StacksScanOp::StartScan { predicate_spec: spec, unfinished_scan_data: last_scanned_block, }); } - (ChainhookSpecification::Bitcoin(spec), last_scanned_block) => { + (ChainhookInstance::Bitcoin(spec), last_scanned_block) => { let _ = bitcoin_scan_op_tx.send(BitcoinScanOp::StartScan { predicate_spec: spec, unfinished_scan_data: last_scanned_block, @@ -361,13 +361,13 @@ impl Service { ); } match spec { - ChainhookSpecification::Stacks(predicate_spec) => { + ChainhookInstance::Stacks(predicate_spec) => { let _ = stacks_scan_op_tx.send(StacksScanOp::StartScan { predicate_spec, unfinished_scan_data: None, }); } - ChainhookSpecification::Bitcoin(predicate_spec) => { + ChainhookInstance::Bitcoin(predicate_spec) => { let _ = bitcoin_scan_op_tx.send(BitcoinScanOp::StartScan { predicate_spec, unfinished_scan_data: None, @@ -419,7 +419,7 @@ impl Service { }; let predicate_key = - ChainhookSpecification::either_stx_or_btc_key(&predicate_uuid); + ChainhookInstance::either_stx_or_btc_key(&predicate_uuid); let res: Result<(), redis::RedisError> = predicates_db_conn.del(predicate_key.clone()); if let Err(e) = res { @@ -755,7 +755,7 @@ fn update_status_from_report( last_triggered_height, triggered_count, }, - &(ChainhookSpecification::either_stx_or_btc_key(predicate_uuid)), + &(ChainhookInstance::either_stx_or_btc_key(predicate_uuid)), predicates_db_conn, &ctx, ); @@ -784,7 +784,7 @@ fn update_status_from_report( last_evaluated_height, evaluated_count, }, - &(ChainhookSpecification::either_stx_or_btc_key(predicate_uuid)), + &(ChainhookInstance::either_stx_or_btc_key(predicate_uuid)), predicates_db_conn, &ctx, ); @@ -797,7 +797,7 @@ fn update_status_from_report( &chain, evaluated_count, last_evaluated_height, - &(ChainhookSpecification::either_stx_or_btc_key(predicate_uuid)), + &(ChainhookInstance::either_stx_or_btc_key(predicate_uuid)), predicates_db_conn, &ctx, ); @@ -1236,7 +1236,7 @@ pub fn update_predicate_status( fn update_predicate_spec( predicate_key: &str, - spec: &ChainhookSpecification, + spec: &ChainhookInstance, predicates_db_conn: &mut Connection, ctx: &Context, ) { diff --git a/components/chainhook-cli/src/service/runloops.rs b/components/chainhook-cli/src/service/runloops.rs index c2f28f394..2322c1545 100644 --- a/components/chainhook-cli/src/service/runloops.rs +++ b/components/chainhook-cli/src/service/runloops.rs @@ -4,8 +4,9 @@ use std::{ }; use chainhook_sdk::{ - chainhooks::types::{ - BitcoinChainhookSpecification, ChainhookSpecification, StacksChainhookSpecification, + chainhooks::{ + bitcoin::BitcoinChainhookInstance, stacks::StacksChainhookInstance, + types::ChainhookInstance, }, observer::ObserverCommand, utils::Context, @@ -26,7 +27,7 @@ use super::ScanningData; pub enum StacksScanOp { StartScan { - predicate_spec: StacksChainhookSpecification, + predicate_spec: StacksChainhookInstance, unfinished_scan_data: Option, }, KillScan(String), @@ -85,7 +86,7 @@ pub fn start_stacks_scan_runloop( | Ok(PredicateScanResult::Deregistered) => {} Ok(PredicateScanResult::ChainTipReached) => { let _ = observer_command_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Stacks(predicate_spec), + ChainhookInstance::Stacks(predicate_spec), )); } Err(e) => { @@ -130,7 +131,7 @@ pub fn start_stacks_scan_runloop( pub enum BitcoinScanOp { StartScan { - predicate_spec: BitcoinChainhookSpecification, + predicate_spec: BitcoinChainhookInstance, unfinished_scan_data: Option, }, KillScan(String), @@ -171,7 +172,7 @@ pub fn start_bitcoin_scan_runloop( | Ok(PredicateScanResult::Deregistered) => {} Ok(PredicateScanResult::ChainTipReached) => { let _ = observer_command_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Bitcoin(predicate_spec), + ChainhookInstance::Bitcoin(predicate_spec), )); } Err(e) => { diff --git a/components/chainhook-cli/src/service/tests/helpers/mock_service.rs b/components/chainhook-cli/src/service/tests/helpers/mock_service.rs index 55ae3e859..7e50e59b8 100644 --- a/components/chainhook-cli/src/service/tests/helpers/mock_service.rs +++ b/components/chainhook-cli/src/service/tests/helpers/mock_service.rs @@ -8,9 +8,8 @@ use crate::service::{ PredicateStatus, Service, }; use chainhook_sdk::{ - chainhooks::types::{ - ChainhookFullSpecification, ChainhookSpecification, StacksChainhookFullSpecification, - }, + chainhooks::stacks::StacksChainhookSpecificationNetworkMap, + chainhooks::types::{ChainhookInstance, ChainhookSpecificationNetworkMap}, indexer::IndexerConfig, observer::ObserverCommand, types::{BitcoinBlockSignaling, BitcoinNetwork, Chain, StacksNetwork, StacksNodeConfig}, @@ -333,7 +332,7 @@ pub fn get_chainhook_config( pub async fn start_chainhook_service( config: Config, ping_startup_port: u16, - startup_predicates: Option>, + startup_predicates: Option>, ctx: &Context, ) -> Result, String> { let mut service = Service::new(config, ctx.clone()); @@ -389,8 +388,8 @@ pub struct TestSetupResult { pub async fn setup_stacks_chainhook_test( starting_chain_tip: u64, - redis_seed: Option<(StacksChainhookFullSpecification, PredicateStatus)>, - startup_predicates: Option>, + redis_seed: Option<(StacksChainhookSpecificationNetworkMap, PredicateStatus)>, + startup_predicates: Option>, ) -> TestSetupResult { let ( redis_port, @@ -426,14 +425,14 @@ pub async fn setup_stacks_chainhook_test( panic!("test failed with error: {e}"); }); let stacks_spec = predicate - .into_selected_network_specification(&StacksNetwork::Devnet) + .into_specification_for_network(&StacksNetwork::Devnet) .unwrap_or_else(|e| { flush_redis(redis_port); redis_process.kill().unwrap(); panic!("test failed with error: {e}"); }); - let spec = ChainhookSpecification::Stacks(stacks_spec); + let spec = ChainhookInstance::Stacks(stacks_spec); update_predicate_spec(&spec.key(), &spec, &mut connection, &ctx); update_predicate_status(&spec.key(), status, &mut connection, &ctx); } diff --git a/components/chainhook-cli/src/service/tests/mod.rs b/components/chainhook-cli/src/service/tests/mod.rs index 71e03ca3f..3b816b544 100644 --- a/components/chainhook-cli/src/service/tests/mod.rs +++ b/components/chainhook-cli/src/service/tests/mod.rs @@ -1,4 +1,4 @@ -use chainhook_sdk::chainhooks::types::ChainhookFullSpecification; +use chainhook_sdk::chainhooks::types::ChainhookSpecificationNetworkMap; use chainhook_sdk::types::Chain; use chainhook_sdk::utils::Context; use rocket::serde::json::Value as JsonValue; @@ -779,7 +779,7 @@ async fn it_allows_specifying_startup_predicate() -> Result<(), String> { ); let predicate = serde_json::from_value(predicate).expect("failed to set up stacks chanhook spec for test"); - let startup_predicate = ChainhookFullSpecification::Stacks(predicate); + let startup_predicate = ChainhookSpecificationNetworkMap::Stacks(predicate); let TestSetupResult { mut redis_process, working_dir, @@ -819,7 +819,7 @@ async fn register_predicate_responds_409_if_uuid_in_use() -> Result<(), String> ); let stacks_spec = serde_json::from_value(predicate.clone()) .expect("failed to set up stacks chanhook spec for test"); - let startup_predicate = ChainhookFullSpecification::Stacks(stacks_spec); + let startup_predicate = ChainhookSpecificationNetworkMap::Stacks(stacks_spec); let TestSetupResult { mut redis_process, diff --git a/components/chainhook-cli/src/service/tests/observer_tests.rs b/components/chainhook-cli/src/service/tests/observer_tests.rs index a1ca7cc13..cfb7699be 100644 --- a/components/chainhook-cli/src/service/tests/observer_tests.rs +++ b/components/chainhook-cli/src/service/tests/observer_tests.rs @@ -1,6 +1,7 @@ use std::{sync::mpsc::channel, thread::sleep, time::Duration}; use chainhook_sdk::{ + chainhooks::types::ChainhookStore, observer::{start_event_observer, EventObserverConfig}, types::{BitcoinNetwork, StacksNodeConfig}, utils::Context, @@ -184,13 +185,12 @@ async fn it_responds_200_for_unimplemented_endpoints( body: Option<&Value>, ) { let ingestion_port = get_free_port().unwrap(); - let (working_dir, _tsv_dir) = create_tmp_working_dir().unwrap_or_else(|e| { + let (_working_dir, _tsv_dir) = create_tmp_working_dir().unwrap_or_else(|e| { panic!("test failed with error: {e}"); }); let config = EventObserverConfig { - chainhook_config: None, + registered_chainhooks: ChainhookStore::new(), bitcoin_rpc_proxy_enabled: false, - ingestion_port: ingestion_port, bitcoind_rpc_username: format!(""), bitcoind_rpc_password: format!(""), bitcoind_rpc_url: format!(""), @@ -200,11 +200,9 @@ async fn it_responds_200_for_unimplemented_endpoints( ingestion_port: ingestion_port, }, ), - display_logs: false, - cache_path: working_dir, + display_stacks_ingestion_logs: false, bitcoin_network: BitcoinNetwork::Regtest, stacks_network: chainhook_sdk::types::StacksNetwork::Devnet, - data_handler_tx: None, prometheus_monitoring_port: None, }; start_and_ping_event_observer(config, ingestion_port).await; diff --git a/components/chainhook-cli/src/service/tests/runloop_tests.rs b/components/chainhook-cli/src/service/tests/runloop_tests.rs index e8d8410fd..b672499d7 100644 --- a/components/chainhook-cli/src/service/tests/runloop_tests.rs +++ b/components/chainhook-cli/src/service/tests/runloop_tests.rs @@ -1,9 +1,10 @@ use std::{path::PathBuf, sync::mpsc::channel, thread::sleep, time::Duration}; use chainhook_sdk::{ - chainhooks::types::{ - BitcoinChainhookSpecification, BitcoinPredicateType, BlockIdentifierIndexRule, HookAction, - StacksChainhookSpecification, StacksPredicate, + chainhooks::{ + bitcoin::{BitcoinChainhookInstance, BitcoinPredicateType}, + stacks::{StacksChainhookInstance, StacksPredicate}, + types::{BlockIdentifierIndexRule, HookAction}, }, types::{BitcoinNetwork, StacksNetwork}, utils::Context, @@ -65,7 +66,7 @@ async fn test_stacks_runloop_kill_scan() { .expect("unable to spawn thread"); let uuid = "test".to_string(); - let predicate_spec = StacksChainhookSpecification { + let predicate_spec = StacksChainhookInstance { uuid: uuid.clone(), owner_uuid: None, name: "idc".to_string(), @@ -133,7 +134,7 @@ async fn test_stacks_bitcoin_kill_scan() { .expect("unable to spawn thread"); let uuid = "test".to_string(); - let predicate_spec = BitcoinChainhookSpecification { + let predicate_spec = BitcoinChainhookInstance { uuid: uuid.clone(), owner_uuid: None, name: "idc".to_string(), diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs index 1f5b306fa..143917a7d 100644 --- a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs @@ -1,15 +1,12 @@ -use super::types::{ - BitcoinChainhookSpecification, BitcoinPredicateType, DescriptorMatchingRule, ExactMatchingRule, - HookAction, InputPredicate, MatchingRule, OrdinalOperations, OrdinalsMetaProtocol, - OutputPredicate, StacksOperations, -}; +use super::types::{ChainhookInstance, ExactMatchingRule, HookAction, MatchingRule}; use crate::utils::Context; use bitcoincore_rpc_json::bitcoin::{address::Payload, Address}; use chainhook_types::{ - BitcoinBlockData, BitcoinChainEvent, BitcoinTransactionData, BlockIdentifier, + BitcoinBlockData, BitcoinChainEvent, BitcoinNetwork, BitcoinTransactionData, BlockIdentifier, StacksBaseChainOperation, TransactionIdentifier, }; +use schemars::JsonSchema; use hiro_system_kit::slog; @@ -17,9 +14,10 @@ use miniscript::bitcoin::secp256k1::Secp256k1; use miniscript::Descriptor; use reqwest::{Client, Method}; +use serde::{de, Deserialize, Deserializer}; use serde_json::Value as JsonValue; use std::{ - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, HashSet}, str::FromStr, }; @@ -27,8 +25,167 @@ use reqwest::RequestBuilder; use hex::FromHex; +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct BitcoinChainhookSpecification { + #[serde(skip_serializing_if = "Option::is_none")] + pub blocks: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expire_after_occurrence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_proof: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_inputs: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_outputs: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_witness: Option, + #[serde(rename = "if_this")] + pub predicate: BitcoinPredicateType, + #[serde(rename = "then_that")] + pub action: HookAction, +} + +/// Maps some [BitcoinChainhookSpecification] to a corresponding [BitcoinNetwork]. This allows maintaining one +/// serialized predicate file for a given predicate on each network. +/// +/// ### Examples +/// Given some file `predicate.json`: +/// ```json +/// { +/// "uuid": "my-id", +/// "name": "My Predicate", +/// "chain": "bitcoin", +/// "version": 1, +/// "networks": { +/// "regtest": { +/// // ... +/// }, +/// "testnet": { +/// // ... +/// }, +/// "mainnet": { +/// // ... +/// } +/// } +/// } +/// ``` +/// You can deserialize the file to this type and create a [BitcoinChainhookInstance] for the desired network: +/// ``` +/// use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookSpecificationNetworkMap; +/// use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookInstance; +/// use chainhook_types::BitcoinNetwork; +/// +/// fn get_predicate(network: &BitcoinNetwork) -> Result { +/// let json_predicate = +/// std::fs::read_to_string("./predicate.json").expect("Unable to read file"); +/// let hook_map: BitcoinChainhookSpecificationNetworkMap = +/// serde_json::from_str(&json_predicate).expect("Unable to parse Chainhook map"); +/// hook_map.into_specification_for_network(network) +/// } +/// +/// ``` +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct BitcoinChainhookSpecificationNetworkMap { + pub uuid: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_uuid: Option, + pub name: String, + pub version: u32, + pub networks: BTreeMap, +} + +impl BitcoinChainhookSpecificationNetworkMap { + pub fn into_specification_for_network( + mut self, + network: &BitcoinNetwork, + ) -> Result { + let spec = self + .networks + .remove(network) + .ok_or("Network unknown".to_string())?; + Ok(BitcoinChainhookInstance { + uuid: self.uuid, + owner_uuid: self.owner_uuid, + name: self.name, + network: network.clone(), + version: self.version, + start_block: spec.start_block, + end_block: spec.end_block, + blocks: spec.blocks, + expire_after_occurrence: spec.expire_after_occurrence, + predicate: spec.predicate, + action: spec.action, + include_proof: spec.include_proof.unwrap_or(false), + include_inputs: spec.include_inputs.unwrap_or(false), + include_outputs: spec.include_outputs.unwrap_or(false), + include_witness: spec.include_witness.unwrap_or(false), + enabled: false, + expired_at: None, + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct BitcoinChainhookInstance { + pub uuid: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_uuid: Option, + pub name: String, + pub network: BitcoinNetwork, + pub version: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub blocks: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expire_after_occurrence: Option, + pub predicate: BitcoinPredicateType, + pub action: HookAction, + pub include_proof: bool, + pub include_inputs: bool, + pub include_outputs: bool, + pub include_witness: bool, + pub enabled: bool, + pub expired_at: Option, +} + +impl BitcoinChainhookInstance { + pub fn key(&self) -> String { + ChainhookInstance::bitcoin_key(&self.uuid) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct BitcoinTransactionFilterPredicate { + pub predicate: BitcoinPredicateType, +} + +impl BitcoinTransactionFilterPredicate { + pub fn new(predicate: BitcoinPredicateType) -> BitcoinTransactionFilterPredicate { + BitcoinTransactionFilterPredicate { predicate } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "scope")] +pub enum BitcoinPredicateType { + Block, + Txid(ExactMatchingRule), + Inputs(InputPredicate), + Outputs(OutputPredicate), + StacksProtocol(StacksOperations), + OrdinalsProtocol(OrdinalOperations), +} + pub struct BitcoinTriggerChainhook<'a> { - pub chainhook: &'a BitcoinChainhookSpecification, + pub chainhook: &'a BitcoinChainhookInstance, pub apply: Vec<(Vec<&'a BitcoinTransactionData>, &'a BitcoinBlockData)>, pub rollback: Vec<(Vec<&'a BitcoinTransactionData>, &'a BitcoinBlockData)>, } @@ -44,6 +201,177 @@ pub struct BitcoinChainhookPayload { pub uuid: String, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum InputPredicate { + Txid(TxinPredicate), + WitnessScript(MatchingRule), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum OutputPredicate { + OpReturn(MatchingRule), + P2pkh(ExactMatchingRule), + P2sh(ExactMatchingRule), + P2wpkh(ExactMatchingRule), + P2wsh(ExactMatchingRule), + Descriptor(DescriptorMatchingRule), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "operation")] +pub enum StacksOperations { + StackerRewarded, + BlockCommitted, + LeaderRegistered, + StxTransferred, + StxLocked, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum OrdinalsMetaProtocol { + All, + #[serde(rename = "brc-20")] + Brc20, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub struct InscriptionFeedData { + #[serde(skip_serializing_if = "Option::is_none")] + pub meta_protocols: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "operation")] +pub enum OrdinalOperations { + InscriptionFeed(InscriptionFeedData), +} + +pub fn get_stacks_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] { + match network { + BitcoinNetwork::Mainnet => *b"X2", + BitcoinNetwork::Testnet => *b"T2", + BitcoinNetwork::Regtest => *b"id", + BitcoinNetwork::Signet => unreachable!(), + } +} + +pub struct PoxConfig { + pub genesis_block_height: u64, + pub prepare_phase_len: u64, + pub reward_phase_len: u64, + pub rewarded_addresses_per_block: usize, +} + +impl PoxConfig { + pub fn get_pox_cycle_len(&self) -> u64 { + self.prepare_phase_len + self.reward_phase_len + } + + pub fn get_pox_cycle_id(&self, block_height: u64) -> u64 { + (block_height.saturating_sub(self.genesis_block_height)) / self.get_pox_cycle_len() + } + + pub fn get_pos_in_pox_cycle(&self, block_height: u64) -> u64 { + (block_height.saturating_sub(self.genesis_block_height)) % self.get_pox_cycle_len() + } + + pub fn get_burn_address(&self) -> &str { + match self.genesis_block_height { + 666050 => "1111111111111111111114oLvT2", + 2000000 => "burn-address-regtest", + _ => "burn-address", + } + } +} +const POX_CONFIG_MAINNET: PoxConfig = PoxConfig { + genesis_block_height: 666050, + prepare_phase_len: 100, + reward_phase_len: 2100, + rewarded_addresses_per_block: 2, +}; + +const POX_CONFIG_TESTNET: PoxConfig = PoxConfig { + genesis_block_height: 2000000, + prepare_phase_len: 50, + reward_phase_len: 1050, + rewarded_addresses_per_block: 2, +}; + +const POX_CONFIG_DEVNET: PoxConfig = PoxConfig { + genesis_block_height: 100, + prepare_phase_len: 4, + reward_phase_len: 10, + rewarded_addresses_per_block: 2, +}; + +pub fn get_canonical_pox_config(network: &BitcoinNetwork) -> PoxConfig { + match network { + BitcoinNetwork::Mainnet => POX_CONFIG_MAINNET, + BitcoinNetwork::Testnet => POX_CONFIG_TESTNET, + BitcoinNetwork::Regtest => POX_CONFIG_DEVNET, + BitcoinNetwork::Signet => unreachable!(), + } +} + +#[derive(Debug, Clone, PartialEq)] +#[repr(u8)] +pub enum StacksOpcodes { + BlockCommit = b'[', + KeyRegister = b'^', + StackStx = b'x', + PreStx = b'p', + TransferStx = b'$', +} + +impl TryFrom for StacksOpcodes { + type Error = (); + + fn try_from(v: u8) -> Result { + match v { + x if x == StacksOpcodes::BlockCommit as u8 => Ok(StacksOpcodes::BlockCommit), + x if x == StacksOpcodes::KeyRegister as u8 => Ok(StacksOpcodes::KeyRegister), + x if x == StacksOpcodes::StackStx as u8 => Ok(StacksOpcodes::StackStx), + x if x == StacksOpcodes::PreStx as u8 => Ok(StacksOpcodes::PreStx), + x if x == StacksOpcodes::TransferStx as u8 => Ok(StacksOpcodes::TransferStx), + _ => Err(()), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct TxinPredicate { + pub txid: String, + pub vout: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct DescriptorMatchingRule { + // expression defines the bitcoin descriptor. + pub expression: String, + #[serde(default, deserialize_with = "deserialize_descriptor_range")] + pub range: Option<[u32; 2]>, +} + +// deserialize_descriptor_range makes sure that the range value is valid. +fn deserialize_descriptor_range<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let range: [u32; 2] = Deserialize::deserialize(deserializer)?; + if !(range[0] < range[1]) { + Err(de::Error::custom( + "First element of 'range' must be lower than the second element", + )) + } else { + Ok(Some(range)) + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BitcoinChainhookOccurrencePayload { pub apply: Vec, @@ -95,7 +423,7 @@ pub enum BitcoinChainhookOccurrence { pub fn evaluate_bitcoin_chainhooks_on_chain_event<'a>( chain_event: &'a BitcoinChainEvent, - active_chainhooks: &Vec<&'a BitcoinChainhookSpecification>, + active_chainhooks: &Vec<&'a BitcoinChainhookInstance>, ctx: &Context, ) -> ( Vec>, @@ -226,7 +554,7 @@ pub fn serialize_bitcoin_payload_to_json<'a>( } pub fn serialize_bitcoin_transactions_to_json<'a>( - predicate_spec: &BitcoinChainhookSpecification, + predicate_spec: &BitcoinChainhookInstance, transactions: &Vec<&BitcoinTransactionData>, proofs: &HashMap<&'a TransactionIdentifier, String>, ) -> Vec { diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs index f2024a289..dd9cb52b2 100644 --- a/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use super::super::types::MatchingRule; use super::*; -use crate::chainhooks::types::InscriptionFeedData; +use crate::chainhooks::bitcoin::InscriptionFeedData; use crate::indexer::tests::helpers::accounts; use crate::indexer::tests::helpers::bitcoin_blocks::generate_test_bitcoin_block; use crate::indexer::tests::helpers::transactions::generate_test_tx_bitcoin_p2pkh_transfer; @@ -164,7 +164,7 @@ fn it_serdes_occurrence_payload( 3, ); let block = generate_test_bitcoin_block(0, 0, vec![transaction.clone()], None); - let chainhook = &BitcoinChainhookSpecification { + let chainhook = &BitcoinChainhookInstance { uuid: "uuid".into(), owner_uuid: None, name: "name".into(), @@ -230,7 +230,7 @@ fn it_serdes_brc20_payload(tick: String) { let block = generate_test_bitcoin_block(0, 0, vec![transaction.clone()], None); let mut meta_protocols = HashSet::::new(); meta_protocols.insert(OrdinalsMetaProtocol::Brc20); - let chainhook = &BitcoinChainhookSpecification { + let chainhook = &BitcoinChainhookInstance { uuid: "uuid".into(), owner_uuid: None, name: "name".into(), diff --git a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs index d76b7ce5f..de5370422 100644 --- a/components/chainhook-sdk/src/chainhooks/stacks/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/stacks/mod.rs @@ -1,16 +1,15 @@ use crate::utils::{AbstractStacksBlock, Context}; -use super::types::{ - BlockIdentifierIndexRule, ExactMatchingRule, HookAction, StacksChainhookSpecification, - StacksContractDeploymentPredicate, StacksPredicate, StacksPrintEventBasedPredicate, -}; +use super::types::{BlockIdentifierIndexRule, ChainhookInstance, ExactMatchingRule, HookAction}; use chainhook_types::{ - BlockIdentifier, StacksChainEvent, StacksTransactionData, StacksTransactionEvent, - StacksTransactionEventPayload, StacksTransactionKind, TransactionIdentifier, + BlockIdentifier, StacksChainEvent, StacksNetwork, StacksTransactionData, + StacksTransactionEvent, StacksTransactionEventPayload, StacksTransactionKind, + TransactionIdentifier, }; use hiro_system_kit::slog; use regex::Regex; use reqwest::{Client, Method}; +use schemars::JsonSchema; use serde_json::Value as JsonValue; use stacks_codec::clarity::codec::StacksMessageCodec; use stacks_codec::clarity::vm::types::{CharType, SequenceData, Value as ClarityValue}; @@ -19,9 +18,222 @@ use std::io::Cursor; use reqwest::RequestBuilder; +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct StacksChainhookSpecification { + #[serde(skip_serializing_if = "Option::is_none")] + pub blocks: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expire_after_occurrence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub capture_all_events: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub decode_clarity_values: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_contract_abi: Option, + #[serde(rename = "if_this")] + pub predicate: StacksPredicate, + #[serde(rename = "then_that")] + pub action: HookAction, +} + +/// Maps some [StacksChainhookSpecification] to a corresponding [StacksNetwork]. This allows maintaining one +/// serialized predicate file for a given predicate on each network. +/// +/// ### Examples +/// Given some file `predicate.json`: +/// ```json +/// { +/// "uuid": "my-id", +/// "name": "My Predicate", +/// "chain": "stacks", +/// "version": 1, +/// "networks": { +/// "devnet": { +/// // ... +/// }, +/// "testnet": { +/// // ... +/// }, +/// "mainnet": { +/// // ... +/// } +/// } +/// } +/// ``` +/// You can deserialize the file to this type and create a [StacksChainhookInstance] for the desired network: +/// ``` +/// use chainhook_sdk::chainhooks::stacks::StacksChainhookSpecificationNetworkMap; +/// use chainhook_sdk::chainhooks::stacks::StacksChainhookInstance; +/// use chainhook_types::StacksNetwork; +/// +/// fn get_predicate(network: &StacksNetwork) -> Result { +/// let json_predicate = +/// std::fs::read_to_string("./predicate.json").expect("Unable to read file"); +/// let hook_map: StacksChainhookSpecificationNetworkMap = +/// serde_json::from_str(&json_predicate).expect("Unable to parse Chainhook map"); +/// hook_map.into_specification_for_network(network) +/// } +/// +/// ``` +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct StacksChainhookSpecificationNetworkMap { + pub uuid: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_uuid: Option, + pub name: String, + pub version: u32, + pub networks: BTreeMap, +} + +impl StacksChainhookSpecificationNetworkMap { + pub fn into_specification_for_network( + mut self, + network: &StacksNetwork, + ) -> Result { + let spec = self + .networks + .remove(network) + .ok_or("Network unknown".to_string())?; + Ok(StacksChainhookInstance { + uuid: self.uuid, + owner_uuid: self.owner_uuid, + name: self.name, + network: network.clone(), + version: self.version, + start_block: spec.start_block, + end_block: spec.end_block, + blocks: spec.blocks, + capture_all_events: spec.capture_all_events, + decode_clarity_values: spec.decode_clarity_values, + expire_after_occurrence: spec.expire_after_occurrence, + include_contract_abi: spec.include_contract_abi, + predicate: spec.predicate, + action: spec.action, + enabled: false, + expired_at: None, + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct StacksChainhookInstance { + pub uuid: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_uuid: Option, + pub name: String, + pub network: StacksNetwork, + pub version: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub blocks: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_block: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expire_after_occurrence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub capture_all_events: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub decode_clarity_values: Option, + pub include_contract_abi: Option, + #[serde(rename = "predicate")] + pub predicate: StacksPredicate, + pub action: HookAction, + pub enabled: bool, + pub expired_at: Option, +} + +impl StacksChainhookInstance { + pub fn key(&self) -> String { + ChainhookInstance::stacks_key(&self.uuid) + } + + pub fn is_predicate_targeting_block_header(&self) -> bool { + match &self.predicate { + StacksPredicate::BlockHeight(_) => true, + _ => false, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "scope")] +pub enum StacksPredicate { + BlockHeight(BlockIdentifierIndexRule), + ContractDeployment(StacksContractDeploymentPredicate), + ContractCall(StacksContractCallBasedPredicate), + PrintEvent(StacksPrintEventBasedPredicate), + FtEvent(StacksFtEventBasedPredicate), + NftEvent(StacksNftEventBasedPredicate), + StxEvent(StacksStxEventBasedPredicate), + Txid(ExactMatchingRule), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct StacksContractCallBasedPredicate { + pub contract_identifier: String, + pub method: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum StacksContractDeploymentPredicate { + Deployer(String), + ImplementTrait(StacksTrait), +} +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum StacksTrait { + Sip09, + Sip10, + #[serde(rename = "*")] + Any, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[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)] +#[serde(rename_all = "snake_case")] +pub struct StacksFtEventBasedPredicate { + pub asset_identifier: String, + pub actions: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct StacksNftEventBasedPredicate { + pub asset_identifier: String, + pub actions: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct StacksStxEventBasedPredicate { + pub actions: Vec, +} + #[derive(Clone)] pub struct StacksTriggerChainhook<'a> { - pub chainhook: &'a StacksChainhookSpecification, + pub chainhook: &'a StacksChainhookInstance, pub apply: Vec<(Vec<&'a StacksTransactionData>, &'a dyn AbstractStacksBlock)>, pub rollback: Vec<(Vec<&'a StacksTransactionData>, &'a dyn AbstractStacksBlock)>, } @@ -103,7 +315,7 @@ impl<'a> StacksTriggerChainhook<'a> { pub fn evaluate_stacks_chainhooks_on_chain_event<'a>( chain_event: &'a StacksChainEvent, - active_chainhooks: Vec<&'a StacksChainhookSpecification>, + active_chainhooks: Vec<&'a StacksChainhookInstance>, ctx: &Context, ) -> ( Vec>, @@ -301,7 +513,7 @@ pub fn evaluate_stacks_chainhooks_on_chain_event<'a>( pub fn evaluate_stacks_chainhook_on_blocks<'a>( blocks: Vec<&'a dyn AbstractStacksBlock>, - chainhook: &'a StacksChainhookSpecification, + chainhook: &'a StacksChainhookInstance, ctx: &Context, ) -> ( Vec<(Vec<&'a StacksTransactionData>, &'a dyn AbstractStacksBlock)>, @@ -338,7 +550,7 @@ pub fn evaluate_stacks_chainhook_on_blocks<'a>( pub fn evaluate_stacks_predicate_on_block<'a>( block: &'a dyn AbstractStacksBlock, - chainhook: &'a StacksChainhookSpecification, + chainhook: &'a StacksChainhookInstance, _ctx: &Context, ) -> bool { match &chainhook.predicate { @@ -366,7 +578,7 @@ pub fn evaluate_stacks_predicate_on_block<'a>( pub fn evaluate_stacks_predicate_on_transaction<'a>( transaction: &'a StacksTransactionData, - chainhook: &'a StacksChainhookSpecification, + chainhook: &'a StacksChainhookInstance, ctx: &Context, ) -> bool { match &chainhook.predicate { diff --git a/components/chainhook-sdk/src/chainhooks/tests/mod.rs b/components/chainhook-sdk/src/chainhooks/tests/mod.rs index ecedbed5d..a503d9340 100644 --- a/components/chainhook-sdk/src/chainhooks/tests/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/tests/mod.rs @@ -5,20 +5,18 @@ use self::fixtures::get_all_event_payload_types; use super::{ stacks::{ evaluate_stacks_chainhooks_on_chain_event, handle_stacks_hook_action, - StacksChainhookOccurrence, StacksTriggerChainhook, - }, - types::{ - ExactMatchingRule, FileHook, StacksChainhookSpecification, - StacksContractCallBasedPredicate, StacksContractDeploymentPredicate, - StacksFtEventBasedPredicate, StacksNftEventBasedPredicate, StacksPrintEventBasedPredicate, - StacksTrait, + StacksChainhookInstance, StacksChainhookOccurrence, StacksContractCallBasedPredicate, + StacksContractDeploymentPredicate, StacksFtEventBasedPredicate, + StacksNftEventBasedPredicate, StacksPredicate, StacksPrintEventBasedPredicate, + StacksStxEventBasedPredicate, StacksTrait, StacksTriggerChainhook, }, + types::{ExactMatchingRule, FileHook}, }; use crate::{chainhooks::stacks::serialize_stacks_payload_to_json, utils::Context}; use crate::{ chainhooks::{ tests::fixtures::{get_expected_occurrence, get_test_event_payload_by_type}, - types::{HookAction, StacksPredicate, StacksStxEventBasedPredicate}, + types::HookAction, }, utils::AbstractStacksBlock, }; @@ -389,7 +387,7 @@ fn test_stacks_predicates( confirmed_blocks: vec![], }); // Prepare predicate - let chainhook = StacksChainhookSpecification { + let chainhook = StacksChainhookInstance { uuid: "".to_string(), owner_uuid: None, name: "".to_string(), @@ -469,7 +467,7 @@ fn test_stacks_predicate_contract_deploy(predicate: StacksPredicate, expected_ap confirmed_blocks: vec![], }); // Prepare predicate - let chainhook = StacksChainhookSpecification { + let chainhook = StacksChainhookInstance { uuid: "".to_string(), owner_uuid: None, name: "".to_string(), @@ -524,7 +522,7 @@ fn verify_optional_addition_of_contract_abi() { new_blocks, confirmed_blocks: vec![], }); - let mut contract_deploy_chainhook = StacksChainhookSpecification { + let mut contract_deploy_chainhook = StacksChainhookInstance { uuid: "contract-deploy".to_string(), owner_uuid: None, name: "".to_string(), @@ -544,7 +542,7 @@ fn verify_optional_addition_of_contract_abi() { enabled: true, expired_at: None, }; - let contract_call_chainhook = StacksChainhookSpecification { + let contract_call_chainhook = StacksChainhookInstance { uuid: "contract-call".to_string(), owner_uuid: None, name: "".to_string(), @@ -663,7 +661,7 @@ fn test_stacks_predicate_contract_call(predicate: StacksPredicate, expected_appl confirmed_blocks: vec![], }); // Prepare predicate - let chainhook = StacksChainhookSpecification { + let chainhook = StacksChainhookInstance { uuid: "".to_string(), owner_uuid: None, name: "".to_string(), @@ -698,7 +696,7 @@ fn test_stacks_predicate_contract_call(predicate: StacksPredicate, expected_appl #[test] fn test_stacks_hook_action_noop() { - let chainhook = StacksChainhookSpecification { + let chainhook = StacksChainhookInstance { uuid: "".to_string(), owner_uuid: None, name: "".to_string(), @@ -756,7 +754,7 @@ fn test_stacks_hook_action_noop() { #[test] fn test_stacks_hook_action_file_append() { - let chainhook = StacksChainhookSpecification { + let chainhook = StacksChainhookInstance { uuid: "".to_string(), owner_uuid: None, name: "".to_string(), diff --git a/components/chainhook-sdk/src/chainhooks/types.rs b/components/chainhook-sdk/src/chainhooks/types.rs index d67ccd429..dc965d9f4 100644 --- a/components/chainhook-sdk/src/chainhooks/types.rs +++ b/components/chainhook-sdk/src/chainhooks/types.rs @@ -1,51 +1,54 @@ -use std::collections::{BTreeMap, HashSet}; - use chainhook_types::{BitcoinNetwork, StacksNetwork}; use reqwest::Url; use serde::ser::{SerializeSeq, Serializer}; -use serde::{de, Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use schemars::JsonSchema; use crate::utils::MAX_BLOCK_HEIGHTS_ENTRIES; +use crate::chainhooks::bitcoin::BitcoinChainhookInstance; +use crate::chainhooks::bitcoin::BitcoinChainhookSpecificationNetworkMap; +use crate::chainhooks::stacks::StacksChainhookInstance; +use crate::chainhooks::stacks::StacksChainhookSpecificationNetworkMap; + #[derive(Deserialize, Debug, Clone)] -pub struct ChainhookConfig { - pub stacks_chainhooks: Vec, - pub bitcoin_chainhooks: Vec, +pub struct ChainhookStore { + pub stacks_chainhooks: Vec, + pub bitcoin_chainhooks: Vec, } -impl ChainhookConfig { - pub fn new() -> ChainhookConfig { - ChainhookConfig { +impl ChainhookStore { + pub fn new() -> ChainhookStore { + ChainhookStore { stacks_chainhooks: vec![], bitcoin_chainhooks: vec![], } } - pub fn register_full_specification( + pub fn register_instance_from_network_map( &mut self, networks: (&BitcoinNetwork, &StacksNetwork), - hook: ChainhookFullSpecification, - ) -> Result { + hook: ChainhookSpecificationNetworkMap, + ) -> Result { let spec = match hook { - ChainhookFullSpecification::Stacks(hook) => { - let spec = hook.into_selected_network_specification(networks.1)?; + ChainhookSpecificationNetworkMap::Stacks(hook) => { + let spec = hook.into_specification_for_network(networks.1)?; self.stacks_chainhooks.push(spec.clone()); - ChainhookSpecification::Stacks(spec) + ChainhookInstance::Stacks(spec) } - ChainhookFullSpecification::Bitcoin(hook) => { - let spec = hook.into_selected_network_specification(networks.0)?; + ChainhookSpecificationNetworkMap::Bitcoin(hook) => { + let spec = hook.into_specification_for_network(networks.0)?; self.bitcoin_chainhooks.push(spec.clone()); - ChainhookSpecification::Bitcoin(spec) + ChainhookInstance::Bitcoin(spec) } }; Ok(spec) } - pub fn enable_specification(&mut self, predicate_spec: &mut ChainhookSpecification) { + pub fn enable_instance(&mut self, predicate_spec: &mut ChainhookInstance) { match predicate_spec { - ChainhookSpecification::Stacks(spec_to_enable) => { + ChainhookInstance::Stacks(spec_to_enable) => { for spec in self.stacks_chainhooks.iter_mut() { if spec.uuid.eq(&spec_to_enable.uuid) { spec.enabled = true; @@ -54,7 +57,7 @@ impl ChainhookConfig { } } } - ChainhookSpecification::Bitcoin(spec_to_enable) => { + ChainhookInstance::Bitcoin(spec_to_enable) => { for spec in self.bitcoin_chainhooks.iter_mut() { if spec.uuid.eq(&spec_to_enable.uuid) { spec.enabled = true; @@ -66,13 +69,13 @@ impl ChainhookConfig { }; } - pub fn register_specification(&mut self, spec: ChainhookSpecification) -> Result<(), String> { + pub fn register_instance(&mut self, spec: ChainhookInstance) -> Result<(), String> { match spec { - ChainhookSpecification::Stacks(spec) => { + ChainhookInstance::Stacks(spec) => { let spec = spec.clone(); self.stacks_chainhooks.push(spec); } - ChainhookSpecification::Bitcoin(spec) => { + ChainhookInstance::Bitcoin(spec) => { let spec = spec.clone(); self.bitcoin_chainhooks.push(spec); } @@ -80,10 +83,7 @@ impl ChainhookConfig { Ok(()) } - pub fn deregister_stacks_hook( - &mut self, - hook_uuid: String, - ) -> Option { + pub fn deregister_stacks_hook(&mut self, hook_uuid: String) -> Option { let mut i = 0; while i < self.stacks_chainhooks.len() { if self.stacks_chainhooks[i].uuid == hook_uuid { @@ -99,7 +99,7 @@ impl ChainhookConfig { pub fn deregister_bitcoin_hook( &mut self, hook_uuid: String, - ) -> Option { + ) -> Option { let mut i = 0; while i < self.bitcoin_chainhooks.len() { if self.bitcoin_chainhooks[i].uuid == hook_uuid { @@ -115,7 +115,7 @@ impl ChainhookConfig { pub fn expire_stacks_hook(&mut self, hook_uuid: String, block_height: u64) { let mut i = 0; while i < self.stacks_chainhooks.len() { - if ChainhookSpecification::stacks_key(&self.stacks_chainhooks[i].uuid) == hook_uuid { + if ChainhookInstance::stacks_key(&self.stacks_chainhooks[i].uuid) == hook_uuid { self.stacks_chainhooks[i].expired_at = Some(block_height); break; } else { @@ -127,7 +127,7 @@ impl ChainhookConfig { pub fn expire_bitcoin_hook(&mut self, hook_uuid: String, block_height: u64) { let mut i = 0; while i < self.bitcoin_chainhooks.len() { - if ChainhookSpecification::bitcoin_key(&self.bitcoin_chainhooks[i].uuid) == hook_uuid { + if ChainhookInstance::bitcoin_key(&self.bitcoin_chainhooks[i].uuid) == hook_uuid { self.bitcoin_chainhooks[i].expired_at = Some(block_height); break; } else { @@ -137,7 +137,7 @@ impl ChainhookConfig { } } -impl Serialize for ChainhookConfig { +impl Serialize for ChainhookStore { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -157,12 +157,12 @@ impl Serialize for ChainhookConfig { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] -pub enum ChainhookSpecification { - Bitcoin(BitcoinChainhookSpecification), - Stacks(StacksChainhookSpecification), +pub enum ChainhookInstance { + Bitcoin(BitcoinChainhookInstance), + Stacks(StacksChainhookInstance), } -impl ChainhookSpecification { +impl ChainhookInstance { pub fn either_stx_or_btc_key(uuid: &str) -> String { format!("predicate:{}", uuid) } @@ -182,8 +182,8 @@ impl ChainhookSpecification { } } - pub fn deserialize_specification(spec: &str) -> Result { - let spec: ChainhookSpecification = serde_json::from_str(spec) + pub fn deserialize_specification(spec: &str) -> Result { + let spec: ChainhookInstance = serde_json::from_str(spec) .map_err(|e| format!("unable to deserialize predicate {}", e.to_string()))?; Ok(spec) } @@ -196,46 +196,14 @@ impl ChainhookSpecification { } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct BitcoinChainhookSpecification { - pub uuid: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_uuid: Option, - pub name: String, - pub network: BitcoinNetwork, - pub version: u32, - #[serde(skip_serializing_if = "Option::is_none")] - pub blocks: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub expire_after_occurrence: Option, - pub predicate: BitcoinPredicateType, - pub action: HookAction, - pub include_proof: bool, - pub include_inputs: bool, - pub include_outputs: bool, - pub include_witness: bool, - pub enabled: bool, - pub expired_at: Option, -} - -impl BitcoinChainhookSpecification { - pub fn key(&self) -> String { - ChainhookSpecification::bitcoin_key(&self.uuid) - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case", tag = "chain")] -pub enum ChainhookFullSpecification { - Bitcoin(BitcoinChainhookFullSpecification), - Stacks(StacksChainhookFullSpecification), +pub enum ChainhookSpecificationNetworkMap { + Bitcoin(BitcoinChainhookSpecificationNetworkMap), + Stacks(StacksChainhookSpecificationNetworkMap), } -impl ChainhookFullSpecification { +impl ChainhookSpecificationNetworkMap { pub fn validate(&self) -> Result<(), String> { match &self { Self::Bitcoin(data) => { @@ -286,140 +254,13 @@ impl ChainhookFullSpecification { pub fn deserialize_specification( spec: &str, _key: &str, - ) -> Result { - let spec: ChainhookFullSpecification = serde_json::from_str(spec) + ) -> Result { + let spec: ChainhookSpecificationNetworkMap = serde_json::from_str(spec) .map_err(|e| format!("unable to deserialize predicate {}", e.to_string()))?; Ok(spec) } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct BitcoinChainhookFullSpecification { - pub uuid: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_uuid: Option, - pub name: String, - pub version: u32, - pub networks: BTreeMap, -} - -impl BitcoinChainhookFullSpecification { - pub fn into_selected_network_specification( - mut self, - network: &BitcoinNetwork, - ) -> Result { - let spec = self - .networks - .remove(network) - .ok_or("Network unknown".to_string())?; - Ok(BitcoinChainhookSpecification { - uuid: self.uuid, - owner_uuid: self.owner_uuid, - name: self.name, - network: network.clone(), - version: self.version, - start_block: spec.start_block, - end_block: spec.end_block, - blocks: spec.blocks, - expire_after_occurrence: spec.expire_after_occurrence, - predicate: spec.predicate, - action: spec.action, - include_proof: spec.include_proof.unwrap_or(false), - include_inputs: spec.include_inputs.unwrap_or(false), - include_outputs: spec.include_outputs.unwrap_or(false), - include_witness: spec.include_witness.unwrap_or(false), - enabled: false, - expired_at: None, - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct BitcoinChainhookNetworkSpecification { - #[serde(skip_serializing_if = "Option::is_none")] - pub blocks: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub expire_after_occurrence: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_proof: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_inputs: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_outputs: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_witness: Option, - #[serde(rename = "if_this")] - pub predicate: BitcoinPredicateType, - #[serde(rename = "then_that")] - pub action: HookAction, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct StacksChainhookFullSpecification { - pub uuid: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_uuid: Option, - pub name: String, - pub version: u32, - pub networks: BTreeMap, -} - -impl StacksChainhookFullSpecification { - pub fn into_selected_network_specification( - mut self, - network: &StacksNetwork, - ) -> Result { - let spec = self - .networks - .remove(network) - .ok_or("Network unknown".to_string())?; - Ok(StacksChainhookSpecification { - uuid: self.uuid, - owner_uuid: self.owner_uuid, - name: self.name, - network: network.clone(), - version: self.version, - start_block: spec.start_block, - end_block: spec.end_block, - blocks: spec.blocks, - capture_all_events: spec.capture_all_events, - decode_clarity_values: spec.decode_clarity_values, - expire_after_occurrence: spec.expire_after_occurrence, - include_contract_abi: spec.include_contract_abi, - predicate: spec.predicate, - action: spec.action, - enabled: false, - expired_at: None, - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct StacksChainhookNetworkSpecification { - #[serde(skip_serializing_if = "Option::is_none")] - pub blocks: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub expire_after_occurrence: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub capture_all_events: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub decode_clarity_values: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_contract_abi: Option, - #[serde(rename = "if_this")] - pub predicate: StacksPredicate, - #[serde(rename = "then_that")] - pub action: HookAction, -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HookAction { @@ -454,7 +295,7 @@ pub struct HttpHook { pub struct FileHook { pub path: String, } - +// todo: can we remove this struct? #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct ScriptTemplate { pub instructions: Vec, @@ -499,177 +340,6 @@ impl ScriptTemplate { } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct BitcoinTransactionFilterPredicate { - pub predicate: BitcoinPredicateType, -} - -impl BitcoinTransactionFilterPredicate { - pub fn new(predicate: BitcoinPredicateType) -> BitcoinTransactionFilterPredicate { - BitcoinTransactionFilterPredicate { predicate } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case", tag = "scope")] -pub enum BitcoinPredicateType { - Block, - Txid(ExactMatchingRule), - Inputs(InputPredicate), - Outputs(OutputPredicate), - StacksProtocol(StacksOperations), - OrdinalsProtocol(OrdinalOperations), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum InputPredicate { - Txid(TxinPredicate), - WitnessScript(MatchingRule), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum OutputPredicate { - OpReturn(MatchingRule), - P2pkh(ExactMatchingRule), - P2sh(ExactMatchingRule), - P2wpkh(ExactMatchingRule), - P2wsh(ExactMatchingRule), - Descriptor(DescriptorMatchingRule), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case", tag = "operation")] -pub enum StacksOperations { - StackerRewarded, - BlockCommitted, - LeaderRegistered, - StxTransferred, - StxLocked, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum OrdinalsMetaProtocol { - All, - #[serde(rename = "brc-20")] - Brc20, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -pub struct InscriptionFeedData { - #[serde(skip_serializing_if = "Option::is_none")] - pub meta_protocols: Option>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case", tag = "operation")] -pub enum OrdinalOperations { - InscriptionFeed(InscriptionFeedData), -} - -pub fn get_stacks_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] { - match network { - BitcoinNetwork::Mainnet => *b"X2", - BitcoinNetwork::Testnet => *b"T2", - BitcoinNetwork::Regtest => *b"id", - BitcoinNetwork::Signet => unreachable!(), - } -} - -pub struct PoxConfig { - pub genesis_block_height: u64, - pub prepare_phase_len: u64, - pub reward_phase_len: u64, - pub rewarded_addresses_per_block: usize, -} - -impl PoxConfig { - pub fn get_pox_cycle_len(&self) -> u64 { - self.prepare_phase_len + self.reward_phase_len - } - - pub fn get_pox_cycle_id(&self, block_height: u64) -> u64 { - (block_height.saturating_sub(self.genesis_block_height)) / self.get_pox_cycle_len() - } - - pub fn get_pos_in_pox_cycle(&self, block_height: u64) -> u64 { - (block_height.saturating_sub(self.genesis_block_height)) % self.get_pox_cycle_len() - } - - pub fn get_burn_address(&self) -> &str { - match self.genesis_block_height { - 666050 => "1111111111111111111114oLvT2", - 2000000 => "burn-address-regtest", - _ => "burn-address", - } - } -} - -const POX_CONFIG_MAINNET: PoxConfig = PoxConfig { - genesis_block_height: 666050, - prepare_phase_len: 100, - reward_phase_len: 2100, - rewarded_addresses_per_block: 2, -}; - -const POX_CONFIG_TESTNET: PoxConfig = PoxConfig { - genesis_block_height: 2000000, - prepare_phase_len: 50, - reward_phase_len: 1050, - rewarded_addresses_per_block: 2, -}; - -const POX_CONFIG_DEVNET: PoxConfig = PoxConfig { - genesis_block_height: 100, - prepare_phase_len: 4, - reward_phase_len: 10, - rewarded_addresses_per_block: 2, -}; - -pub fn get_canonical_pox_config(network: &BitcoinNetwork) -> PoxConfig { - match network { - BitcoinNetwork::Mainnet => POX_CONFIG_MAINNET, - BitcoinNetwork::Testnet => POX_CONFIG_TESTNET, - BitcoinNetwork::Regtest => POX_CONFIG_DEVNET, - BitcoinNetwork::Signet => unreachable!(), - } -} - -#[derive(Debug, Clone, PartialEq)] -#[repr(u8)] -pub enum StacksOpcodes { - BlockCommit = '[' as u8, - KeyRegister = '^' as u8, - StackStx = 'x' as u8, - PreStx = 'p' as u8, - TransferStx = '$' as u8, -} - -impl TryFrom for StacksOpcodes { - type Error = (); - - fn try_from(v: u8) -> Result { - match v { - x if x == StacksOpcodes::BlockCommit as u8 => Ok(StacksOpcodes::BlockCommit), - x if x == StacksOpcodes::KeyRegister as u8 => Ok(StacksOpcodes::KeyRegister), - x if x == StacksOpcodes::StackStx as u8 => Ok(StacksOpcodes::StackStx), - x if x == StacksOpcodes::PreStx as u8 => Ok(StacksOpcodes::PreStx), - x if x == StacksOpcodes::TransferStx as u8 => Ok(StacksOpcodes::TransferStx), - _ => Err(()), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct TxinPredicate { - pub txid: String, - pub vout: u32, -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum BlockIdentifierIndexRule { @@ -700,30 +370,6 @@ pub enum ExactMatchingRule { Equals(String), } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct DescriptorMatchingRule { - // expression defines the bitcoin descriptor. - pub expression: String, - #[serde(default, deserialize_with = "deserialize_descriptor_range")] - pub range: Option<[u32; 2]>, -} - -// deserialize_descriptor_range makes sure that the range value is valid. -fn deserialize_descriptor_range<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let range: [u32; 2] = Deserialize::deserialize(deserializer)?; - if !(range[0] < range[1]) { - Err(de::Error::custom( - "First element of 'range' must be lower than the second element", - )) - } else { - Ok(Some(range)) - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum BlockIdentifierHashRule { @@ -731,121 +377,6 @@ pub enum BlockIdentifierHashRule { BuildsOff(String), } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct StacksChainhookSpecification { - pub uuid: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub owner_uuid: Option, - pub name: String, - pub network: StacksNetwork, - pub version: u32, - #[serde(skip_serializing_if = "Option::is_none")] - pub blocks: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_block: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub expire_after_occurrence: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub capture_all_events: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub decode_clarity_values: Option, - pub include_contract_abi: Option, - #[serde(rename = "predicate")] - pub predicate: StacksPredicate, - pub action: HookAction, - pub enabled: bool, - pub expired_at: Option, -} - -impl StacksChainhookSpecification { - pub fn key(&self) -> String { - ChainhookSpecification::stacks_key(&self.uuid) - } - - pub fn is_predicate_targeting_block_header(&self) -> bool { - match &self.predicate { - StacksPredicate::BlockHeight(_) - // | &StacksPredicate::BitcoinBlockHeight(_) - => true, - _ => false, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -#[serde(tag = "scope")] -pub enum StacksPredicate { - BlockHeight(BlockIdentifierIndexRule), - ContractDeployment(StacksContractDeploymentPredicate), - ContractCall(StacksContractCallBasedPredicate), - PrintEvent(StacksPrintEventBasedPredicate), - FtEvent(StacksFtEventBasedPredicate), - NftEvent(StacksNftEventBasedPredicate), - StxEvent(StacksStxEventBasedPredicate), - Txid(ExactMatchingRule), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct StacksContractCallBasedPredicate { - pub contract_identifier: String, - pub method: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -// #[serde(tag = "type", content = "rule")] -pub enum StacksContractDeploymentPredicate { - Deployer(String), - ImplementTrait(StacksTrait), -} -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum StacksTrait { - Sip09, - Sip10, - #[serde(rename = "*")] - Any, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -#[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)] -#[serde(rename_all = "snake_case")] -pub struct StacksFtEventBasedPredicate { - pub asset_identifier: String, - pub actions: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct StacksNftEventBasedPredicate { - pub asset_identifier: String, - pub actions: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct StacksStxEventBasedPredicate { - pub actions: Vec, -} - pub fn opcode_to_hex(asm: &str) -> Option { match asm { "OP_PUSHBYTES_0" => Some(0x00), diff --git a/components/chainhook-sdk/src/indexer/bitcoin/mod.rs b/components/chainhook-sdk/src/indexer/bitcoin/mod.rs index 65889dbbf..c28826020 100644 --- a/components/chainhook-sdk/src/indexer/bitcoin/mod.rs +++ b/components/chainhook-sdk/src/indexer/bitcoin/mod.rs @@ -1,9 +1,8 @@ use std::time::Duration; -use crate::chainhooks::types::{ +use crate::chainhooks::bitcoin::{ get_canonical_pox_config, get_stacks_canonical_magic_bytes, PoxConfig, StacksOpcodes, }; - use crate::observer::BitcoinConfig; use crate::utils::Context; use bitcoincore_rpc::bitcoin::hashes::Hash; @@ -92,9 +91,8 @@ pub struct GetRawTransactionResultVinScriptSig { } impl BitcoinTransactionInputFullBreakdown { - /// Whether this input is from a coinbase tx. - /// The [txid], [vout] and [script_sig] fields are not provided - /// for coinbase transactions. + /// Whether this input is from a coinbase tx. If there is not a [BitcoinTransactionInputFullBreakdown::txid] field, the transaction is a coinbase transaction. + // Note: vout and script_sig fields are also not provided for coinbase transactions. pub fn is_coinbase(&self) -> bool { self.txid.is_none() } diff --git a/components/chainhook-sdk/src/observer/mod.rs b/components/chainhook-sdk/src/observer/mod.rs index c4d2dbc40..0b887acba 100644 --- a/components/chainhook-sdk/src/observer/mod.rs +++ b/components/chainhook-sdk/src/observer/mod.rs @@ -4,14 +4,15 @@ mod zmq; use crate::chainhooks::bitcoin::{ evaluate_bitcoin_chainhooks_on_chain_event, handle_bitcoin_hook_action, - BitcoinChainhookOccurrence, BitcoinChainhookOccurrencePayload, BitcoinTriggerChainhook, + BitcoinChainhookInstance, BitcoinChainhookOccurrence, BitcoinChainhookOccurrencePayload, + BitcoinTriggerChainhook, }; use crate::chainhooks::stacks::{ - evaluate_stacks_chainhooks_on_chain_event, handle_stacks_hook_action, + evaluate_stacks_chainhooks_on_chain_event, handle_stacks_hook_action, StacksChainhookInstance, StacksChainhookOccurrence, StacksChainhookOccurrencePayload, }; use crate::chainhooks::types::{ - ChainhookConfig, ChainhookFullSpecification, ChainhookSpecification, + ChainhookInstance, ChainhookSpecificationNetworkMap, ChainhookStore, }; use crate::indexer::bitcoin::{ @@ -39,7 +40,6 @@ use rocket::Shutdown; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::error::Error; use std::net::{IpAddr, Ipv4Addr}; -use std::path::PathBuf; use std::str; use std::str::FromStr; use std::sync::mpsc::{Receiver, Sender}; @@ -68,40 +68,286 @@ pub enum DataHandlerEvent { #[derive(Debug, Clone)] pub struct EventObserverConfig { - pub chainhook_config: Option, + pub registered_chainhooks: ChainhookStore, pub bitcoin_rpc_proxy_enabled: bool, - pub ingestion_port: u16, pub bitcoind_rpc_username: String, pub bitcoind_rpc_password: String, pub bitcoind_rpc_url: String, pub bitcoin_block_signaling: BitcoinBlockSignaling, - pub display_logs: bool, - pub cache_path: String, + pub display_stacks_ingestion_logs: bool, pub bitcoin_network: BitcoinNetwork, pub stacks_network: StacksNetwork, - pub data_handler_tx: Option>, pub prometheus_monitoring_port: Option, } +/// A builder that is used to create a general purpose [EventObserverConfig]. +/// +/// ## Examples +/// ``` +/// use chainhook_sdk::observer::EventObserverConfig; +/// use chainhook_sdk::observer::EventObserverConfigBuilder; +/// +/// fn get_config() -> Result { +/// EventObserverConfigBuilder::new() +/// .bitcoind_rpc_password("my_password") +/// .bitcoin_network("mainnet") +/// .stacks_network("mainnet") +/// .finish() +/// } +/// ``` #[derive(Deserialize, Debug, Clone)] -pub struct EventObserverConfigOverrides { - pub ingestion_port: Option, +pub struct EventObserverConfigBuilder { pub bitcoind_rpc_username: Option, pub bitcoind_rpc_password: Option, pub bitcoind_rpc_url: Option, pub bitcoind_zmq_url: Option, + pub chainhook_stacks_block_ingestion_port: Option, pub stacks_node_rpc_url: Option, - pub display_logs: Option, - pub cache_path: Option, + pub display_stacks_ingestion_logs: Option, pub bitcoin_network: Option, pub stacks_network: Option, + pub prometheus_monitoring_port: Option, +} + +impl EventObserverConfigBuilder { + pub fn new() -> Self { + EventObserverConfigBuilder { + bitcoind_rpc_username: None, + bitcoind_rpc_password: None, + bitcoind_rpc_url: None, + bitcoind_zmq_url: None, + chainhook_stacks_block_ingestion_port: None, + stacks_node_rpc_url: None, + display_stacks_ingestion_logs: None, + bitcoin_network: None, + stacks_network: None, + prometheus_monitoring_port: None, + } + } + + /// Sets the bitcoind node's RPC username. + pub fn bitcoind_rpc_username(&mut self, username: &str) -> &mut Self { + self.bitcoind_rpc_username = Some(username.to_string()); + self + } + + /// Sets the bitcoind node's RPC password. + pub fn bitcoind_rpc_password(&mut self, password: &str) -> &mut Self { + self.bitcoind_rpc_password = Some(password.to_string()); + self + } + + /// Sets the bitcoind node's RPC url. + pub fn bitcoind_rpc_url(&mut self, url: &str) -> &mut Self { + self.bitcoind_rpc_url = Some(url.to_string()); + self + } + + /// Sets the bitcoind node's ZMQ url, used by the observer to receive new block events from bitcoind. + pub fn bitcoind_zmq_url(&mut self, url: &str) -> &mut Self { + self.bitcoind_zmq_url = Some(url.to_string()); + self + } + + /// Sets the Bitcoin network. Must be a valid bitcoin network string according to [BitcoinNetwork::from_str]. + pub fn bitcoin_network(&mut self, network: &str) -> &mut Self { + self.bitcoin_network = Some(network.to_string()); + self + } + + /// Sets the Stacks network. Must be a valid bitcoin network string according to [StacksNetwork::from_str]. + pub fn stacks_network(&mut self, network: &str) -> &mut Self { + self.stacks_network = Some(network.to_string()); + self + } + + /// Sets the Stacks node's RPC url. + pub fn stacks_node_rpc_url(&mut self, url: &str) -> &mut Self { + self.stacks_node_rpc_url = Some(url.to_string()); + self + } + + /// Sets the port at which Chainhook will observer Stacks blockchain events. The Stacks node's config should have an events_observer + /// entry matching this port in order to send block events the Chainhook. + pub fn chainhook_stacks_block_ingestion_port(&mut self, port: u16) -> &mut Self { + self.chainhook_stacks_block_ingestion_port = Some(port); + self + } + + /// Sets whether Chainhook should display Stacks ingestion logs. + pub fn display_stacks_ingestion_logs(&mut self, display_logs: bool) -> &mut Self { + self.display_stacks_ingestion_logs = Some(display_logs); + self + } + + /// Sets the Prometheus monitoring port. + pub fn prometheus_monitoring_port(&mut self, port: u16) -> &mut Self { + self.prometheus_monitoring_port = Some(port); + self + } + + /// Attempts to convert a [EventObserverConfigBuilder] instance into an [EventObserverConfig], filling in + /// defaults as necessary according to [EventObserverConfig::default]. + /// + /// This function will return an error if the `bitcoin_network` or `stacks_network` strings are set and are not a valid [BitcoinNetwork] or [StacksNetwork]. + /// + pub fn finish(&self) -> Result { + EventObserverConfig::new_using_overrides(Some(self)) + } +} + +/// A builder that is used to create an [EventObserverConfig] that is tailored for use with a bitcoind node emitting events via the ZMQ interface. +/// +/// ## Examples +/// ``` +/// use chainhook_sdk::observer::EventObserverConfig; +/// use chainhook_sdk::observer::BitcoinEventObserverConfigBuilder; +/// +/// fn get_config() -> Result { +/// BitcoinEventObserverConfigBuilder::new() +/// .rpc_password("my_password") +/// .network("mainnet") +/// .finish() +/// } +/// ``` +pub struct BitcoinEventObserverConfigBuilder { + pub bitcoind_rpc_username: Option, + pub bitcoind_rpc_password: Option, + pub bitcoind_rpc_url: Option, + pub bitcoin_network: Option, + pub bitcoind_zmq_url: Option, + pub prometheus_monitoring_port: Option, +} +impl BitcoinEventObserverConfigBuilder { + pub fn new() -> Self { + BitcoinEventObserverConfigBuilder { + bitcoind_rpc_username: None, + bitcoind_rpc_password: None, + bitcoind_rpc_url: None, + bitcoin_network: None, + bitcoind_zmq_url: None, + prometheus_monitoring_port: None, + } + } + + /// Sets the bitcoind node's RPC username. + pub fn rpc_username(&mut self, username: &str) -> &mut Self { + self.bitcoind_rpc_username = Some(username.to_string()); + self + } + + /// Sets the bitcoind node's RPC password. + pub fn rpc_password(&mut self, password: &str) -> &mut Self { + self.bitcoind_rpc_password = Some(password.to_string()); + self + } + + /// Sets the bitcoind node's RPC url. + pub fn rpc_url(&mut self, url: &str) -> &mut Self { + self.bitcoind_rpc_url = Some(url.to_string()); + self + } + + /// Sets the bitcoind node's ZMQ url, used by the observer to receive new block events from bitcoind. + pub fn zmq_url(&mut self, url: &str) -> &mut Self { + self.bitcoind_zmq_url = Some(url.to_string()); + self + } + + /// Sets the Bitcoin network. Must be a valid bitcoin network string according to [BitcoinNetwork::from_str]. + pub fn network(&mut self, network: &str) -> &mut Self { + self.bitcoin_network = Some(network.to_string()); + self + } + + /// Sets the Prometheus monitoring port. + pub fn prometheus_monitoring_port(&mut self, port: u16) -> &mut Self { + self.prometheus_monitoring_port = Some(port); + self + } + + /// Attempts to convert a [BitcoinEventObserverConfigBuilder] instance into an [EventObserverConfig], filling in + /// defaults as necessary according to [EventObserverConfig::default]. + /// + /// This function will return an error if the `bitcoin_network` string is set and is not a valid [BitcoinNetwork]. + pub fn finish(&self) -> Result { + let bitcoin_network = if let Some(network) = self.bitcoin_network.as_ref() { + BitcoinNetwork::from_str(&network)? + } else { + BitcoinNetwork::Regtest + }; + Ok(EventObserverConfig { + registered_chainhooks: ChainhookStore::new(), + bitcoin_rpc_proxy_enabled: false, + bitcoind_rpc_username: self + .bitcoind_rpc_username + .clone() + .unwrap_or_else(|| "devnet".into()), + bitcoind_rpc_password: self + .bitcoind_rpc_password + .clone() + .unwrap_or_else(|| "devnet".into()), + bitcoind_rpc_url: self + .bitcoind_rpc_url + .clone() + .unwrap_or_else(|| "http://localhost:18443".into()), + bitcoin_block_signaling: BitcoinBlockSignaling::ZeroMQ( + self.bitcoind_zmq_url + .clone() + .unwrap_or_else(|| "tcp://0.0.0.0:18543".into()), + ), + display_stacks_ingestion_logs: false, + bitcoin_network: bitcoin_network, + stacks_network: StacksNetwork::Devnet, + prometheus_monitoring_port: self.prometheus_monitoring_port, + }) + } } impl EventObserverConfig { - pub fn get_cache_path_buf(&self) -> PathBuf { - let mut path_buf = PathBuf::new(); - path_buf.push(&self.cache_path); - path_buf + pub fn default() -> Self { + EventObserverConfig { + registered_chainhooks: ChainhookStore::new(), + bitcoin_rpc_proxy_enabled: false, + bitcoind_rpc_username: "devnet".into(), + bitcoind_rpc_password: "devnet".into(), + bitcoind_rpc_url: "http://localhost:18443".into(), + bitcoin_block_signaling: BitcoinBlockSignaling::Stacks(StacksNodeConfig::new( + DEFAULT_STACKS_NODE_RPC.to_string(), + DEFAULT_INGESTION_PORT, + )), + display_stacks_ingestion_logs: false, + bitcoin_network: BitcoinNetwork::Regtest, + stacks_network: StacksNetwork::Devnet, + prometheus_monitoring_port: None, + } + } + + /// Adds a [ChainhookInstance] to config's the registered chainhook store, returning the updated config. + pub fn register_chainhook_instance( + &mut self, + spec: ChainhookInstance, + ) -> Result<&mut Self, String> { + let mut chainhook_config = ChainhookStore::new(); + chainhook_config.register_instance(spec)?; + self.registered_chainhooks = chainhook_config; + + Ok(self) + } + + /// Adds a [BitcoinChainhookInstance] to the config's registered chainhook store, returning the updated config. + pub fn register_bitcoin_chainhook_instance( + &mut self, + spec: BitcoinChainhookInstance, + ) -> Result<&mut Self, String> { + self.register_chainhook_instance(ChainhookInstance::Bitcoin(spec)) + } + /// Adds a [StacksChainhookInstance] to the config's registered chainhook store, returning the updated config. + pub fn register_stacks_chainhook_instance( + &mut self, + spec: StacksChainhookInstance, + ) -> Result<&mut Self, String> { + self.register_chainhook_instance(ChainhookInstance::Stacks(spec)) } pub fn get_bitcoin_config(&self) -> BitcoinConfig { @@ -115,23 +361,6 @@ impl EventObserverConfig { bitcoin_config } - pub fn get_chainhook_store(&self) -> ChainhookStore { - let mut chainhook_store = ChainhookStore::new(); - // If authorization not required, we create a default ChainhookConfig - if let Some(ref chainhook_config) = self.chainhook_config { - let mut chainhook_config = chainhook_config.clone(); - chainhook_store - .predicates - .stacks_chainhooks - .append(&mut chainhook_config.stacks_chainhooks); - chainhook_store - .predicates - .bitcoin_chainhooks - .append(&mut chainhook_config.bitcoin_chainhooks); - } - chainhook_store - } - pub fn get_stacks_node_config(&self) -> &StacksNodeConfig { match self.bitcoin_block_signaling { BitcoinBlockSignaling::Stacks(ref config) => config, @@ -143,7 +372,7 @@ impl EventObserverConfig { /// /// *Note: This is used by external crates, so it should not be removed, even if not used internally by Chainhook.* pub fn new_using_overrides( - overrides: Option<&EventObserverConfigOverrides>, + overrides: Option<&EventObserverConfigBuilder>, ) -> Result { let bitcoin_network = if let Some(network) = overrides.and_then(|c| c.bitcoin_network.as_ref()) { @@ -161,38 +390,35 @@ impl EventObserverConfig { let config = EventObserverConfig { bitcoin_rpc_proxy_enabled: false, - chainhook_config: None, - ingestion_port: overrides - .and_then(|c| c.ingestion_port) - .unwrap_or(DEFAULT_INGESTION_PORT), + registered_chainhooks: ChainhookStore::new(), bitcoind_rpc_username: overrides .and_then(|c| c.bitcoind_rpc_username.clone()) - .unwrap_or("devnet".to_string()), + .unwrap_or_else(|| "devnet".to_string()), bitcoind_rpc_password: overrides .and_then(|c| c.bitcoind_rpc_password.clone()) - .unwrap_or("devnet".to_string()), + .unwrap_or_else(|| "devnet".to_string()), bitcoind_rpc_url: overrides .and_then(|c| c.bitcoind_rpc_url.clone()) - .unwrap_or("http://localhost:18443".to_string()), + .unwrap_or_else(|| "http://localhost:18443".to_string()), bitcoin_block_signaling: overrides .and_then(|c| c.bitcoind_zmq_url.as_ref()) .map(|url| BitcoinBlockSignaling::ZeroMQ(url.clone())) - .unwrap_or(BitcoinBlockSignaling::Stacks(StacksNodeConfig::new( - overrides - .and_then(|c| c.stacks_node_rpc_url.clone()) - .unwrap_or(DEFAULT_STACKS_NODE_RPC.to_string()), - overrides - .and_then(|c| c.ingestion_port) - .unwrap_or(DEFAULT_INGESTION_PORT), - ))), - display_logs: overrides.and_then(|c| c.display_logs).unwrap_or(false), - cache_path: overrides - .and_then(|c| c.cache_path.clone()) - .unwrap_or("cache".to_string()), + .unwrap_or_else(|| { + BitcoinBlockSignaling::Stacks(StacksNodeConfig::new( + overrides + .and_then(|c| c.stacks_node_rpc_url.clone()) + .unwrap_or_else(|| DEFAULT_STACKS_NODE_RPC.to_string()), + overrides + .and_then(|c| c.chainhook_stacks_block_ingestion_port) + .unwrap_or_else(|| DEFAULT_INGESTION_PORT), + )) + }), + display_stacks_ingestion_logs: overrides + .and_then(|c| c.display_stacks_ingestion_logs) + .unwrap_or_else(|| false), bitcoin_network, stacks_network, - data_handler_tx: None, - prometheus_monitoring_port: None, + prometheus_monitoring_port: overrides.and_then(|c| c.prometheus_monitoring_port), }; Ok(config) } @@ -211,8 +437,8 @@ pub enum ObserverCommand { PropagateBitcoinChainEvent(BlockchainEvent), PropagateStacksChainEvent(StacksChainEvent), PropagateStacksMempoolEvent(StacksChainMempoolEvent), - RegisterPredicate(ChainhookFullSpecification), - EnablePredicate(ChainhookSpecification), + RegisterPredicate(ChainhookSpecificationNetworkMap), + EnablePredicate(ChainhookInstance), DeregisterBitcoinPredicate(String), DeregisterStacksPredicate(String), ExpireBitcoinPredicate(HookExpirationData), @@ -311,9 +537,9 @@ pub enum ObserverEvent { BitcoinChainEvent((BitcoinChainEvent, PredicateEvaluationReport)), StacksChainEvent((StacksChainEvent, PredicateEvaluationReport)), NotifyBitcoinTransactionProxied, - PredicateRegistered(ChainhookSpecification), + PredicateRegistered(ChainhookInstance), PredicateDeregistered(PredicateDeregisteredEvent), - PredicateEnabled(ChainhookSpecification), + PredicateEnabled(ChainhookInstance), BitcoinPredicateTriggered(BitcoinChainhookOccurrencePayload), StacksPredicateTriggered(StacksChainhookOccurrencePayload), PredicatesTriggered(usize), @@ -350,22 +576,6 @@ pub struct BitcoinConfig { pub bitcoin_block_signaling: BitcoinBlockSignaling, } -#[derive(Debug, Clone)] -pub struct ChainhookStore { - pub predicates: ChainhookConfig, -} - -impl ChainhookStore { - pub fn new() -> Self { - Self { - predicates: ChainhookConfig { - stacks_chainhooks: vec![], - bitcoin_chainhooks: vec![], - }, - } - } -} - #[derive(Debug, Clone)] pub struct BitcoinBlockDataCached { pub block: BitcoinBlockData, @@ -441,6 +651,98 @@ impl ObserverSidecar { } } +/// A helper struct used to configure and call [start_event_observer], which spawns a thread to observer chain events. +/// +/// ### Examples +/// ``` +/// use chainhook_sdk::observer::EventObserverBuilder; +/// use chainhook_sdk::observer::EventObserverConfig; +/// use chainhook_sdk::observer::ObserverCommand; +/// use chainhook_sdk::utils::Context; +/// use std::error::Error; +/// use std::sync::mpsc::{Receiver, Sender}; +/// +/// fn start_event_observer( +/// config: EventObserverConfig, +/// observer_commands_tx: &Sender, +/// observer_commands_rx: Receiver, +/// ctx: &Context, +/// )-> Result<(), Box> { +/// EventObserverBuilder::new( +/// config, +/// &observer_commands_tx, +/// observer_commands_rx, +/// &ctx +/// ) +/// .start() +/// } +/// ``` +pub struct EventObserverBuilder { + config: EventObserverConfig, + observer_commands_tx: Sender, + observer_commands_rx: Receiver, + ctx: Context, + observer_events_tx: Option>, + observer_sidecar: Option, + stacks_startup_context: Option, +} + +impl EventObserverBuilder { + pub fn new( + config: EventObserverConfig, + observer_commands_tx: &Sender, + observer_commands_rx: Receiver, + ctx: &Context, + ) -> Self { + EventObserverBuilder { + config: config, + observer_commands_tx: observer_commands_tx.clone(), + observer_commands_rx: observer_commands_rx, + ctx: ctx.clone(), + observer_events_tx: None, + observer_sidecar: None, + stacks_startup_context: None, + } + } + + /// Sets the `observer_events_tx` Sender. Set this and listen on the corresponding + /// Receiver to be notified of every [ObserverEvent]. + pub fn events_tx( + &mut self, + observer_events_tx: crossbeam_channel::Sender, + ) -> &mut Self { + self.observer_events_tx = Some(observer_events_tx); + self + } + + /// Sets a sidecar for the observer. See [ObserverSidecar]. + pub fn sidecar(&mut self, sidecar: ObserverSidecar) -> &mut Self { + self.observer_sidecar = Some(sidecar); + self + } + + /// Sets the Stacks startup context. See [StacksObserverStartupContext]. + pub fn stacks_startup_context(&mut self, context: StacksObserverStartupContext) -> &mut Self { + self.stacks_startup_context = Some(context); + self + } + + /// Starts the event observer, calling [start_event_observer]. This function consumes the + /// [EventObserverBuilder] and spawns a new thread to run the observer. + pub fn start(self) -> Result<(), Box> { + start_event_observer( + self.config, + self.observer_commands_tx, + self.observer_commands_rx, + self.observer_events_tx, + self.observer_sidecar, + self.stacks_startup_context, + self.ctx, + ) + } +} + +/// Spawns a thread to observe blockchain events. Use [EventObserverBuilder] to configure easily. pub fn start_event_observer( config: EventObserverConfig, observer_commands_tx: Sender, @@ -543,7 +845,7 @@ pub async fn start_bitcoin_event_observer( observer_sidecar: Option, ctx: Context, ) -> Result<(), Box> { - let chainhook_store = config.get_chainhook_store(); + let chainhook_store = config.registered_chainhooks.clone(); #[cfg(feature = "zeromq")] { let ctx_moved = ctx.clone(); @@ -556,8 +858,8 @@ pub async fn start_bitcoin_event_observer( let prometheus_monitoring = PrometheusMonitoring::new(); prometheus_monitoring.initialize( - chainhook_store.predicates.stacks_chainhooks.len() as u64, - chainhook_store.predicates.bitcoin_chainhooks.len() as u64, + chainhook_store.stacks_chainhooks.len() as u64, + chainhook_store.bitcoin_chainhooks.len() as u64, None, ); @@ -609,7 +911,7 @@ pub async fn start_stacks_event_observer( indexer.seed_stacks_block_pool(stacks_startup_context.block_pool_seed, &ctx); - let log_level = if config.display_logs { + let log_level = if config.display_stacks_ingestion_logs { if cfg!(feature = "cli") { LogLevel::Critical } else { @@ -623,7 +925,7 @@ pub async fn start_stacks_event_observer( let bitcoin_rpc_proxy_enabled = config.bitcoin_rpc_proxy_enabled; let bitcoin_config = config.get_bitcoin_config(); - let chainhook_store = config.get_chainhook_store(); + let chainhook_store = config.registered_chainhooks.clone(); let indexer_rw_lock = Arc::new(RwLock::new(indexer)); @@ -631,8 +933,8 @@ pub async fn start_stacks_event_observer( let prometheus_monitoring = PrometheusMonitoring::new(); prometheus_monitoring.initialize( - chainhook_store.predicates.stacks_chainhooks.len() as u64, - chainhook_store.predicates.bitcoin_chainhooks.len() as u64, + chainhook_store.stacks_chainhooks.len() as u64, + chainhook_store.bitcoin_chainhooks.len() as u64, Some(stacks_startup_context.last_block_height_appended), ); @@ -1075,7 +1377,6 @@ pub async fn start_observer_commands_handler( let mut report = PredicateEvaluationReport::new(); let bitcoin_chainhooks = chainhook_store - .predicates .bitcoin_chainhooks .iter() .filter(|p| p.enabled) @@ -1202,7 +1503,6 @@ pub async fn start_observer_commands_handler( for hook_uuid in hooks_ids_to_deregister.iter() { if chainhook_store - .predicates .deregister_bitcoin_hook(hook_uuid.clone()) .is_some() { @@ -1226,12 +1526,10 @@ pub async fn start_observer_commands_handler( } } Err(e) => { - chainhook_store - .predicates - .deregister_bitcoin_hook(data.chainhook.uuid.clone()); + chainhook_store.deregister_bitcoin_hook(data.chainhook.uuid.clone()); if let Some(ref tx) = observer_events_tx { let _ = tx.send(ObserverEvent::PredicateInterrupted(PredicateInterruptedData { - predicate_key: ChainhookSpecification::bitcoin_key(&data.chainhook.uuid), + predicate_key: ChainhookInstance::bitcoin_key(&data.chainhook.uuid), error: format!("Unable to evaluate predicate on Bitcoin chainstate: {}", e) })); } @@ -1254,7 +1552,6 @@ pub async fn start_observer_commands_handler( let mut report = PredicateEvaluationReport::new(); let stacks_chainhooks = chainhook_store - .predicates .stacks_chainhooks .iter() .filter(|p| p.enabled) @@ -1388,7 +1685,6 @@ pub async fn start_observer_commands_handler( for hook_uuid in hooks_ids_to_deregister.iter() { if chainhook_store - .predicates .deregister_stacks_hook(hook_uuid.clone()) .is_some() { @@ -1420,12 +1716,10 @@ pub async fn start_observer_commands_handler( } } Err(e) => { - chainhook_store - .predicates - .deregister_stacks_hook(data.chainhook.uuid.clone()); + chainhook_store.deregister_stacks_hook(data.chainhook.uuid.clone()); if let Some(ref tx) = observer_events_tx { let _ = tx.send(ObserverEvent::PredicateInterrupted(PredicateInterruptedData { - predicate_key: ChainhookSpecification::stacks_key(&data.chainhook.uuid), + predicate_key: ChainhookInstance::stacks_key(&data.chainhook.uuid), error: format!("Unable to evaluate predicate on Bitcoin chainstate: {}", e) })); } @@ -1458,28 +1752,26 @@ pub async fn start_observer_commands_handler( ObserverCommand::RegisterPredicate(spec) => { ctx.try_log(|logger| slog::info!(logger, "Handling RegisterPredicate command")); - let mut spec = match chainhook_store - .predicates - .register_full_specification(networks, spec) - { - Ok(spec) => spec, - Err(e) => { - ctx.try_log(|logger| { - slog::warn!( - logger, - "Unable to register new chainhook spec: {}", - e.to_string() - ) - }); - continue; - } - }; + let mut spec = + match chainhook_store.register_instance_from_network_map(networks, spec) { + Ok(spec) => spec, + Err(e) => { + ctx.try_log(|logger| { + slog::warn!( + logger, + "Unable to register new chainhook spec: {}", + e.to_string() + ) + }); + continue; + } + }; match spec { - ChainhookSpecification::Bitcoin(_) => { + ChainhookInstance::Bitcoin(_) => { prometheus_monitoring.btc_metrics_register_predicate() } - ChainhookSpecification::Stacks(_) => { + ChainhookInstance::Stacks(_) => { prometheus_monitoring.stx_metrics_register_predicate() } }; @@ -1493,12 +1785,12 @@ pub async fn start_observer_commands_handler( ctx.try_log(|logger| { slog::debug!(logger, "Enabling Predicate {}", spec.uuid()) }); - chainhook_store.predicates.enable_specification(&mut spec); + chainhook_store.enable_instance(&mut spec); } } ObserverCommand::EnablePredicate(mut spec) => { ctx.try_log(|logger| slog::info!(logger, "Enabling Predicate {}", spec.uuid())); - chainhook_store.predicates.enable_specification(&mut spec); + chainhook_store.enable_instance(&mut spec); if let Some(ref tx) = observer_events_tx { let _ = tx.send(ObserverEvent::PredicateEnabled(spec)); } @@ -1507,9 +1799,7 @@ pub async fn start_observer_commands_handler( ctx.try_log(|logger| { slog::info!(logger, "Handling DeregisterStacksPredicate command") }); - let hook = chainhook_store - .predicates - .deregister_stacks_hook(hook_uuid.clone()); + let hook = chainhook_store.deregister_stacks_hook(hook_uuid.clone()); if hook.is_some() { // on startup, only the predicates in the `chainhook_store` are added to the monitoring count, @@ -1530,9 +1820,7 @@ pub async fn start_observer_commands_handler( ctx.try_log(|logger| { slog::info!(logger, "Handling DeregisterBitcoinPredicate command") }); - let hook = chainhook_store - .predicates - .deregister_bitcoin_hook(hook_uuid.clone()); + let hook = chainhook_store.deregister_bitcoin_hook(hook_uuid.clone()); if hook.is_some() { // on startup, only the predicates in the `chainhook_store` are added to the monitoring count, @@ -1554,9 +1842,7 @@ pub async fn start_observer_commands_handler( block_height, }) => { ctx.try_log(|logger| slog::info!(logger, "Handling ExpireStacksPredicate command")); - chainhook_store - .predicates - .expire_stacks_hook(hook_uuid, block_height); + chainhook_store.expire_stacks_hook(hook_uuid, block_height); } ObserverCommand::ExpireBitcoinPredicate(HookExpirationData { hook_uuid, @@ -1565,9 +1851,7 @@ pub async fn start_observer_commands_handler( ctx.try_log(|logger| { slog::info!(logger, "Handling ExpireBitcoinPredicate command") }); - chainhook_store - .predicates - .expire_bitcoin_hook(hook_uuid, block_height); + chainhook_store.expire_bitcoin_hook(hook_uuid, block_height); } } } diff --git a/components/chainhook-sdk/src/observer/tests/mod.rs b/components/chainhook-sdk/src/observer/tests/mod.rs index 51525de73..aee263086 100644 --- a/components/chainhook-sdk/src/observer/tests/mod.rs +++ b/components/chainhook-sdk/src/observer/tests/mod.rs @@ -1,10 +1,18 @@ +use crate::chainhooks::bitcoin::BitcoinChainhookInstance; +use crate::chainhooks::bitcoin::BitcoinChainhookSpecification; +use crate::chainhooks::bitcoin::BitcoinChainhookSpecificationNetworkMap; +use crate::chainhooks::bitcoin::BitcoinPredicateType; +use crate::chainhooks::bitcoin::InscriptionFeedData; +use crate::chainhooks::bitcoin::OrdinalOperations; +use crate::chainhooks::bitcoin::OutputPredicate; +use crate::chainhooks::stacks::StacksChainhookInstance; +use crate::chainhooks::stacks::StacksChainhookSpecification; +use crate::chainhooks::stacks::StacksChainhookSpecificationNetworkMap; +use crate::chainhooks::stacks::StacksContractCallBasedPredicate; +use crate::chainhooks::stacks::StacksPredicate; use crate::chainhooks::types::{ - BitcoinChainhookFullSpecification, BitcoinChainhookNetworkSpecification, - BitcoinChainhookSpecification, BitcoinPredicateType, ChainhookConfig, - ChainhookFullSpecification, ChainhookSpecification, ExactMatchingRule, HookAction, - InscriptionFeedData, OrdinalOperations, OutputPredicate, StacksChainhookFullSpecification, - StacksChainhookNetworkSpecification, StacksChainhookSpecification, - StacksContractCallBasedPredicate, StacksPredicate, + ChainhookInstance, ChainhookSpecificationNetworkMap, ChainhookStore, ExactMatchingRule, + HookAction, }; use crate::indexer::fork_scratch_pad::ForkScratchPad; use crate::indexer::tests::helpers::transactions::generate_test_tx_bitcoin_p2pkh_transfer; @@ -12,9 +20,9 @@ use crate::indexer::tests::helpers::{ accounts, bitcoin_blocks, stacks_blocks, transactions::generate_test_tx_stacks_contract_call, }; use crate::monitoring::PrometheusMonitoring; +use crate::observer::PredicateDeregisteredEvent; use crate::observer::{ - start_observer_commands_handler, ChainhookStore, EventObserverConfig, ObserverCommand, - ObserverSidecar, PredicateDeregisteredEvent, + start_observer_commands_handler, EventObserverConfig, ObserverCommand, ObserverSidecar, }; use crate::utils::{AbstractBlock, Context}; use chainhook_types::{ @@ -31,25 +39,20 @@ use super::{ObserverEvent, DEFAULT_INGESTION_PORT}; fn generate_test_config() -> (EventObserverConfig, ChainhookStore) { let config: EventObserverConfig = EventObserverConfig { - chainhook_config: Some(ChainhookConfig::new()), + registered_chainhooks: ChainhookStore::new(), bitcoin_rpc_proxy_enabled: false, - ingestion_port: 0, bitcoind_rpc_username: "user".into(), bitcoind_rpc_password: "user".into(), bitcoind_rpc_url: "http://localhost:18443".into(), - display_logs: false, + display_stacks_ingestion_logs: false, bitcoin_block_signaling: BitcoinBlockSignaling::Stacks( StacksNodeConfig::default_localhost(DEFAULT_INGESTION_PORT), ), - cache_path: "cache".into(), bitcoin_network: BitcoinNetwork::Regtest, stacks_network: StacksNetwork::Devnet, - data_handler_tx: None, prometheus_monitoring_port: None, }; - let predicates = ChainhookConfig::new(); - let chainhook_store = ChainhookStore { predicates }; - (config, chainhook_store) + (config, ChainhookStore::new()) } fn stacks_chainhook_contract_call( @@ -57,11 +60,11 @@ fn stacks_chainhook_contract_call( contract_identifier: &str, expire_after_occurrence: Option, method: &str, -) -> StacksChainhookFullSpecification { +) -> StacksChainhookSpecificationNetworkMap { let mut networks = BTreeMap::new(); networks.insert( StacksNetwork::Devnet, - StacksChainhookNetworkSpecification { + StacksChainhookSpecification { start_block: None, end_block: None, blocks: None, @@ -77,7 +80,7 @@ fn stacks_chainhook_contract_call( }, ); - let spec = StacksChainhookFullSpecification { + let spec = StacksChainhookSpecificationNetworkMap { uuid: format!("{}", id), name: format!("Chainhook {}", id), owner_uuid: None, @@ -91,11 +94,11 @@ fn bitcoin_chainhook_p2pkh( id: u8, address: &str, expire_after_occurrence: Option, -) -> BitcoinChainhookFullSpecification { +) -> BitcoinChainhookSpecificationNetworkMap { let mut networks = BTreeMap::new(); networks.insert( BitcoinNetwork::Regtest, - BitcoinChainhookNetworkSpecification { + BitcoinChainhookSpecification { start_block: None, end_block: None, blocks: None, @@ -111,7 +114,7 @@ fn bitcoin_chainhook_p2pkh( }, ); - let spec = BitcoinChainhookFullSpecification { + let spec = BitcoinChainhookSpecificationNetworkMap { uuid: format!("{}", id), name: format!("Chainhook {}", id), owner_uuid: None, @@ -121,11 +124,11 @@ fn bitcoin_chainhook_p2pkh( spec } -fn bitcoin_chainhook_ordinals(id: u8) -> BitcoinChainhookFullSpecification { +fn bitcoin_chainhook_ordinals(id: u8) -> BitcoinChainhookSpecificationNetworkMap { let mut networks = BTreeMap::new(); networks.insert( BitcoinNetwork::Regtest, - BitcoinChainhookNetworkSpecification { + BitcoinChainhookSpecification { start_block: None, end_block: None, blocks: None, @@ -143,7 +146,7 @@ fn bitcoin_chainhook_ordinals(id: u8) -> BitcoinChainhookFullSpecification { }, ); - let spec = BitcoinChainhookFullSpecification { + let spec = BitcoinChainhookSpecificationNetworkMap { uuid: format!("{}", id), name: format!("Chainhook {}", id), owner_uuid: None, @@ -159,19 +162,19 @@ fn generate_and_register_new_stacks_chainhook( id: u8, contract_name: &str, method: &str, -) -> StacksChainhookSpecification { +) -> StacksChainhookInstance { let contract_identifier = format!("{}.{}", accounts::deployer_stx_address(), contract_name); let chainhook = stacks_chainhook_contract_call(id, &contract_identifier, None, method); let _ = observer_commands_tx.send(ObserverCommand::RegisterPredicate( - ChainhookFullSpecification::Stacks(chainhook.clone()), + ChainhookSpecificationNetworkMap::Stacks(chainhook.clone()), )); let mut chainhook = chainhook - .into_selected_network_specification(&StacksNetwork::Devnet) + .into_specification_for_network(&StacksNetwork::Devnet) .unwrap(); chainhook.enabled = true; - let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Stacks(chainhook.clone()), - )); + let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate(ChainhookInstance::Stacks( + chainhook.clone(), + ))); assert!(match observer_events_rx.recv() { Ok(ObserverEvent::PredicateRegistered(_)) => { // assert_eq!( @@ -182,9 +185,9 @@ fn generate_and_register_new_stacks_chainhook( } _ => false, }); - let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Stacks(chainhook.clone()), - )); + let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate(ChainhookInstance::Stacks( + chainhook.clone(), + ))); assert!(match observer_events_rx.recv() { Ok(ObserverEvent::PredicateEnabled(_)) => { // assert_eq!( @@ -204,17 +207,17 @@ fn generate_and_register_new_bitcoin_chainhook( id: u8, p2pkh_address: &str, expire_after_occurrence: Option, -) -> BitcoinChainhookSpecification { +) -> BitcoinChainhookInstance { let chainhook = bitcoin_chainhook_p2pkh(id, &p2pkh_address, expire_after_occurrence); let _ = observer_commands_tx.send(ObserverCommand::RegisterPredicate( - ChainhookFullSpecification::Bitcoin(chainhook.clone()), + ChainhookSpecificationNetworkMap::Bitcoin(chainhook.clone()), )); let mut chainhook = chainhook - .into_selected_network_specification(&BitcoinNetwork::Regtest) + .into_specification_for_network(&BitcoinNetwork::Regtest) .unwrap(); chainhook.enabled = true; let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Bitcoin(chainhook.clone()), + ChainhookInstance::Bitcoin(chainhook.clone()), )); assert!(match observer_events_rx.recv() { Ok(ObserverEvent::PredicateRegistered(_)) => { @@ -323,17 +326,17 @@ fn generate_and_register_new_ordinals_chainhook( observer_commands_tx: &Sender, observer_events_rx: &crossbeam_channel::Receiver, id: u8, -) -> BitcoinChainhookSpecification { +) -> BitcoinChainhookInstance { let chainhook = bitcoin_chainhook_ordinals(id); let _ = observer_commands_tx.send(ObserverCommand::RegisterPredicate( - ChainhookFullSpecification::Bitcoin(chainhook.clone()), + ChainhookSpecificationNetworkMap::Bitcoin(chainhook.clone()), )); let mut chainhook = chainhook - .into_selected_network_specification(&BitcoinNetwork::Regtest) + .into_specification_for_network(&BitcoinNetwork::Regtest) .unwrap(); chainhook.enabled = true; let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Bitcoin(chainhook.clone()), + ChainhookInstance::Bitcoin(chainhook.clone()), )); assert!(match observer_events_rx.recv() { Ok(ObserverEvent::PredicateRegistered(_)) => { @@ -585,15 +588,15 @@ fn test_stacks_chainhook_auto_deregister() { let contract_identifier = format!("{}.{}", accounts::deployer_stx_address(), "counter"); let chainhook = stacks_chainhook_contract_call(0, &contract_identifier, Some(1), "increment"); let _ = observer_commands_tx.send(ObserverCommand::RegisterPredicate( - ChainhookFullSpecification::Stacks(chainhook.clone()), + ChainhookSpecificationNetworkMap::Stacks(chainhook.clone()), )); let mut chainhook = chainhook - .into_selected_network_specification(&StacksNetwork::Devnet) + .into_specification_for_network(&StacksNetwork::Devnet) .unwrap(); chainhook.enabled = true; - let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate( - ChainhookSpecification::Stacks(chainhook.clone()), - )); + let _ = observer_commands_tx.send(ObserverCommand::EnablePredicate(ChainhookInstance::Stacks( + chainhook.clone(), + ))); assert!(match observer_events_rx.recv() { Ok(ObserverEvent::PredicateRegistered(_)) => { // assert_eq!( diff --git a/components/chainhook-types-rs/src/bitcoin.rs b/components/chainhook-types-rs/src/bitcoin.rs index 79743bb14..7718b65b7 100644 --- a/components/chainhook-types-rs/src/bitcoin.rs +++ b/components/chainhook-types-rs/src/bitcoin.rs @@ -63,7 +63,7 @@ impl TxOut { /// #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize)] pub struct Witness { - /// contains the witness Vec> serialization without the initial varint indicating the + /// contains the witness `Vec>` serialization without the initial varint indicating the /// number of elements (which is stored in `witness_elements`) content: Vec, diff --git a/components/chainhook-types-rs/src/rosetta.rs b/components/chainhook-types-rs/src/rosetta.rs index d917912cd..a794a27b0 100644 --- a/components/chainhook-types-rs/src/rosetta.rs +++ b/components/chainhook-types-rs/src/rosetta.rs @@ -456,9 +456,9 @@ pub struct PublicKey { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CurveType { - /// `y (255-bits) || x-sign-bit (1-bit)` - `32 bytes` (https://ed25519.cr.yp.to/ed25519-20110926.pdf) + /// `y (255-bits) || x-sign-bit (1-bit)` - `32 bytes` () Edwards25519, - /// SEC compressed - `33 bytes` (https://secg.org/sec1-v2.pdf#subsubsection.2.3.3) + /// SEC compressed - `33 bytes` () Secp256k1, } diff --git a/docs/chainhook-openapi.json b/docs/chainhook-openapi.json index c8235911f..1375554f7 100644 --- a/docs/chainhook-openapi.json +++ b/docs/chainhook-openapi.json @@ -49,7 +49,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChainhookFullSpecification" + "$ref": "#/components/schemas/ChainhookSpecificationNetworkMap" } } }, @@ -154,9 +154,10 @@ }, "components": { "schemas": { - "ChainhookFullSpecification": { + "ChainhookSpecificationNetworkMap": { "oneOf": [ { + "description": "Maps some [BitcoinChainhookSpecification] to a corresponding [BitcoinNetwork]. This allows maintaining one serialized predicate file for a given predicate on each network.\n\n### Examples Given some file `predicate.json`: ```json { \"uuid\": \"my-id\", \"name\": \"My Predicate\", \"chain\": \"bitcoin\", \"version\": 1, \"networks\": { \"regtest\": { // ... }, \"testnet\": { // ... }, \"mainnet\": { // ... } } } ``` You can deserialize the file to this type and create a [BitcoinChainhookInstance] for the desired network: ``` use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookSpecificationNetworkMap; use chainhook_sdk::chainhooks::bitcoin::BitcoinChainhookInstance; use chainhook_types::BitcoinNetwork;\n\nfn get_predicate(network: &BitcoinNetwork) -> Result { let json_predicate = std::fs::read_to_string(\"./predicate.json\").expect(\"Unable to read file\"); let hook_map: BitcoinChainhookSpecificationNetworkMap = serde_json::from_str(&json_predicate).expect(\"Unable to parse Chainhook map\"); hook_map.into_specification_for_network(network) }\n\n```", "type": "object", "required": [ "chain", @@ -196,7 +197,7 @@ ], "properties": { "regtest": { - "$ref": "#/components/schemas/BitcoinChainhookNetworkSpecification" + "$ref": "#/components/schemas/BitcoinChainhookSpecification" } } }, @@ -207,7 +208,7 @@ ], "properties": { "testnet": { - "$ref": "#/components/schemas/BitcoinChainhookNetworkSpecification" + "$ref": "#/components/schemas/BitcoinChainhookSpecification" } } }, @@ -218,7 +219,7 @@ ], "properties": { "signet": { - "$ref": "#/components/schemas/BitcoinChainhookNetworkSpecification" + "$ref": "#/components/schemas/BitcoinChainhookSpecification" } } }, @@ -229,7 +230,7 @@ ], "properties": { "mainnet": { - "$ref": "#/components/schemas/BitcoinChainhookNetworkSpecification" + "$ref": "#/components/schemas/BitcoinChainhookSpecification" } } } @@ -238,6 +239,7 @@ } }, { + "description": "Maps some [StacksChainhookSpecification] to a corresponding [StacksNetwork]. This allows maintaining one serialized predicate file for a given predicate on each network.\n\n### Examples Given some file `predicate.json`: ```json { \"uuid\": \"my-id\", \"name\": \"My Predicate\", \"chain\": \"stacks\", \"version\": 1, \"networks\": { \"devnet\": { // ... }, \"testnet\": { // ... }, \"mainnet\": { // ... } } } ``` You can deserialize the file to this type and create a [StacksChainhookInstance] for the desired network: ``` use chainhook_sdk::chainhooks::stacks::StacksChainhookSpecificationNetworkMap; use chainhook_sdk::chainhooks::stacks::StacksChainhookInstance; use chainhook_types::StacksNetwork;\n\nfn get_predicate(network: &StacksNetwork) -> Result { let json_predicate = std::fs::read_to_string(\"./predicate.json\").expect(\"Unable to read file\"); let hook_map: StacksChainhookSpecificationNetworkMap = serde_json::from_str(&json_predicate).expect(\"Unable to parse Chainhook map\"); hook_map.into_specification_for_network(network) }\n\n```", "type": "object", "required": [ "chain", @@ -277,7 +279,7 @@ ], "properties": { "simnet": { - "$ref": "#/components/schemas/StacksChainhookNetworkSpecification" + "$ref": "#/components/schemas/StacksChainhookSpecification" } } }, @@ -288,7 +290,7 @@ ], "properties": { "devnet": { - "$ref": "#/components/schemas/StacksChainhookNetworkSpecification" + "$ref": "#/components/schemas/StacksChainhookSpecification" } } }, @@ -299,7 +301,7 @@ ], "properties": { "testnet": { - "$ref": "#/components/schemas/StacksChainhookNetworkSpecification" + "$ref": "#/components/schemas/StacksChainhookSpecification" } } }, @@ -310,7 +312,7 @@ ], "properties": { "mainnet": { - "$ref": "#/components/schemas/StacksChainhookNetworkSpecification" + "$ref": "#/components/schemas/StacksChainhookSpecification" } } } @@ -329,7 +331,7 @@ "mainnet" ] }, - "BitcoinChainhookNetworkSpecification": { + "BitcoinChainhookSpecification": { "type": "object", "required": [ "if_this", @@ -857,7 +859,7 @@ "mainnet" ] }, - "StacksChainhookNetworkSpecification": { + "StacksChainhookSpecification": { "type": "object", "required": [ "if_this",