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(¤t_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}