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