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