From 611c79cee35de63a1b8ca623d676d186dc86d244 Mon Sep 17 00:00:00 2001 From: Ludo Galabru Date: Fri, 10 Feb 2023 00:11:41 -0500 Subject: [PATCH] feat: introduce new predicate + refactor schemas --- components/chainhook-cli/src/cli/mod.rs | 126 +++++++++++------- .../src/chainhooks/bitcoin/mod.rs | 110 +++++++-------- .../src/chainhooks/types.rs | 106 ++++++++------- .../src/observer/tests/mod.rs | 11 +- components/chainhook-types-rs/src/rosetta.rs | 17 +++ 5 files changed, 220 insertions(+), 150 deletions(-) diff --git a/components/chainhook-cli/src/cli/mod.rs b/components/chainhook-cli/src/cli/mod.rs index 7b67beb4..1789cf4d 100644 --- a/components/chainhook-cli/src/cli/mod.rs +++ b/components/chainhook-cli/src/cli/mod.rs @@ -47,32 +47,45 @@ struct Opts { #[derive(Subcommand, PartialEq, Clone, Debug)] enum Command { + /// Manage predicates + #[clap(subcommand)] + Predicates(PredicatesCommand), /// Start chainhook-cli #[clap(subcommand)] Node(NodeCommand), /// Start chainhook-cli in replay mode #[clap(name = "replay", bin_name = "replay")] Replay(ReplayCommand), +} + +#[derive(Subcommand, PartialEq, Clone, Debug)] +#[clap(bin_name = "predicate", aliases = &["predicate"])] +enum PredicatesCommand { + /// Generate new predicate + #[clap(name = "new", bin_name = "new", aliases = &["generate"])] + New(NewPredicate), /// Scan blocks (one-off) from specified network and apply provided predicate #[clap(name = "scan", bin_name = "scan")] - Scan(ScanCommand), + Scan(ScanPredicate), } -#[derive(Subcommand, PartialEq, Clone, Debug)] -enum NodeCommand { - /// Start chainhook-cli - #[clap(name = "start", bin_name = "start")] - Start(StartCommand), +#[derive(Parser, PartialEq, Clone, Debug)] +struct NewPredicate { + /// Predicate's name + pub name: String, + /// Path to Clarinet.toml + #[clap(long = "manifest-path")] + pub manifest_path: Option, + /// Generate a Bitcoin chainhook + #[clap(long = "bitcoin", conflicts_with = "stacks")] + pub bitcoin: bool, + /// Generate a Stacks chainhook + #[clap(long = "stacks", conflicts_with = "bitcoin")] + pub stacks: bool, } #[derive(Parser, PartialEq, Clone, Debug)] -struct StartCommand { - /// Target Devnet network - #[clap( - long = "devnet", - conflicts_with = "testnet", - conflicts_with = "mainnet" - )] +struct ScanPredicate { pub devnet: bool, /// Target Testnet network #[clap( @@ -96,10 +109,26 @@ struct StartCommand { conflicts_with = "devnet" )] pub config_path: Option, + /// Load chainhook file path (yaml format) + #[clap(long = "predicate-path", short = 'p')] + pub chainhook_spec_path: String, +} + +#[derive(Subcommand, PartialEq, Clone, Debug)] +enum NodeCommand { + /// Start chainhook-cli + #[clap(name = "start", bin_name = "start")] + Start(StartCommand), } #[derive(Parser, PartialEq, Clone, Debug)] -struct ReplayCommand { +struct StartCommand { + /// Target Devnet network + #[clap( + long = "devnet", + conflicts_with = "testnet", + conflicts_with = "mainnet" + )] pub devnet: bool, /// Target Testnet network #[clap( @@ -123,16 +152,10 @@ struct ReplayCommand { conflicts_with = "devnet" )] pub config_path: Option, - /// Apply chainhook action (false by default) - #[clap(long = "apply-trigger")] - pub apply_trigger: bool, - /// Bitcoind node url override - #[clap(long = "bitcoind-rpc-url")] - pub bitcoind_rpc_url: Option, } #[derive(Parser, PartialEq, Clone, Debug)] -struct ScanCommand { +struct ReplayCommand { pub devnet: bool, /// Target Testnet network #[clap( @@ -156,12 +179,12 @@ struct ScanCommand { conflicts_with = "devnet" )] pub config_path: Option, - /// Load chainhook file path (yaml format) - #[clap( - long = "chainhook-spec-path", - short = 'p' - )] - pub chainhook_spec_path: String, + /// Apply chainhook action (false by default) + #[clap(long = "apply-trigger")] + pub apply_trigger: bool, + /// Bitcoind node url override + #[clap(long = "bitcoind-rpc-url")] + pub bitcoind_rpc_url: Option, } pub fn main() { @@ -194,6 +217,30 @@ pub fn main() { start_node(config, ctx); } }, + Command::Predicates(subcmd) => match subcmd { + PredicatesCommand::New(cmd) => { + // Predicates can either be generated manually by letting developers + // craft their own json payload, or using the interactive approach. + // A list of contracts is displayed, then list of methods, then list of events detected + // 3 files are generated: + // predicates/simnet/name.json + // predicates/devnet/name.json + // predicates/testnet/name.json + // predicates/mainnet/name.json + let manifest = clarinet_files::get_manifest_location(None); + } + PredicatesCommand::Scan(cmd) => { + let config = + match Config::default(cmd.devnet, cmd.testnet, cmd.mainnet, &cmd.config_path) { + Ok(config) => config, + Err(e) => { + println!("{e}"); + process::exit(1); + } + }; + start_scan(config, ctx); + } + }, Command::Replay(cmd) => { let mut config = match Config::default(cmd.devnet, cmd.testnet, cmd.mainnet, &cmd.config_path) { @@ -230,41 +277,28 @@ pub fn main() { } start_replay(config, cmd.apply_trigger, ctx); } - Command::Scan(cmd) => { - let config = - match Config::default(cmd.devnet, cmd.testnet, cmd.mainnet, &cmd.config_path) { - Ok(config) => config, - Err(e) => { - println!("{e}"); - process::exit(1); - } - }; - start_scan(config, ctx); - } } } - pub fn install_ctrlc_handler(terminate_tx: Sender, ctx: Context) { ctrlc::set_handler(move || { - warn!( - &ctx.expect_logger(), - "Manual interruption signal received" - ); + warn!(&ctx.expect_logger(), "Manual interruption signal received"); terminate_tx .send(DigestingCommand::Kill) .expect("Unable to terminate service"); - }).expect("Error setting Ctrl-C handler"); + }) + .expect("Error setting Ctrl-C handler"); } - pub fn download_dataset_if_required(config: &mut Config, ctx: &Context) -> bool { if config.is_initial_ingestion_required() { // Download default tsv. if config.rely_on_remote_tsv() && config.should_download_remote_tsv() { let url = config.expected_remote_tsv_url(); let mut destination_path = config.expected_cache_path(); - destination_path.push(archive::default_tsv_file_path(&config.network.stacks_network)); + destination_path.push(archive::default_tsv_file_path( + &config.network.stacks_network, + )); // Download archive if not already present in cache if !destination_path.exists() { info!(ctx.expect_logger(), "Downloading {}", url); diff --git a/components/chainhook-event-observer/src/chainhooks/bitcoin/mod.rs b/components/chainhook-event-observer/src/chainhooks/bitcoin/mod.rs index 46243908..15a87e42 100644 --- a/components/chainhook-event-observer/src/chainhooks/bitcoin/mod.rs +++ b/components/chainhook-event-observer/src/chainhooks/bitcoin/mod.rs @@ -1,7 +1,7 @@ use super::types::{ BitcoinChainhookSpecification, BitcoinPredicateType, ExactMatchingRule, HookAction, - KeyRegistrationPredicate, LockSTXPredicate, MatchingRule, PobPredicate, PoxPredicate, - TransferSTXPredicate, + InputPredicate, MatchingRule, OrdinalOperations, OutputPredicate, Protocols, Scopes, + StacksOperations, TxinPredicate, }; use base58::FromBase58; use bitcoincore_rpc::bitcoin::blockdata::opcodes; @@ -9,8 +9,8 @@ use bitcoincore_rpc::bitcoin::blockdata::script::Builder as BitcoinScriptBuilder use bitcoincore_rpc::bitcoin::util::address::Payload; use bitcoincore_rpc::bitcoin::Address; use chainhook_types::{ - BitcoinBlockData, BitcoinChainEvent, BitcoinTransactionData, StacksBaseChainOperation, - TransactionIdentifier, + BitcoinBlockData, BitcoinChainEvent, BitcoinTransactionData, OrdinalOperation, + StacksBaseChainOperation, TransactionIdentifier, }; use clarity_repl::clarity::util::hash::to_hex; use reqwest::{Client, Method}; @@ -144,6 +144,7 @@ pub fn serialize_bitcoin_payload_to_json<'a>( "inputs": transaction.metadata.inputs, "outputs": transaction.metadata.outputs, "stacks_operations": transaction.metadata.stacks_operations, + "ordinal_operations": transaction.metadata.ordinal_operations, "proof": proofs.get(&transaction.transaction_identifier), }), }) @@ -231,11 +232,13 @@ pub fn handle_bitcoin_hook_action<'a>( impl BitcoinChainhookSpecification { pub fn evaluate_transaction_predicate(&self, tx: &BitcoinTransactionData) -> bool { // TODO(lgalabru): follow-up on this implementation - match &self.predicate.kind { - BitcoinPredicateType::TransactionIdentifierHash(ExactMatchingRule::Equals(txid)) => { + match &self.predicate { + BitcoinPredicateType::Txid(ExactMatchingRule::Equals(txid)) => { tx.transaction_identifier.hash.eq(txid) } - BitcoinPredicateType::OpReturn(MatchingRule::Equals(hex_bytes)) => { + BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::OpReturn( + MatchingRule::Equals(hex_bytes), + ))) => { for output in tx.metadata.outputs.iter() { if output.script_pubkey.eq(hex_bytes) { return true; @@ -243,7 +246,9 @@ impl BitcoinChainhookSpecification { } false } - BitcoinPredicateType::OpReturn(MatchingRule::StartsWith(hex_bytes)) => { + BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::OpReturn( + MatchingRule::StartsWith(hex_bytes), + ))) => { for output in tx.metadata.outputs.iter() { if output.script_pubkey.starts_with(hex_bytes) { return true; @@ -251,7 +256,9 @@ impl BitcoinChainhookSpecification { } false } - BitcoinPredicateType::OpReturn(MatchingRule::EndsWith(hex_bytes)) => { + BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::OpReturn( + MatchingRule::EndsWith(hex_bytes), + ))) => { for output in tx.metadata.outputs.iter() { if output.script_pubkey.ends_with(hex_bytes) { return true; @@ -259,7 +266,9 @@ impl BitcoinChainhookSpecification { } false } - BitcoinPredicateType::P2pkh(ExactMatchingRule::Equals(address)) => { + BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2pkh( + ExactMatchingRule::Equals(address), + ))) => { let pubkey_hash = address .from_base58() .expect("Unable to get bytes from btc address"); @@ -278,7 +287,9 @@ impl BitcoinChainhookSpecification { } false } - BitcoinPredicateType::P2sh(ExactMatchingRule::Equals(address)) => { + BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2sh( + ExactMatchingRule::Equals(address), + ))) => { let script_hash = address .from_base58() .expect("Unable to get bytes from btc address"); @@ -295,8 +306,12 @@ impl BitcoinChainhookSpecification { } false } - BitcoinPredicateType::P2wpkh(ExactMatchingRule::Equals(encoded_address)) - | BitcoinPredicateType::P2wsh(ExactMatchingRule::Equals(encoded_address)) => { + BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2wpkh( + ExactMatchingRule::Equals(encoded_address), + ))) + | BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2wsh( + ExactMatchingRule::Equals(encoded_address), + ))) => { let address = match Address::from_str(encoded_address) { Ok(address) => match address.payload { Payload::WitnessProgram { @@ -315,79 +330,68 @@ impl BitcoinChainhookSpecification { } false } - BitcoinPredicateType::Pob(PobPredicate::Any) => { - for op in tx.metadata.stacks_operations.iter() { - if let StacksBaseChainOperation::PobBlockCommitment(_) = op { + BitcoinPredicateType::Scope(Scopes::Inputs(InputPredicate::Txid(predicate))) => { + // TODO(lgalabru): add support for transaction chainhing, if enabled + for input in tx.metadata.inputs.iter() { + if input.previous_output.txid.eq(&predicate.txid) + && input.previous_output.vout.eq(&predicate.vout) + { return true; } } false } - BitcoinPredicateType::Pox(PoxPredicate::Any) => { - for op in tx.metadata.stacks_operations.iter() { - if let StacksBaseChainOperation::PoxBlockCommitment(_) = op { - return true; - } - } - false + BitcoinPredicateType::Scope(Scopes::Inputs(InputPredicate::WitnessScript(_))) => { + // TODO(lgalabru) + unimplemented!() } - BitcoinPredicateType::Pox(PoxPredicate::Recipient(MatchingRule::Equals(address))) => { + BitcoinPredicateType::Protocol(Protocols::Stacks(StacksOperations::Pob)) => { for op in tx.metadata.stacks_operations.iter() { - if let StacksBaseChainOperation::PoxBlockCommitment(commitment) = op { - for reward in commitment.rewards.iter() { - if reward.recipient.eq(address) { - return true; - } - } + if let StacksBaseChainOperation::PobBlockCommitment(_) = op { + return true; } } false } - BitcoinPredicateType::Pox(PoxPredicate::Recipient(MatchingRule::StartsWith( - prefix, - ))) => { + BitcoinPredicateType::Protocol(Protocols::Stacks(StacksOperations::Pox)) => { for op in tx.metadata.stacks_operations.iter() { - if let StacksBaseChainOperation::PoxBlockCommitment(commitment) = op { - for reward in commitment.rewards.iter() { - if reward.recipient.starts_with(prefix) { - return true; - } - } + if let StacksBaseChainOperation::PoxBlockCommitment(_) = op { + return true; } } false } - BitcoinPredicateType::Pox(PoxPredicate::Recipient(MatchingRule::EndsWith(suffix))) => { + BitcoinPredicateType::Protocol(Protocols::Stacks( + StacksOperations::KeyRegistration, + )) => { for op in tx.metadata.stacks_operations.iter() { - if let StacksBaseChainOperation::PoxBlockCommitment(commitment) = op { - for reward in commitment.rewards.iter() { - if reward.recipient.ends_with(suffix) { - return true; - } - } + if let StacksBaseChainOperation::KeyRegistration(_) = op { + return true; } } false } - BitcoinPredicateType::KeyRegistration(KeyRegistrationPredicate::Any) => { + BitcoinPredicateType::Protocol(Protocols::Stacks(StacksOperations::TransferSTX)) => { for op in tx.metadata.stacks_operations.iter() { - if let StacksBaseChainOperation::KeyRegistration(_) = op { + if let StacksBaseChainOperation::TransferSTX(_) = op { return true; } } false } - BitcoinPredicateType::TransferSTX(TransferSTXPredicate::Any) => { + BitcoinPredicateType::Protocol(Protocols::Stacks(StacksOperations::LockSTX)) => { for op in tx.metadata.stacks_operations.iter() { - if let StacksBaseChainOperation::TransferSTX(_) = op { + if let StacksBaseChainOperation::LockSTX(_) = op { return true; } } false } - BitcoinPredicateType::LockSTX(LockSTXPredicate::Any) => { - for op in tx.metadata.stacks_operations.iter() { - if let StacksBaseChainOperation::LockSTX(_) = op { + BitcoinPredicateType::Protocol(Protocols::Ordinal( + OrdinalOperations::NewInscription, + )) => { + for op in tx.metadata.ordinal_operations.iter() { + if let OrdinalOperation::InscriptionReveal(_) = op { return true; } } diff --git a/components/chainhook-event-observer/src/chainhooks/types.rs b/components/chainhook-event-observer/src/chainhooks/types.rs index 3fe066d3..be8b3d87 100644 --- a/components/chainhook-event-observer/src/chainhooks/types.rs +++ b/components/chainhook-event-observer/src/chainhooks/types.rs @@ -39,7 +39,7 @@ impl ChainhookConfig { pub fn get_serialized_bitcoin_predicates( &self, - ) -> Vec<(&String, &BitcoinNetwork, &BitcoinTransactionFilterPredicate)> { + ) -> Vec<(&String, &BitcoinNetwork, &BitcoinPredicateType)> { let mut bitcoin = vec![]; for chainhook in self.bitcoin_chainhooks.iter() { bitcoin.push((&chainhook.uuid, &chainhook.network, &chainhook.predicate)); @@ -189,7 +189,7 @@ pub struct BitcoinChainhookSpecification { pub end_block: Option, #[serde(skip_serializing_if = "Option::is_none")] pub expire_after_occurrence: Option, - pub predicate: BitcoinTransactionFilterPredicate, + pub predicate: BitcoinPredicateType, pub action: HookAction, } @@ -276,42 +276,82 @@ impl ScriptTemplate { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct BitcoinTransactionFilterPredicate { - pub scope: Scope, - #[serde(flatten)] - pub kind: BitcoinPredicateType, + pub predicate: BitcoinPredicateType, } impl BitcoinTransactionFilterPredicate { - pub fn new(scope: Scope, kind: BitcoinPredicateType) -> BitcoinTransactionFilterPredicate { - BitcoinTransactionFilterPredicate { scope, kind } + pub fn new(predicate: BitcoinPredicateType) -> BitcoinTransactionFilterPredicate { + BitcoinTransactionFilterPredicate { predicate } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -#[serde(tag = "type", content = "rule")] pub enum BitcoinPredicateType { - TransactionIdentifierHash(ExactMatchingRule), + Txid(ExactMatchingRule), + Scope(Scopes), + Protocol(Protocols), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Scopes { + Inputs(InputPredicate), + Outputs(OutputPredicate), +} + +#[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), - Pox(PoxPredicate), - Pob(PobPredicate), - KeyRegistration(KeyRegistrationPredicate), - TransferSTX(TransferSTXPredicate), - LockSTX(LockSTXPredicate), } -pub fn get_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] { +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Protocols { + Stacks(StacksOperations), + Ordinal(OrdinalOperations), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum StacksOperations { + Pox, + Pob, + KeyRegistration, + TransferSTX, + LockSTX, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum OrdinalOperations { + NewInscription, +} + +pub fn get_stacks_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] { match network { - BitcoinNetwork::Mainnet => ['X' as u8, '2' as u8], - BitcoinNetwork::Testnet => ['T' as u8, '2' as u8], - BitcoinNetwork::Regtest => ['i' as u8, 'd' as u8], + BitcoinNetwork::Mainnet => *b"X2", + BitcoinNetwork::Testnet => *b"T2", + BitcoinNetwork::Regtest => *b"id", } } +pub fn get_ordinal_canonical_magic_bytes() -> (usize, [u8; 3]) { + return (37, *b"ord"); +} + pub struct PoxConfig { pub genesis_block_height: u64, pub prepare_phase_len: u64, @@ -386,33 +426,9 @@ impl TryFrom for StacksOpcodes { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum KeyRegistrationPredicate { - Any, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum TransferSTXPredicate { - Any, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum LockSTXPredicate { - Any, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum PoxPredicate { - Any, - Recipient(MatchingRule), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum PobPredicate { - Any, +pub struct TxinPredicate { + pub txid: String, + pub vout: u32, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] diff --git a/components/chainhook-event-observer/src/observer/tests/mod.rs b/components/chainhook-event-observer/src/observer/tests/mod.rs index 4df57a20..e835a7d9 100644 --- a/components/chainhook-event-observer/src/observer/tests/mod.rs +++ b/components/chainhook-event-observer/src/observer/tests/mod.rs @@ -1,7 +1,7 @@ use crate::chainhooks::types::{ BitcoinChainhookSpecification, BitcoinPredicateType, BitcoinTransactionFilterPredicate, - ChainhookConfig, ChainhookSpecification, ExactMatchingRule, HookAction, Scope, - StacksChainhookSpecification, StacksContractCallBasedPredicate, + ChainhookConfig, ChainhookSpecification, ExactMatchingRule, HookAction, OutputPredicate, Scope, + Scopes, StacksChainhookSpecification, StacksContractCallBasedPredicate, StacksTransactionFilterPredicate, }; use crate::indexer::tests::helpers::transactions::generate_test_tx_bitcoin_p2pkh_transfer; @@ -89,10 +89,9 @@ fn bitcoin_chainhook_p2pkh( start_block: None, end_block: None, expire_after_occurrence, - predicate: BitcoinTransactionFilterPredicate { - scope: Scope::Outputs, - kind: BitcoinPredicateType::P2pkh(ExactMatchingRule::Equals(address.to_string())), - }, + predicate: BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2pkh( + ExactMatchingRule::Equals(address.to_string()), + ))), action: HookAction::Noop, }; spec diff --git a/components/chainhook-types-rs/src/rosetta.rs b/components/chainhook-types-rs/src/rosetta.rs index 061793c4..9ef09255 100644 --- a/components/chainhook-types-rs/src/rosetta.rs +++ b/components/chainhook-types-rs/src/rosetta.rs @@ -268,9 +268,26 @@ pub struct BitcoinTransactionMetadata { pub inputs: Vec, pub outputs: Vec, pub stacks_operations: Vec, + pub ordinal_operations: Vec, pub proof: Option, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub enum OrdinalOperation { + InscriptionCommit(OrdinalInscriptionCommitData), + InscriptionReveal(OrdinalInscriptionRevealData), +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct OrdinalInscriptionCommitData {} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct OrdinalInscriptionRevealData { + pub satoshi_point: String, + pub content_type: String, + pub content: Vec, +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum StacksBaseChainOperation { PoxBlockCommitment(PoxBlockCommitmentData),