use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::marker::PhantomData;
pub use arete_idl::snapshot::*;
pub const CURRENT_AST_VERSION: &str = "0.0.4";
pub const COMPATIBLE_AST_VERSIONS: &[&str] = &["0.0.1", "0.0.2", "0.0.3"];
fn default_ast_version() -> String {
CURRENT_AST_VERSION.to_string()
}
pub fn idl_type_snapshot_to_rust_string(ty: &IdlTypeSnapshot) -> String {
match ty {
IdlTypeSnapshot::Simple(s) => map_simple_idl_type(s),
IdlTypeSnapshot::Array(arr) => {
if arr.array.len() == 2 {
match (&arr.array[0], &arr.array[1]) {
(IdlArrayElementSnapshot::TypeName(t), IdlArrayElementSnapshot::Size(size)) => {
format!("[{}; {}]", map_simple_idl_type(t), size)
}
(
IdlArrayElementSnapshot::Type(nested),
IdlArrayElementSnapshot::Size(size),
) => {
format!("[{}; {}]", idl_type_snapshot_to_rust_string(nested), size)
}
_ => "Vec<u8>".to_string(),
}
} else {
"Vec<u8>".to_string()
}
}
IdlTypeSnapshot::Option(opt) => {
format!("Option<{}>", idl_type_snapshot_to_rust_string(&opt.option))
}
IdlTypeSnapshot::Vec(vec) => {
format!("Vec<{}>", idl_type_snapshot_to_rust_string(&vec.vec))
}
IdlTypeSnapshot::HashMap(map) => {
let key_type = idl_type_snapshot_to_rust_string(&map.hash_map.0);
let val_type = idl_type_snapshot_to_rust_string(&map.hash_map.1);
format!("std::collections::HashMap<{}, {}>", key_type, val_type)
}
IdlTypeSnapshot::Defined(def) => match &def.defined {
IdlDefinedInnerSnapshot::Named { name } => name.clone(),
IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
},
}
}
fn map_simple_idl_type(idl_type: &str) -> String {
match idl_type {
"u8" => "u8".to_string(),
"u16" => "u16".to_string(),
"u32" => "u32".to_string(),
"u64" => "u64".to_string(),
"u128" => "u128".to_string(),
"i8" => "i8".to_string(),
"i16" => "i16".to_string(),
"i32" => "i32".to_string(),
"i64" => "i64".to_string(),
"i128" => "i128".to_string(),
"bool" => "bool".to_string(),
"string" => "String".to_string(),
"publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
"bytes" => "Vec<u8>".to_string(),
_ => idl_type.to_string(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FieldPath {
pub segments: Vec<String>,
pub offsets: Option<Vec<usize>>,
}
impl FieldPath {
pub fn new(segments: &[&str]) -> Self {
FieldPath {
segments: segments.iter().map(|s| s.to_string()).collect(),
offsets: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Transformation {
HexEncode,
HexDecode,
Base58Encode,
Base58Decode,
ToString,
ToNumber,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PopulationStrategy {
SetOnce,
LastWrite,
Append,
Merge,
Max,
Sum,
Count,
Min,
UniqueCount,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComputedFieldSpec {
pub target_path: String,
pub expression: ComputedExpr,
pub result_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum ResolverType {
Token,
Url(UrlResolverConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "lowercase")]
pub enum HttpMethod {
#[default]
Get,
Post,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum UrlTemplatePart {
Literal(String),
FieldRef(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum UrlSource {
FieldPath(String),
Template(Vec<UrlTemplatePart>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct UrlResolverConfig {
pub url_source: UrlSource,
#[serde(default)]
pub method: HttpMethod,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extract_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ResolverExtractSpec {
pub target_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transform: Option<Transformation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum ResolveStrategy {
#[default]
SetOnce,
LastWrite,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ResolverCondition {
pub field_path: String,
pub op: ComparisonOp,
pub value: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolverSpec {
pub resolver: ResolverType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_value: Option<Value>,
#[serde(default)]
pub strategy: ResolveStrategy,
pub extracts: Vec<ResolverExtractSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub condition: Option<ResolverCondition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schedule_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComputedExpr {
FieldRef {
path: String,
},
UnwrapOr {
expr: Box<ComputedExpr>,
default: serde_json::Value,
},
Binary {
op: BinaryOp,
left: Box<ComputedExpr>,
right: Box<ComputedExpr>,
},
Cast {
expr: Box<ComputedExpr>,
to_type: String,
},
MethodCall {
expr: Box<ComputedExpr>,
method: String,
args: Vec<ComputedExpr>,
},
ResolverComputed {
resolver: String,
method: String,
args: Vec<ComputedExpr>,
},
Literal {
value: serde_json::Value,
},
Paren {
expr: Box<ComputedExpr>,
},
Var {
name: String,
},
Let {
name: String,
value: Box<ComputedExpr>,
body: Box<ComputedExpr>,
},
If {
condition: Box<ComputedExpr>,
then_branch: Box<ComputedExpr>,
else_branch: Box<ComputedExpr>,
},
None,
Some {
value: Box<ComputedExpr>,
},
Slice {
expr: Box<ComputedExpr>,
start: usize,
end: usize,
},
Index {
expr: Box<ComputedExpr>,
index: usize,
},
U64FromLeBytes {
bytes: Box<ComputedExpr>,
},
U64FromBeBytes {
bytes: Box<ComputedExpr>,
},
ByteArray {
bytes: Vec<u8>,
},
Closure {
param: String,
body: Box<ComputedExpr>,
},
Unary {
op: UnaryOp,
expr: Box<ComputedExpr>,
},
JsonToBytes {
expr: Box<ComputedExpr>,
},
ContextSlot,
ContextTimestamp,
Keccak256 {
expr: Box<ComputedExpr>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BinaryOp {
Add,
Sub,
Mul,
Div,
Mod,
Gt,
Lt,
Gte,
Lte,
Eq,
Ne,
And,
Or,
Xor,
BitAnd,
BitOr,
Shl,
Shr,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UnaryOp {
Not,
ReverseBits,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableStreamSpec {
#[serde(default = "default_ast_version")]
pub ast_version: String,
pub state_name: String,
#[serde(default)]
pub program_id: Option<String>,
#[serde(default)]
pub idl: Option<IdlSnapshot>,
pub identity: IdentitySpec,
pub handlers: Vec<SerializableHandlerSpec>,
pub sections: Vec<EntitySection>,
pub field_mappings: BTreeMap<String, FieldTypeInfo>,
pub resolver_hooks: Vec<ResolverHook>,
pub instruction_hooks: Vec<InstructionHook>,
#[serde(default)]
pub resolver_specs: Vec<ResolverSpec>,
#[serde(default)]
pub computed_fields: Vec<String>,
#[serde(default)]
pub computed_field_specs: Vec<ComputedFieldSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
#[serde(default)]
pub views: Vec<ViewDef>,
}
impl SerializableStreamSpec {
pub fn normalize_event_names(&mut self) {
for handler in &mut self.handlers {
handler.normalize_event_names();
}
for hook in &mut self.instruction_hooks {
hook.instruction_type =
crate::event_type_helpers::canonicalize_event_type_name(&hook.instruction_type);
}
}
}
#[derive(Debug, Clone)]
pub struct TypedStreamSpec<S> {
pub state_name: String,
pub identity: IdentitySpec,
pub handlers: Vec<TypedHandlerSpec<S>>,
pub sections: Vec<EntitySection>, pub field_mappings: BTreeMap<String, FieldTypeInfo>, pub resolver_hooks: Vec<ResolverHook>, pub instruction_hooks: Vec<InstructionHook>, pub resolver_specs: Vec<ResolverSpec>,
pub computed_fields: Vec<String>, _phantom: PhantomData<S>,
}
impl<S> TypedStreamSpec<S> {
pub fn new(
state_name: String,
identity: IdentitySpec,
handlers: Vec<TypedHandlerSpec<S>>,
) -> Self {
TypedStreamSpec {
state_name,
identity,
handlers,
sections: Vec::new(),
field_mappings: BTreeMap::new(),
resolver_hooks: Vec::new(),
instruction_hooks: Vec::new(),
resolver_specs: Vec::new(),
computed_fields: Vec::new(),
_phantom: PhantomData,
}
}
pub fn with_type_info(
state_name: String,
identity: IdentitySpec,
handlers: Vec<TypedHandlerSpec<S>>,
sections: Vec<EntitySection>,
field_mappings: BTreeMap<String, FieldTypeInfo>,
) -> Self {
TypedStreamSpec {
state_name,
identity,
handlers,
sections,
field_mappings,
resolver_hooks: Vec::new(),
instruction_hooks: Vec::new(),
resolver_specs: Vec::new(),
computed_fields: Vec::new(),
_phantom: PhantomData,
}
}
pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
self.resolver_specs = resolver_specs;
self
}
pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
self.field_mappings.get(path)
}
pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
self.sections
.iter()
.find(|s| s.name == section_name)
.map(|s| &s.fields)
}
pub fn get_section_names(&self) -> Vec<&String> {
self.sections.iter().map(|s| &s.name).collect()
}
pub fn to_serializable(&self) -> SerializableStreamSpec {
let mut spec = SerializableStreamSpec {
ast_version: CURRENT_AST_VERSION.to_string(),
state_name: self.state_name.clone(),
program_id: None,
idl: None,
identity: self.identity.clone(),
handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
sections: self.sections.clone(),
field_mappings: self.field_mappings.clone(),
resolver_hooks: self.resolver_hooks.clone(),
instruction_hooks: self.instruction_hooks.clone(),
resolver_specs: self.resolver_specs.clone(),
computed_fields: self.computed_fields.clone(),
computed_field_specs: Vec::new(),
content_hash: None,
views: Vec::new(),
};
spec.content_hash = Some(spec.compute_content_hash());
spec
}
pub fn from_serializable(mut spec: SerializableStreamSpec) -> Self {
spec.normalize_event_names();
TypedStreamSpec {
state_name: spec.state_name,
identity: spec.identity,
handlers: spec
.handlers
.into_iter()
.map(|h| TypedHandlerSpec::from_serializable(h))
.collect(),
sections: spec.sections,
field_mappings: spec.field_mappings,
resolver_hooks: spec.resolver_hooks,
instruction_hooks: spec.instruction_hooks,
resolver_specs: spec.resolver_specs,
computed_fields: spec.computed_fields,
_phantom: PhantomData,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentitySpec {
pub primary_keys: Vec<String>,
pub lookup_indexes: Vec<LookupIndexSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LookupIndexSpec {
pub field_name: String,
pub temporal_field: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolverHook {
pub account_type: String,
pub strategy: ResolverStrategy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ResolverStrategy {
PdaReverseLookup {
lookup_name: String,
queue_discriminators: Vec<Vec<u8>>,
},
DirectField { field_path: FieldPath },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstructionHook {
pub instruction_type: String,
pub actions: Vec<HookAction>,
pub lookup_by: Option<FieldPath>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HookAction {
RegisterPdaMapping {
pda_field: FieldPath,
seed_field: FieldPath,
lookup_name: String,
},
SetField {
target_field: String,
source: MappingSource,
condition: Option<ConditionExpr>,
},
IncrementField {
target_field: String,
increment_by: i64,
condition: Option<ConditionExpr>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConditionExpr {
pub expression: String,
pub parsed: Option<ParsedCondition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ParsedCondition {
Comparison {
field: FieldPath,
op: ComparisonOp,
value: serde_json::Value,
},
Logical {
op: LogicalOp,
conditions: Vec<ParsedCondition>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ComparisonOp {
Equal,
NotEqual,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LogicalOp {
And,
Or,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableHandlerSpec {
pub source: SourceSpec,
pub key_resolution: KeyResolutionStrategy,
pub mappings: Vec<SerializableFieldMapping>,
pub conditions: Vec<Condition>,
pub emit: bool,
}
impl SerializableHandlerSpec {
pub fn normalize_event_names(&mut self) {
let SourceSpec::Source { type_name, .. } = &mut self.source;
*type_name = crate::event_type_helpers::canonicalize_event_type_name(type_name);
for mapping in &mut self.mappings {
if let Some(when) = &mut mapping.when {
*when = crate::event_type_helpers::canonicalize_event_type_name(when);
}
if let Some(stop) = &mut mapping.stop {
*stop = crate::event_type_helpers::canonicalize_event_type_name(stop);
}
}
}
}
#[derive(Debug, Clone)]
pub struct TypedHandlerSpec<S> {
pub source: SourceSpec,
pub key_resolution: KeyResolutionStrategy,
pub mappings: Vec<TypedFieldMapping<S>>,
pub conditions: Vec<Condition>,
pub emit: bool,
_phantom: PhantomData<S>,
}
impl<S> TypedHandlerSpec<S> {
pub fn new(
source: SourceSpec,
key_resolution: KeyResolutionStrategy,
mappings: Vec<TypedFieldMapping<S>>,
emit: bool,
) -> Self {
TypedHandlerSpec {
source,
key_resolution,
mappings,
conditions: vec![],
emit,
_phantom: PhantomData,
}
}
pub fn to_serializable(&self) -> SerializableHandlerSpec {
SerializableHandlerSpec {
source: self.source.clone(),
key_resolution: self.key_resolution.clone(),
mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
conditions: self.conditions.clone(),
emit: self.emit,
}
}
pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
TypedHandlerSpec {
source: spec.source,
key_resolution: spec.key_resolution,
mappings: spec
.mappings
.into_iter()
.map(|m| TypedFieldMapping::from_serializable(m))
.collect(),
conditions: spec.conditions,
emit: spec.emit,
_phantom: PhantomData,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KeyResolutionStrategy {
Embedded {
primary_field: FieldPath,
},
Lookup {
primary_field: FieldPath,
},
Computed {
primary_field: FieldPath,
compute_partition: ComputeFunction,
},
TemporalLookup {
lookup_field: FieldPath,
timestamp_field: FieldPath,
index_name: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SourceSpec {
Source {
program_id: Option<String>,
discriminator: Option<Vec<u8>>,
type_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
serialization: Option<IdlSerializationSnapshot>,
#[serde(default)]
is_account: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableFieldMapping {
pub target_path: String,
pub source: MappingSource,
pub transform: Option<Transformation>,
pub population: PopulationStrategy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub condition: Option<ConditionExpr>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub when: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop: Option<String>,
#[serde(default = "default_emit", skip_serializing_if = "is_true")]
pub emit: bool,
}
fn default_emit() -> bool {
true
}
fn default_instruction_discriminant_size() -> usize {
8
}
fn is_true(value: &bool) -> bool {
*value
}
#[derive(Debug, Clone)]
pub struct TypedFieldMapping<S> {
pub target_path: String,
pub source: MappingSource,
pub transform: Option<Transformation>,
pub population: PopulationStrategy,
pub condition: Option<ConditionExpr>,
pub when: Option<String>,
pub stop: Option<String>,
pub emit: bool,
_phantom: PhantomData<S>,
}
impl<S> TypedFieldMapping<S> {
pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
TypedFieldMapping {
target_path,
source,
transform: None,
population,
condition: None,
when: None,
stop: None,
emit: true,
_phantom: PhantomData,
}
}
pub fn with_transform(mut self, transform: Transformation) -> Self {
self.transform = Some(transform);
self
}
pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
self.condition = Some(condition);
self
}
pub fn with_when(mut self, when: String) -> Self {
self.when = Some(when);
self
}
pub fn with_stop(mut self, stop: String) -> Self {
self.stop = Some(stop);
self
}
pub fn with_emit(mut self, emit: bool) -> Self {
self.emit = emit;
self
}
pub fn to_serializable(&self) -> SerializableFieldMapping {
SerializableFieldMapping {
target_path: self.target_path.clone(),
source: self.source.clone(),
transform: self.transform.clone(),
population: self.population.clone(),
condition: self.condition.clone(),
when: self.when.clone(),
stop: self.stop.clone(),
emit: self.emit,
}
}
pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
TypedFieldMapping {
target_path: mapping.target_path,
source: mapping.source,
transform: mapping.transform,
population: mapping.population,
condition: mapping.condition,
when: mapping.when,
stop: mapping.stop,
emit: mapping.emit,
_phantom: PhantomData,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MappingSource {
FromSource {
path: FieldPath,
default: Option<Value>,
transform: Option<Transformation>,
},
Constant(Value),
Computed {
inputs: Vec<FieldPath>,
function: ComputeFunction,
},
FromState {
path: String,
},
AsEvent {
fields: Vec<Box<MappingSource>>,
},
WholeSource,
AsCapture {
field_transforms: BTreeMap<String, Transformation>,
},
FromContext {
field: String,
},
}
impl MappingSource {
pub fn with_transform(self, transform: Transformation) -> Self {
match self {
MappingSource::FromSource {
path,
default,
transform: _,
} => MappingSource::FromSource {
path,
default,
transform: Some(transform),
},
other => other,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComputeFunction {
Sum,
Concat,
Format(String),
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Condition {
pub field: FieldPath,
pub operator: ConditionOp,
pub value: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConditionOp {
Equals,
NotEquals,
GreaterThan,
LessThan,
Contains,
Exists,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldTypeInfo {
pub field_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canonical_name: Option<String>,
pub rust_type_name: String, pub base_type: BaseType, #[serde(default, skip_serializing_if = "Option::is_none")]
pub integer_kind: Option<IntegerKind>,
pub is_optional: bool, pub is_array: bool, pub inner_type: Option<String>, pub source_path: Option<String>, #[serde(default)]
pub resolved_type: Option<ResolvedStructType>,
#[serde(default = "default_emit", skip_serializing_if = "is_true")]
pub emit: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedStructType {
pub type_name: String,
pub fields: Vec<ResolvedField>,
pub is_instruction: bool,
pub is_account: bool,
pub is_event: bool,
#[serde(default)]
pub is_enum: bool,
#[serde(default)]
pub enum_variants: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedField {
pub field_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canonical_name: Option<String>,
pub field_type: String,
pub base_type: BaseType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub integer_kind: Option<IntegerKind>,
pub is_optional: bool,
pub is_array: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IntegerKind {
U8,
U16,
U32,
U64,
U128,
Usize,
I8,
I16,
I32,
I64,
I128,
Isize,
}
impl IntegerKind {
pub fn from_rust_type(type_str: &str) -> Option<Self> {
match type_str.trim() {
"u8" => Some(Self::U8),
"u16" => Some(Self::U16),
"u32" => Some(Self::U32),
"u64" => Some(Self::U64),
"u128" => Some(Self::U128),
"usize" => Some(Self::Usize),
"i8" => Some(Self::I8),
"i16" => Some(Self::I16),
"i32" => Some(Self::I32),
"i64" => Some(Self::I64),
"i128" => Some(Self::I128),
"isize" => Some(Self::Isize),
_ => None,
}
}
pub fn is_bigint(self) -> bool {
matches!(self, Self::U64 | Self::U128 | Self::I64 | Self::I128)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BaseType {
Integer, Float, String, Boolean, Object, Array, Binary, Timestamp, Pubkey, Any, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntitySection {
pub name: String,
pub fields: Vec<FieldTypeInfo>,
pub is_nested_struct: bool,
pub parent_field: Option<String>, }
impl FieldTypeInfo {
pub fn new(field_name: String, rust_type_name: String) -> Self {
let (base_type, integer_kind, is_optional, is_array, inner_type) =
Self::analyze_rust_type(&rust_type_name);
let canonical_name = to_camel_case_owned(&field_name);
FieldTypeInfo {
field_name: field_name.clone(),
raw_name: Some(field_name.clone()),
canonical_name: Some(canonical_name),
rust_type_name,
base_type: Self::infer_semantic_type(&field_name, base_type),
integer_kind,
is_optional,
is_array,
inner_type,
source_path: None,
resolved_type: None,
emit: true,
}
}
pub fn with_source_path(mut self, source_path: String) -> Self {
self.source_path = Some(source_path);
self
}
pub fn raw_field_name(&self) -> &str {
self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
}
pub fn canonical_field_name(&self) -> String {
self.canonical_name
.clone()
.unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
}
pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
self.integer_kind.or_else(|| {
IntegerKind::from_rust_type(
self.inner_type
.as_deref()
.unwrap_or(self.rust_type_name.as_str()),
)
})
}
fn analyze_rust_type(
rust_type: &str,
) -> (BaseType, Option<IntegerKind>, bool, bool, Option<String>) {
let type_str = rust_type.trim();
if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
let (inner_base_type, inner_integer_kind, _, inner_is_array, inner_inner_type) =
Self::analyze_rust_type(&inner);
return (
inner_base_type,
inner_integer_kind,
true,
inner_is_array,
inner_inner_type.or(Some(inner)),
);
}
if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
let (_inner_base_type, inner_integer_kind, inner_is_optional, _, inner_inner_type) =
Self::analyze_rust_type(&inner);
return (
BaseType::Array,
inner_integer_kind,
inner_is_optional,
true,
inner_inner_type.or(Some(inner)),
);
}
let integer_kind = IntegerKind::from_rust_type(type_str);
let base_type = match integer_kind {
Some(_) => BaseType::Integer,
None => match type_str {
"f32" | "f64" => BaseType::Float,
"bool" => BaseType::Boolean,
"String" | "&str" | "str" => BaseType::String,
"Value" | "serde_json::Value" => BaseType::Any,
"Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
_ => {
if type_str.contains("Bytes") || type_str.contains("bytes") {
BaseType::Binary
} else if type_str.contains("Pubkey") {
BaseType::Pubkey
} else {
BaseType::Object
}
}
},
};
(base_type, integer_kind, false, false, None)
}
fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
let pattern = format!("{}<", generic_name);
if type_str.starts_with(&pattern) && type_str.ends_with('>') {
let start = pattern.len();
let end = type_str.len() - 1;
if end > start {
return Some(type_str[start..end].trim().to_string());
}
}
None
}
fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
let lower_name = field_name.to_lowercase();
if base_type == BaseType::Integer
&& (lower_name.ends_with("_at")
|| lower_name.ends_with("_time")
|| lower_name.contains("timestamp")
|| lower_name.contains("created")
|| lower_name.contains("settled")
|| lower_name.contains("activated"))
{
return BaseType::Timestamp;
}
base_type
}
}
impl ResolvedField {
pub fn raw_field_name(&self) -> &str {
self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
}
pub fn canonical_field_name(&self) -> String {
self.canonical_name
.clone()
.unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
}
pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
self.integer_kind
.or_else(|| IntegerKind::from_rust_type(self.field_type.as_str()))
}
}
fn to_camel_case_owned(s: &str) -> String {
let mut result = String::new();
let mut uppercase_next = false;
for ch in s.chars() {
if matches!(ch, '_' | '-' | '.') {
uppercase_next = true;
continue;
}
if result.is_empty() {
result.extend(ch.to_lowercase());
continue;
}
if uppercase_next {
result.extend(ch.to_uppercase());
uppercase_next = false;
} else {
result.push(ch);
}
}
result
}
pub trait FieldAccessor<S> {
fn path(&self) -> String;
}
impl SerializableStreamSpec {
pub fn compute_content_hash(&self) -> String {
use sha2::{Digest, Sha256};
let mut spec_for_hash = self.clone();
spec_for_hash.content_hash = None;
let json =
serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
let mut hasher = Sha256::new();
hasher.update(json.as_bytes());
let result = hasher.finalize();
hex::encode(result)
}
pub fn verify_content_hash(&self) -> bool {
match &self.content_hash {
Some(hash) => {
let computed = self.compute_content_hash();
hash == &computed
}
None => true, }
}
pub fn with_content_hash(mut self) -> Self {
self.content_hash = Some(self.compute_content_hash());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PdaDefinition {
pub name: String,
pub seeds: Vec<PdaSeedDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub program_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PdaSeedDef {
Literal { value: String },
Bytes { value: Vec<u8> },
ArgRef {
arg_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
arg_type: Option<String>,
},
AccountRef { account_name: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "category", rename_all = "camelCase")]
pub enum AccountResolution {
Signer,
Known { address: String },
PdaRef { pda_name: String },
PdaInline {
seeds: Vec<PdaSeedDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
program_id: Option<String>,
},
UserProvided,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InstructionAccountDef {
pub name: String,
#[serde(default)]
pub is_signer: bool,
#[serde(default)]
pub is_writable: bool,
pub resolution: AccountResolution,
#[serde(default)]
pub is_optional: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub docs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct InstructionAmountHint {
pub decimals_source: AmountDecimalsSource,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(
tag = "kind",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
pub enum AmountDecimalsSource {
ArgMint { arg_name: String },
ArgDecimals { arg_name: String },
KnownAccount { account_name: String },
Constant { decimals: u8 },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InstructionArgDef {
pub name: String,
#[serde(rename = "type")]
pub arg_type: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub docs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub amount_hint: Option<InstructionAmountHint>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InstructionDef {
pub name: String,
pub discriminator: Vec<u8>,
#[serde(default = "default_instruction_discriminant_size")]
pub discriminator_size: usize,
pub accounts: Vec<InstructionAccountDef>,
pub args: Vec<InstructionArgDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<IdlErrorSnapshot>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub program_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub docs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableStackSpec {
#[serde(default = "default_ast_version")]
pub ast_version: String,
pub stack_name: String,
#[serde(default)]
pub program_ids: Vec<String>,
#[serde(default)]
pub idls: Vec<IdlSnapshot>,
pub entities: Vec<SerializableStreamSpec>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub instructions: Vec<InstructionDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
}
impl SerializableStackSpec {
pub fn normalize_event_names(&mut self) {
for entity in &mut self.entities {
entity.normalize_event_names();
}
}
pub fn compute_content_hash(&self) -> String {
use sha2::{Digest, Sha256};
let mut spec_for_hash = self.clone();
spec_for_hash.content_hash = None;
let json = serde_json::to_string(&spec_for_hash)
.expect("Failed to serialize stack spec for hashing");
let mut hasher = Sha256::new();
hasher.update(json.as_bytes());
hex::encode(hasher.finalize())
}
pub fn with_content_hash(mut self) -> Self {
self.content_hash = Some(self.compute_content_hash());
self
}
}
#[cfg(test)]
mod tests {
use super::{
HookAction, IdentitySpec, InstructionHook, MappingSource, PopulationStrategy,
SerializableFieldMapping, SerializableHandlerSpec, SerializableStackSpec,
SerializableStreamSpec, SourceSpec, TypedStreamSpec, CURRENT_AST_VERSION,
};
use serde_json::Value;
fn legacy_stream_spec() -> SerializableStreamSpec {
SerializableStreamSpec {
ast_version: CURRENT_AST_VERSION.to_string(),
state_name: "PumpfunToken".to_string(),
program_id: None,
idl: None,
identity: IdentitySpec {
primary_keys: vec!["id.mint".to_string()],
lookup_indexes: vec![],
},
handlers: vec![SerializableHandlerSpec {
source: SourceSpec::Source {
program_id: None,
discriminator: None,
type_name: "pump::buyIxState".to_string(),
serialization: None,
is_account: false,
},
key_resolution: super::KeyResolutionStrategy::Embedded {
primary_field: super::FieldPath::new(&["accounts", "mint"]),
},
mappings: vec![SerializableFieldMapping {
target_path: "info.last_buy_at".to_string(),
source: MappingSource::Constant(Value::Null),
transform: None,
population: PopulationStrategy::SetOnce,
condition: None,
when: Some("pump::sellIxState".to_string()),
stop: Some("pump::buy_exact_sol_inIxState".to_string()),
emit: true,
}],
conditions: vec![],
emit: true,
}],
sections: vec![],
field_mappings: Default::default(),
resolver_hooks: vec![],
instruction_hooks: vec![InstructionHook {
instruction_type: "pump::buyIxState".to_string(),
actions: vec![HookAction::SetField {
target_field: "info.last_buy_at".to_string(),
source: MappingSource::Constant(Value::Null),
condition: None,
}],
lookup_by: None,
}],
resolver_specs: vec![],
computed_fields: vec![],
computed_field_specs: vec![],
content_hash: None,
views: vec![],
}
}
#[test]
fn typed_stream_spec_from_serializable_normalizes_legacy_instruction_event_names() {
let typed = TypedStreamSpec::<Value>::from_serializable(legacy_stream_spec());
let handler = &typed.handlers[0];
let SourceSpec::Source { type_name, .. } = &handler.source;
assert_eq!(type_name, "pump::BuyIxState");
assert_eq!(
handler.mappings[0].when.as_deref(),
Some("pump::SellIxState")
);
assert_eq!(
handler.mappings[0].stop.as_deref(),
Some("pump::BuyExactSolInIxState")
);
assert_eq!(
typed.instruction_hooks[0].instruction_type,
"pump::BuyIxState"
);
}
#[test]
fn stack_spec_normalize_event_names_updates_all_entities() {
let mut stack = SerializableStackSpec {
ast_version: CURRENT_AST_VERSION.to_string(),
stack_name: "PumpStack".to_string(),
program_ids: vec![],
idls: vec![],
entities: vec![legacy_stream_spec()],
pdas: Default::default(),
instructions: vec![],
content_hash: None,
};
stack.normalize_event_names();
let SourceSpec::Source { type_name, .. } = &stack.entities[0].handlers[0].source;
assert_eq!(type_name, "pump::BuyIxState");
assert_eq!(
stack.entities[0].instruction_hooks[0].instruction_type,
"pump::BuyIxState"
);
}
#[test]
fn field_type_info_recognizes_128_bit_integers() {
let unsigned = super::FieldTypeInfo::new("amount".to_string(), "u128".to_string());
assert_eq!(unsigned.base_type, super::BaseType::Integer);
assert!(!unsigned.is_optional);
assert_eq!(unsigned.integer_kind, Some(super::IntegerKind::U128));
assert_eq!(unsigned.raw_field_name(), "amount");
assert_eq!(unsigned.canonical_field_name(), "amount");
let signed = super::FieldTypeInfo::new("delta".to_string(), "Option<i128>".to_string());
assert_eq!(signed.base_type, super::BaseType::Integer);
assert!(signed.is_optional);
assert_eq!(signed.integer_kind, Some(super::IntegerKind::I128));
assert_eq!(signed.inner_type.as_deref(), Some("i128"));
}
#[test]
fn field_type_info_preserves_timestamp_integer_kind_and_names() {
let timestamp =
super::FieldTypeInfo::new("last_updated_at".to_string(), "Option<i64>".to_string());
assert_eq!(timestamp.base_type, super::BaseType::Timestamp);
assert_eq!(timestamp.integer_kind, Some(super::IntegerKind::I64));
assert!(timestamp.is_optional);
assert_eq!(timestamp.raw_field_name(), "last_updated_at");
assert_eq!(timestamp.canonical_field_name(), "lastUpdatedAt");
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SortOrder {
#[default]
Asc,
Desc,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum CompareOp {
Eq,
Ne,
Gt,
Gte,
Lt,
Lte,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum PredicateValue {
Literal(serde_json::Value),
Dynamic(String),
Field(FieldPath),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Predicate {
Compare {
field: FieldPath,
op: CompareOp,
value: PredicateValue,
},
And(Vec<Predicate>),
Or(Vec<Predicate>),
Not(Box<Predicate>),
Exists { field: FieldPath },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ViewTransform {
Filter { predicate: Predicate },
Sort {
key: FieldPath,
#[serde(default)]
order: SortOrder,
},
Take { count: usize },
Skip { count: usize },
First,
Last,
MaxBy { key: FieldPath },
MinBy { key: FieldPath },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ViewSource {
Entity { name: String },
View { id: String },
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub enum ViewOutput {
#[default]
Collection,
Single,
Keyed { key_field: FieldPath },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ViewDef {
pub id: String,
pub source: ViewSource,
#[serde(default)]
pub pipeline: Vec<ViewTransform>,
#[serde(default)]
pub output: ViewOutput,
}
impl ViewDef {
pub fn list(entity_name: &str) -> Self {
ViewDef {
id: format!("{}/list", entity_name),
source: ViewSource::Entity {
name: entity_name.to_string(),
},
pipeline: vec![],
output: ViewOutput::Collection,
}
}
pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
ViewDef {
id: format!("{}/state", entity_name),
source: ViewSource::Entity {
name: entity_name.to_string(),
},
pipeline: vec![],
output: ViewOutput::Keyed {
key_field: FieldPath::new(key_field),
},
}
}
pub fn is_single(&self) -> bool {
matches!(self.output, ViewOutput::Single)
}
pub fn has_single_transform(&self) -> bool {
self.pipeline.iter().any(|t| {
matches!(
t,
ViewTransform::First
| ViewTransform::Last
| ViewTransform::MaxBy { .. }
| ViewTransform::MinBy { .. }
)
})
}
}
#[macro_export]
macro_rules! define_accessor {
($name:ident, $state:ty, $path:expr) => {
pub struct $name;
impl $crate::ast::FieldAccessor<$state> for $name {
fn path(&self) -> String {
$path.to_string()
}
}
};
}