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