carbon_core/
schema.rs

1//! Defines the structures and functions for constructing and matching
2//! transaction schemas in `carbon-core`.
3//!
4//! This module provides the `TransactionSchema`, `SchemaNode`, and
5//! `InstructionSchemaNode` types, enabling users to define and validate
6//! transactions against a specific schema. Transaction schemas can be nested,
7//! allowing for complex, multi-layered transaction structures that represent
8//! various instructions and sub-instructions.
9//!
10//! ## Key Components
11//!
12//! - **TransactionSchema**: Represents the overall schema for a transaction,
13//!   consisting of a collection of schema nodes at its root.
14//! - **SchemaNode**: A node in the schema that can either be an instruction
15//!   node or an `Any` node, allowing flexibility in matching instructions at
16//!   that level.
17//! - **InstructionSchemaNode**: Represents an instruction with its type, name,
18//!   and any nested inner instructions.
19//!
20//! ## Usage
21//!
22//! The `TransactionSchema` type provides methods to match a given transaction’s
23//! instructions against the schema and return a mapped representation of the
24//! data if it conforms to the schema. The `match_schema` and `match_nodes`
25//! methods allow for hierarchical matching and data extraction from
26//! transactions.
27//!
28//! ## Notes
29//!
30//! - **Schema Matching**: Schema matching is sequential, with `Any` nodes
31//!   providing flexibility in handling unknown instructions within
32//!   transactions. Each `InstructionSchemaNode` defines specific instructions
33//!   to be matched, allowing for strict validation where needed.
34//! - **Nested Instructions**: Instruction schemas can contain nested
35//!   instructions, enabling validation of complex transactions with inner
36//!   instructions.
37//! - **Data Conversion**: The `match_schema` method returns data as a
38//!   deserialized type using `serde_json`. Ensure that your expected output
39//!   type implements `DeserializeOwned`.
40
41use {
42    crate::{collection::InstructionDecoderCollection, instruction::DecodedInstruction},
43    serde::de::DeserializeOwned,
44    solana_instruction::AccountMeta,
45    solana_pubkey::Pubkey,
46    std::collections::HashMap,
47};
48
49/// Represents a node within a transaction schema, which can be either an
50/// `Instruction` node or an `Any` node to allow for flexible matching.
51#[derive(Debug, Clone)]
52pub enum SchemaNode<T: InstructionDecoderCollection> {
53    /// Represents a specific instruction type and its nested structure.
54    Instruction(InstructionSchemaNode<T>),
55    /// Matches any instruction type, providing flexibility within the schema.
56    Any,
57}
58
59/// Represents an instruction node within a schema, containing the instruction
60/// type, name, and optional nested instructions for further validation.
61#[derive(Debug, Clone)]
62pub struct InstructionSchemaNode<T: InstructionDecoderCollection> {
63    /// The type of the instruction, as defined by the associated collection.
64    pub ix_type: T::InstructionType,
65    /// A unique name identifier for the instruction node within the schema.
66    pub name: String,
67    /// A vector of nested schema nodes for matching nested instructions.
68    pub inner_instructions: Vec<SchemaNode<T>>,
69}
70
71/// Represents a parsed instruction, containing its program ID, decoded
72/// instruction data, and any nested instructions within the transaction.
73#[derive(Debug)]
74pub struct ParsedInstruction<T: InstructionDecoderCollection> {
75    /// The program ID associated with this instruction.
76    pub program_id: Pubkey,
77    /// The decoded instruction data.
78    pub instruction: DecodedInstruction<T>,
79    /// A vector of parsed nested instructions.
80    pub inner_instructions: Vec<ParsedInstruction<T>>,
81}
82
83/// Represents the schema for a transaction, defining the structure and expected
84/// instructions.
85///
86/// `TransactionSchema` allows you to define the structure of a transaction by
87/// specifying a list of `SchemaNode` elements at the root level. These nodes
88/// can represent specific instruction types or allow for flexibility with `Any`
89/// nodes. Nested instructions are supported to enable complex hierarchical
90/// schemas.
91///
92/// ## Methods
93///
94/// - `match_schema`: Attempts to match the transaction’s instructions against
95///   the schema, returning a deserialized representation of the data.
96/// - `match_nodes`: Matches the instructions against the schema nodes,
97///   returning a mapping of instruction names to data, if successful.
98#[derive(Debug, Clone)]
99pub struct TransactionSchema<T: InstructionDecoderCollection> {
100    pub root: Vec<SchemaNode<T>>,
101}
102
103impl<T: InstructionDecoderCollection> TransactionSchema<T> {
104    /// Matches the transaction's instructions against the schema and returns a
105    /// deserialized result.
106    ///
107    /// # Parameters
108    ///
109    /// - `instructions`: A slice of `ParsedInstruction` representing the
110    ///   instructions to be matched.
111    ///
112    /// # Returns
113    ///
114    /// An `Option<U>` containing the deserialized data if matching and
115    /// deserialization are successful. The U represents the expected output
116    /// type, manually made by the developer.
117    pub fn match_schema<U>(&self, instructions: &[ParsedInstruction<T>]) -> Option<U>
118    where
119        U: DeserializeOwned,
120    {
121        log::trace!(
122            "Schema::match_schema(self: {:?}, instructions: {:?})",
123            self,
124            instructions
125        );
126        let value = serde_json::to_value(self.match_nodes(instructions)).ok()?;
127
128        log::trace!("Schema::match_schema: deserializing value: {:?}", value);
129        serde_json::from_value::<U>(value).ok()
130    }
131
132    /// Matches the instructions against the schema nodes and returns a mapping
133    /// of instruction names to data.
134    ///
135    /// This method processes the instructions and checks them against the
136    /// schema nodes sequentially. If the instructions match, a `HashMap` of
137    /// instruction names to decoded data and associated accounts is returned.
138    ///
139    /// # Parameters
140    ///
141    /// - `instructions`: A slice of `ParsedInstruction` representing the
142    ///   instructions to be matched.
143    ///
144    /// # Returns
145    ///
146    /// An `Option<HashMap<String, (T, Vec<AccountMeta>)>>` containing the
147    /// matched instruction data, or `None` if the instructions do not match
148    /// the schema.
149    pub fn match_nodes(
150        &self,
151        instructions: &[ParsedInstruction<T>],
152    ) -> Option<HashMap<String, (T, Vec<AccountMeta>)>> {
153        log::trace!(
154            "Schema::match_nodes(self: {:?}, instructions: {:?})",
155            self,
156            instructions
157        );
158        let mut output = HashMap::<String, (T, Vec<AccountMeta>)>::new();
159
160        let mut node_index = 0;
161        let mut instruction_index = 0;
162
163        let mut any = false;
164
165        while let Some(node) = self.root.get(node_index) {
166            log::trace!(
167                "Schema::match_nodes: current node ({}): {:?}",
168                node_index,
169                node
170            );
171
172            if let SchemaNode::Any = node {
173                log::trace!("Schema::match_nodes: Any node detected, skipping");
174                any = true;
175                node_index += 1;
176                continue;
177            }
178
179            let mut matched = false;
180
181            while let Some(current_instruction) = instructions.get(instruction_index) {
182                log::trace!(
183                    "Schema::match_nodes: current instruction ({}): {:?}",
184                    instruction_index,
185                    current_instruction
186                );
187
188                let SchemaNode::Instruction(instruction_node) = node else {
189                    return None;
190                };
191
192                if current_instruction.instruction.data.get_type() != instruction_node.ix_type
193                    && !any
194                {
195                    log::trace!(
196                        "Schema::match_nodes: instruction type mismatch, returning (any = false)"
197                    );
198                    return None;
199                }
200
201                if current_instruction.instruction.data.get_type() != instruction_node.ix_type
202                    && any
203                {
204                    log::trace!(
205                        "Schema::match_nodes: instruction type mismatch, skipping (any = true)"
206                    );
207                    instruction_index += 1;
208                    continue;
209                }
210
211                output.insert(
212                    instruction_node.name.clone(),
213                    (
214                        current_instruction.instruction.data.clone(),
215                        current_instruction.instruction.accounts.clone(),
216                    ),
217                );
218
219                if !instruction_node.inner_instructions.is_empty() {
220                    let inner_output = TransactionSchema {
221                        root: instruction_node.inner_instructions.clone(),
222                    }
223                    .match_nodes(&current_instruction.inner_instructions)?;
224                    output = merge_hashmaps(output, inner_output);
225                }
226
227                log::trace!(
228                    "Schema::match_nodes: instruction matched, output: {:?}",
229                    output
230                );
231
232                instruction_index += 1;
233                node_index += 1;
234                any = false;
235                matched = true;
236                break;
237            }
238
239            if !matched {
240                log::trace!("Schema::match_nodes: node not matched, returning");
241                return None;
242            }
243        }
244
245        log::trace!("Schema::match_nodes: final output: {:?}", output);
246
247        Some(output)
248    }
249}
250
251/// Merges two hash maps containing instruction data and account information.
252///
253/// # Parameters
254///
255/// - `a`: The first `HashMap` to be merged.
256/// - `b`: The second `HashMap` to be merged.
257///
258/// # Returns
259///
260/// A new `HashMap` containing all elements from `a` and `b`. In the case of
261/// duplicate keys, values from `b` will overwrite those from `a`.
262pub fn merge_hashmaps<K, V>(
263    a: HashMap<K, (V, Vec<AccountMeta>)>,
264    b: HashMap<K, (V, Vec<AccountMeta>)>,
265) -> HashMap<K, (V, Vec<AccountMeta>)>
266where
267    K: std::cmp::Eq + std::hash::Hash,
268{
269    log::trace!("merge_hashmaps(a, b)");
270    let mut output = a;
271    for (key, value) in b {
272        output.insert(key, value);
273    }
274    output
275}