1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::marker::PhantomData;
5
6pub use arete_idl::snapshot::*;
7
8pub const CURRENT_AST_VERSION: &str = "0.0.2";
17
18pub const COMPATIBLE_AST_VERSIONS: &[&str] = &["0.0.1"];
21
22fn default_ast_version() -> String {
23 CURRENT_AST_VERSION.to_string()
24}
25
26pub fn idl_type_snapshot_to_rust_string(ty: &IdlTypeSnapshot) -> String {
27 match ty {
28 IdlTypeSnapshot::Simple(s) => map_simple_idl_type(s),
29 IdlTypeSnapshot::Array(arr) => {
30 if arr.array.len() == 2 {
31 match (&arr.array[0], &arr.array[1]) {
32 (IdlArrayElementSnapshot::TypeName(t), IdlArrayElementSnapshot::Size(size)) => {
33 format!("[{}; {}]", map_simple_idl_type(t), size)
34 }
35 (
36 IdlArrayElementSnapshot::Type(nested),
37 IdlArrayElementSnapshot::Size(size),
38 ) => {
39 format!("[{}; {}]", idl_type_snapshot_to_rust_string(nested), size)
40 }
41 _ => "Vec<u8>".to_string(),
42 }
43 } else {
44 "Vec<u8>".to_string()
45 }
46 }
47 IdlTypeSnapshot::Option(opt) => {
48 format!("Option<{}>", idl_type_snapshot_to_rust_string(&opt.option))
49 }
50 IdlTypeSnapshot::Vec(vec) => {
51 format!("Vec<{}>", idl_type_snapshot_to_rust_string(&vec.vec))
52 }
53 IdlTypeSnapshot::HashMap(map) => {
54 let key_type = idl_type_snapshot_to_rust_string(&map.hash_map.0);
55 let val_type = idl_type_snapshot_to_rust_string(&map.hash_map.1);
56 format!("std::collections::HashMap<{}, {}>", key_type, val_type)
57 }
58 IdlTypeSnapshot::Defined(def) => match &def.defined {
59 IdlDefinedInnerSnapshot::Named { name } => name.clone(),
60 IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
61 },
62 }
63}
64
65fn map_simple_idl_type(idl_type: &str) -> String {
66 match idl_type {
67 "u8" => "u8".to_string(),
68 "u16" => "u16".to_string(),
69 "u32" => "u32".to_string(),
70 "u64" => "u64".to_string(),
71 "u128" => "u128".to_string(),
72 "i8" => "i8".to_string(),
73 "i16" => "i16".to_string(),
74 "i32" => "i32".to_string(),
75 "i64" => "i64".to_string(),
76 "i128" => "i128".to_string(),
77 "bool" => "bool".to_string(),
78 "string" => "String".to_string(),
79 "publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
80 "bytes" => "Vec<u8>".to_string(),
81 _ => idl_type.to_string(),
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86pub struct FieldPath {
87 pub segments: Vec<String>,
88 pub offsets: Option<Vec<usize>>,
89}
90
91impl FieldPath {
92 pub fn new(segments: &[&str]) -> Self {
93 FieldPath {
94 segments: segments.iter().map(|s| s.to_string()).collect(),
95 offsets: None,
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101pub enum Transformation {
102 HexEncode,
103 HexDecode,
104 Base58Encode,
105 Base58Decode,
106 ToString,
107 ToNumber,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum PopulationStrategy {
112 SetOnce,
113 LastWrite,
114 Append,
115 Merge,
116 Max,
117 Sum,
119 Count,
121 Min,
123 UniqueCount,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ComputedFieldSpec {
135 pub target_path: String,
137 pub expression: ComputedExpr,
139 pub result_type: String,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
148#[serde(rename_all = "lowercase")]
149pub enum ResolverType {
150 Token,
151 Url(UrlResolverConfig),
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
155#[serde(rename_all = "lowercase")]
156pub enum HttpMethod {
157 #[default]
158 Get,
159 Post,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
163pub enum UrlTemplatePart {
164 Literal(String),
165 FieldRef(String),
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
169pub enum UrlSource {
170 FieldPath(String),
171 Template(Vec<UrlTemplatePart>),
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
175pub struct UrlResolverConfig {
176 pub url_source: UrlSource,
177 #[serde(default)]
178 pub method: HttpMethod,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub extract_path: Option<String>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
184pub struct ResolverExtractSpec {
185 pub target_path: String,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub source_path: Option<String>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub transform: Option<Transformation>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
193pub enum ResolveStrategy {
194 #[default]
195 SetOnce,
196 LastWrite,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
200pub struct ResolverCondition {
201 pub field_path: String,
202 pub op: ComparisonOp,
203 pub value: Value,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ResolverSpec {
208 pub resolver: ResolverType,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub input_path: Option<String>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub input_value: Option<Value>,
213 #[serde(default)]
214 pub strategy: ResolveStrategy,
215 pub extracts: Vec<ResolverExtractSpec>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub condition: Option<ResolverCondition>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub schedule_at: Option<String>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
232pub enum ComputedExpr {
233 FieldRef {
236 path: String,
237 },
238
239 UnwrapOr {
241 expr: Box<ComputedExpr>,
242 default: serde_json::Value,
243 },
244
245 Binary {
247 op: BinaryOp,
248 left: Box<ComputedExpr>,
249 right: Box<ComputedExpr>,
250 },
251
252 Cast {
254 expr: Box<ComputedExpr>,
255 to_type: String,
256 },
257
258 MethodCall {
260 expr: Box<ComputedExpr>,
261 method: String,
262 args: Vec<ComputedExpr>,
263 },
264
265 ResolverComputed {
267 resolver: String,
268 method: String,
269 args: Vec<ComputedExpr>,
270 },
271
272 Literal {
274 value: serde_json::Value,
275 },
276
277 Paren {
279 expr: Box<ComputedExpr>,
280 },
281
282 Var {
284 name: String,
285 },
286
287 Let {
289 name: String,
290 value: Box<ComputedExpr>,
291 body: Box<ComputedExpr>,
292 },
293
294 If {
296 condition: Box<ComputedExpr>,
297 then_branch: Box<ComputedExpr>,
298 else_branch: Box<ComputedExpr>,
299 },
300
301 None,
303 Some {
304 value: Box<ComputedExpr>,
305 },
306
307 Slice {
309 expr: Box<ComputedExpr>,
310 start: usize,
311 end: usize,
312 },
313 Index {
314 expr: Box<ComputedExpr>,
315 index: usize,
316 },
317
318 U64FromLeBytes {
320 bytes: Box<ComputedExpr>,
321 },
322 U64FromBeBytes {
323 bytes: Box<ComputedExpr>,
324 },
325
326 ByteArray {
328 bytes: Vec<u8>,
329 },
330
331 Closure {
333 param: String,
334 body: Box<ComputedExpr>,
335 },
336
337 Unary {
339 op: UnaryOp,
340 expr: Box<ComputedExpr>,
341 },
342
343 JsonToBytes {
345 expr: Box<ComputedExpr>,
346 },
347
348 ContextSlot,
351 ContextTimestamp,
353
354 Keccak256 {
357 expr: Box<ComputedExpr>,
358 },
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
363pub enum BinaryOp {
364 Add,
366 Sub,
367 Mul,
368 Div,
369 Mod,
370 Gt,
372 Lt,
373 Gte,
374 Lte,
375 Eq,
376 Ne,
377 And,
379 Or,
380 Xor,
382 BitAnd,
383 BitOr,
384 Shl,
385 Shr,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub enum UnaryOp {
391 Not,
392 ReverseBits,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct SerializableStreamSpec {
398 #[serde(default = "default_ast_version")]
401 pub ast_version: String,
402 pub state_name: String,
403 #[serde(default)]
405 pub program_id: Option<String>,
406 #[serde(default)]
408 pub idl: Option<IdlSnapshot>,
409 pub identity: IdentitySpec,
410 pub handlers: Vec<SerializableHandlerSpec>,
411 pub sections: Vec<EntitySection>,
412 pub field_mappings: BTreeMap<String, FieldTypeInfo>,
413 pub resolver_hooks: Vec<ResolverHook>,
414 pub instruction_hooks: Vec<InstructionHook>,
415 #[serde(default)]
416 pub resolver_specs: Vec<ResolverSpec>,
417 #[serde(default)]
419 pub computed_fields: Vec<String>,
420 #[serde(default)]
422 pub computed_field_specs: Vec<ComputedFieldSpec>,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub content_hash: Option<String>,
427 #[serde(default)]
429 pub views: Vec<ViewDef>,
430}
431
432impl SerializableStreamSpec {
433 pub fn normalize_event_names(&mut self) {
435 for handler in &mut self.handlers {
436 handler.normalize_event_names();
437 }
438
439 for hook in &mut self.instruction_hooks {
440 hook.instruction_type =
441 crate::event_type_helpers::canonicalize_event_type_name(&hook.instruction_type);
442 }
443 }
444}
445
446#[derive(Debug, Clone)]
447pub struct TypedStreamSpec<S> {
448 pub state_name: String,
449 pub identity: IdentitySpec,
450 pub handlers: Vec<TypedHandlerSpec<S>>,
451 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>,
456 pub computed_fields: Vec<String>, _phantom: PhantomData<S>,
458}
459
460impl<S> TypedStreamSpec<S> {
461 pub fn new(
462 state_name: String,
463 identity: IdentitySpec,
464 handlers: Vec<TypedHandlerSpec<S>>,
465 ) -> Self {
466 TypedStreamSpec {
467 state_name,
468 identity,
469 handlers,
470 sections: Vec::new(),
471 field_mappings: BTreeMap::new(),
472 resolver_hooks: Vec::new(),
473 instruction_hooks: Vec::new(),
474 resolver_specs: Vec::new(),
475 computed_fields: Vec::new(),
476 _phantom: PhantomData,
477 }
478 }
479
480 pub fn with_type_info(
482 state_name: String,
483 identity: IdentitySpec,
484 handlers: Vec<TypedHandlerSpec<S>>,
485 sections: Vec<EntitySection>,
486 field_mappings: BTreeMap<String, FieldTypeInfo>,
487 ) -> Self {
488 TypedStreamSpec {
489 state_name,
490 identity,
491 handlers,
492 sections,
493 field_mappings,
494 resolver_hooks: Vec::new(),
495 instruction_hooks: Vec::new(),
496 resolver_specs: Vec::new(),
497 computed_fields: Vec::new(),
498 _phantom: PhantomData,
499 }
500 }
501
502 pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
503 self.resolver_specs = resolver_specs;
504 self
505 }
506
507 pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
509 self.field_mappings.get(path)
510 }
511
512 pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
514 self.sections
515 .iter()
516 .find(|s| s.name == section_name)
517 .map(|s| &s.fields)
518 }
519
520 pub fn get_section_names(&self) -> Vec<&String> {
522 self.sections.iter().map(|s| &s.name).collect()
523 }
524
525 pub fn to_serializable(&self) -> SerializableStreamSpec {
527 let mut spec = SerializableStreamSpec {
528 ast_version: CURRENT_AST_VERSION.to_string(),
529 state_name: self.state_name.clone(),
530 program_id: None,
531 idl: None,
532 identity: self.identity.clone(),
533 handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
534 sections: self.sections.clone(),
535 field_mappings: self.field_mappings.clone(),
536 resolver_hooks: self.resolver_hooks.clone(),
537 instruction_hooks: self.instruction_hooks.clone(),
538 resolver_specs: self.resolver_specs.clone(),
539 computed_fields: self.computed_fields.clone(),
540 computed_field_specs: Vec::new(),
541 content_hash: None,
542 views: Vec::new(),
543 };
544 spec.content_hash = Some(spec.compute_content_hash());
545 spec
546 }
547
548 pub fn from_serializable(mut spec: SerializableStreamSpec) -> Self {
550 spec.normalize_event_names();
551 TypedStreamSpec {
552 state_name: spec.state_name,
553 identity: spec.identity,
554 handlers: spec
555 .handlers
556 .into_iter()
557 .map(|h| TypedHandlerSpec::from_serializable(h))
558 .collect(),
559 sections: spec.sections,
560 field_mappings: spec.field_mappings,
561 resolver_hooks: spec.resolver_hooks,
562 instruction_hooks: spec.instruction_hooks,
563 resolver_specs: spec.resolver_specs,
564 computed_fields: spec.computed_fields,
565 _phantom: PhantomData,
566 }
567 }
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize)]
571pub struct IdentitySpec {
572 pub primary_keys: Vec<String>,
573 pub lookup_indexes: Vec<LookupIndexSpec>,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct LookupIndexSpec {
578 pub field_name: String,
579 pub temporal_field: Option<String>,
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct ResolverHook {
589 pub account_type: String,
591
592 pub strategy: ResolverStrategy,
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize)]
597pub enum ResolverStrategy {
598 PdaReverseLookup {
600 lookup_name: String,
601 queue_discriminators: Vec<Vec<u8>>,
603 },
604
605 DirectField { field_path: FieldPath },
607}
608
609#[derive(Debug, Clone, Serialize, Deserialize)]
611pub struct InstructionHook {
612 pub instruction_type: String,
614
615 pub actions: Vec<HookAction>,
617
618 pub lookup_by: Option<FieldPath>,
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize)]
623pub enum HookAction {
624 RegisterPdaMapping {
626 pda_field: FieldPath,
627 seed_field: FieldPath,
628 lookup_name: String,
629 },
630
631 SetField {
633 target_field: String,
634 source: MappingSource,
635 condition: Option<ConditionExpr>,
636 },
637
638 IncrementField {
640 target_field: String,
641 increment_by: i64,
642 condition: Option<ConditionExpr>,
643 },
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct ConditionExpr {
649 pub expression: String,
651
652 pub parsed: Option<ParsedCondition>,
654}
655
656#[derive(Debug, Clone, Serialize, Deserialize)]
657pub enum ParsedCondition {
658 Comparison {
660 field: FieldPath,
661 op: ComparisonOp,
662 value: serde_json::Value,
663 },
664
665 Logical {
667 op: LogicalOp,
668 conditions: Vec<ParsedCondition>,
669 },
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
673pub enum ComparisonOp {
674 Equal,
675 NotEqual,
676 GreaterThan,
677 GreaterThanOrEqual,
678 LessThan,
679 LessThanOrEqual,
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize)]
683pub enum LogicalOp {
684 And,
685 Or,
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct SerializableHandlerSpec {
691 pub source: SourceSpec,
692 pub key_resolution: KeyResolutionStrategy,
693 pub mappings: Vec<SerializableFieldMapping>,
694 pub conditions: Vec<Condition>,
695 pub emit: bool,
696}
697
698impl SerializableHandlerSpec {
699 pub fn normalize_event_names(&mut self) {
700 let SourceSpec::Source { type_name, .. } = &mut self.source;
701 *type_name = crate::event_type_helpers::canonicalize_event_type_name(type_name);
702
703 for mapping in &mut self.mappings {
704 if let Some(when) = &mut mapping.when {
705 *when = crate::event_type_helpers::canonicalize_event_type_name(when);
706 }
707 if let Some(stop) = &mut mapping.stop {
708 *stop = crate::event_type_helpers::canonicalize_event_type_name(stop);
709 }
710 }
711 }
712}
713
714#[derive(Debug, Clone)]
715pub struct TypedHandlerSpec<S> {
716 pub source: SourceSpec,
717 pub key_resolution: KeyResolutionStrategy,
718 pub mappings: Vec<TypedFieldMapping<S>>,
719 pub conditions: Vec<Condition>,
720 pub emit: bool,
721 _phantom: PhantomData<S>,
722}
723
724impl<S> TypedHandlerSpec<S> {
725 pub fn new(
726 source: SourceSpec,
727 key_resolution: KeyResolutionStrategy,
728 mappings: Vec<TypedFieldMapping<S>>,
729 emit: bool,
730 ) -> Self {
731 TypedHandlerSpec {
732 source,
733 key_resolution,
734 mappings,
735 conditions: vec![],
736 emit,
737 _phantom: PhantomData,
738 }
739 }
740
741 pub fn to_serializable(&self) -> SerializableHandlerSpec {
743 SerializableHandlerSpec {
744 source: self.source.clone(),
745 key_resolution: self.key_resolution.clone(),
746 mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
747 conditions: self.conditions.clone(),
748 emit: self.emit,
749 }
750 }
751
752 pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
754 TypedHandlerSpec {
755 source: spec.source,
756 key_resolution: spec.key_resolution,
757 mappings: spec
758 .mappings
759 .into_iter()
760 .map(|m| TypedFieldMapping::from_serializable(m))
761 .collect(),
762 conditions: spec.conditions,
763 emit: spec.emit,
764 _phantom: PhantomData,
765 }
766 }
767}
768
769#[derive(Debug, Clone, Serialize, Deserialize)]
770pub enum KeyResolutionStrategy {
771 Embedded {
772 primary_field: FieldPath,
773 },
774 Lookup {
775 primary_field: FieldPath,
776 },
777 Computed {
778 primary_field: FieldPath,
779 compute_partition: ComputeFunction,
780 },
781 TemporalLookup {
782 lookup_field: FieldPath,
783 timestamp_field: FieldPath,
784 index_name: String,
785 },
786}
787
788#[derive(Debug, Clone, Serialize, Deserialize)]
789pub enum SourceSpec {
790 Source {
791 program_id: Option<String>,
792 discriminator: Option<Vec<u8>>,
793 type_name: String,
794 #[serde(default, skip_serializing_if = "Option::is_none")]
795 serialization: Option<IdlSerializationSnapshot>,
796 #[serde(default)]
801 is_account: bool,
802 },
803}
804
805#[derive(Debug, Clone, Serialize, Deserialize)]
807pub struct SerializableFieldMapping {
808 pub target_path: String,
809 pub source: MappingSource,
810 pub transform: Option<Transformation>,
811 pub population: PopulationStrategy,
812 #[serde(default, skip_serializing_if = "Option::is_none")]
813 pub condition: Option<ConditionExpr>,
814 #[serde(default, skip_serializing_if = "Option::is_none")]
815 pub when: Option<String>,
816 #[serde(default, skip_serializing_if = "Option::is_none")]
817 pub stop: Option<String>,
818 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
819 pub emit: bool,
820}
821
822fn default_emit() -> bool {
823 true
824}
825
826fn default_instruction_discriminant_size() -> usize {
827 8
828}
829
830fn is_true(value: &bool) -> bool {
831 *value
832}
833
834#[derive(Debug, Clone)]
835pub struct TypedFieldMapping<S> {
836 pub target_path: String,
837 pub source: MappingSource,
838 pub transform: Option<Transformation>,
839 pub population: PopulationStrategy,
840 pub condition: Option<ConditionExpr>,
841 pub when: Option<String>,
842 pub stop: Option<String>,
843 pub emit: bool,
844 _phantom: PhantomData<S>,
845}
846
847impl<S> TypedFieldMapping<S> {
848 pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
849 TypedFieldMapping {
850 target_path,
851 source,
852 transform: None,
853 population,
854 condition: None,
855 when: None,
856 stop: None,
857 emit: true,
858 _phantom: PhantomData,
859 }
860 }
861
862 pub fn with_transform(mut self, transform: Transformation) -> Self {
863 self.transform = Some(transform);
864 self
865 }
866
867 pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
868 self.condition = Some(condition);
869 self
870 }
871
872 pub fn with_when(mut self, when: String) -> Self {
873 self.when = Some(when);
874 self
875 }
876
877 pub fn with_stop(mut self, stop: String) -> Self {
878 self.stop = Some(stop);
879 self
880 }
881
882 pub fn with_emit(mut self, emit: bool) -> Self {
883 self.emit = emit;
884 self
885 }
886
887 pub fn to_serializable(&self) -> SerializableFieldMapping {
889 SerializableFieldMapping {
890 target_path: self.target_path.clone(),
891 source: self.source.clone(),
892 transform: self.transform.clone(),
893 population: self.population.clone(),
894 condition: self.condition.clone(),
895 when: self.when.clone(),
896 stop: self.stop.clone(),
897 emit: self.emit,
898 }
899 }
900
901 pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
903 TypedFieldMapping {
904 target_path: mapping.target_path,
905 source: mapping.source,
906 transform: mapping.transform,
907 population: mapping.population,
908 condition: mapping.condition,
909 when: mapping.when,
910 stop: mapping.stop,
911 emit: mapping.emit,
912 _phantom: PhantomData,
913 }
914 }
915}
916
917#[derive(Debug, Clone, Serialize, Deserialize)]
918pub enum MappingSource {
919 FromSource {
920 path: FieldPath,
921 default: Option<Value>,
922 transform: Option<Transformation>,
923 },
924 Constant(Value),
925 Computed {
926 inputs: Vec<FieldPath>,
927 function: ComputeFunction,
928 },
929 FromState {
930 path: String,
931 },
932 AsEvent {
933 fields: Vec<Box<MappingSource>>,
934 },
935 WholeSource,
936 AsCapture {
939 field_transforms: BTreeMap<String, Transformation>,
940 },
941 FromContext {
944 field: String,
945 },
946}
947
948impl MappingSource {
949 pub fn with_transform(self, transform: Transformation) -> Self {
950 match self {
951 MappingSource::FromSource {
952 path,
953 default,
954 transform: _,
955 } => MappingSource::FromSource {
956 path,
957 default,
958 transform: Some(transform),
959 },
960 other => other,
961 }
962 }
963}
964
965#[derive(Debug, Clone, Serialize, Deserialize)]
966pub enum ComputeFunction {
967 Sum,
968 Concat,
969 Format(String),
970 Custom(String),
971}
972
973#[derive(Debug, Clone, Serialize, Deserialize)]
974pub struct Condition {
975 pub field: FieldPath,
976 pub operator: ConditionOp,
977 pub value: Value,
978}
979
980#[derive(Debug, Clone, Serialize, Deserialize)]
981pub enum ConditionOp {
982 Equals,
983 NotEquals,
984 GreaterThan,
985 LessThan,
986 Contains,
987 Exists,
988}
989
990#[derive(Debug, Clone, Serialize, Deserialize)]
992pub struct FieldTypeInfo {
993 pub field_name: String,
994 pub rust_type_name: String, pub base_type: BaseType, pub is_optional: bool, pub is_array: bool, pub inner_type: Option<String>, pub source_path: Option<String>, #[serde(default)]
1002 pub resolved_type: Option<ResolvedStructType>,
1003 #[serde(default = "default_emit", skip_serializing_if = "is_true")]
1004 pub emit: bool,
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize)]
1009pub struct ResolvedStructType {
1010 pub type_name: String,
1011 pub fields: Vec<ResolvedField>,
1012 pub is_instruction: bool,
1013 pub is_account: bool,
1014 pub is_event: bool,
1015 #[serde(default)]
1017 pub is_enum: bool,
1018 #[serde(default)]
1020 pub enum_variants: Vec<String>,
1021}
1022
1023#[derive(Debug, Clone, Serialize, Deserialize)]
1025pub struct ResolvedField {
1026 pub field_name: String,
1027 pub field_type: String,
1028 pub base_type: BaseType,
1029 pub is_optional: bool,
1030 pub is_array: bool,
1031}
1032
1033#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1035pub enum BaseType {
1036 Integer, Float, String, Boolean, Object, Array, Binary, Timestamp, Pubkey, Any, }
1052
1053#[derive(Debug, Clone, Serialize, Deserialize)]
1055pub struct EntitySection {
1056 pub name: String,
1057 pub fields: Vec<FieldTypeInfo>,
1058 pub is_nested_struct: bool,
1059 pub parent_field: Option<String>, }
1061
1062impl FieldTypeInfo {
1063 pub fn new(field_name: String, rust_type_name: String) -> Self {
1064 let (base_type, is_optional, is_array, inner_type) =
1065 Self::analyze_rust_type(&rust_type_name);
1066
1067 FieldTypeInfo {
1068 field_name: field_name.clone(),
1069 rust_type_name,
1070 base_type: Self::infer_semantic_type(&field_name, base_type),
1071 is_optional,
1072 is_array,
1073 inner_type,
1074 source_path: None,
1075 resolved_type: None,
1076 emit: true,
1077 }
1078 }
1079
1080 pub fn with_source_path(mut self, source_path: String) -> Self {
1081 self.source_path = Some(source_path);
1082 self
1083 }
1084
1085 fn analyze_rust_type(rust_type: &str) -> (BaseType, bool, bool, Option<String>) {
1087 let type_str = rust_type.trim();
1088
1089 if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1091 let (inner_base_type, _, inner_is_array, inner_inner_type) =
1092 Self::analyze_rust_type(&inner);
1093 return (
1094 inner_base_type,
1095 true,
1096 inner_is_array,
1097 inner_inner_type.or(Some(inner)),
1098 );
1099 }
1100
1101 if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1103 let (_inner_base_type, inner_is_optional, _, inner_inner_type) =
1104 Self::analyze_rust_type(&inner);
1105 return (
1106 BaseType::Array,
1107 inner_is_optional,
1108 true,
1109 inner_inner_type.or(Some(inner)),
1110 );
1111 }
1112
1113 let base_type = match type_str {
1115 "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => {
1116 BaseType::Integer
1117 }
1118 "f32" | "f64" => BaseType::Float,
1119 "bool" => BaseType::Boolean,
1120 "String" | "&str" | "str" => BaseType::String,
1121 "Value" | "serde_json::Value" => BaseType::Any,
1122 "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1123 _ => {
1124 if type_str.contains("Bytes") || type_str.contains("bytes") {
1126 BaseType::Binary
1127 } else if type_str.contains("Pubkey") {
1128 BaseType::Pubkey
1129 } else {
1130 BaseType::Object
1131 }
1132 }
1133 };
1134
1135 (base_type, false, false, None)
1136 }
1137
1138 fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1140 let pattern = format!("{}<", generic_name);
1141 if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1142 let start = pattern.len();
1143 let end = type_str.len() - 1;
1144 if end > start {
1145 return Some(type_str[start..end].trim().to_string());
1146 }
1147 }
1148 None
1149 }
1150
1151 fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1153 let lower_name = field_name.to_lowercase();
1154
1155 if base_type == BaseType::Integer
1157 && (lower_name.ends_with("_at")
1158 || lower_name.ends_with("_time")
1159 || lower_name.contains("timestamp")
1160 || lower_name.contains("created")
1161 || lower_name.contains("settled")
1162 || lower_name.contains("activated"))
1163 {
1164 return BaseType::Timestamp;
1165 }
1166
1167 base_type
1168 }
1169}
1170
1171pub trait FieldAccessor<S> {
1172 fn path(&self) -> String;
1173}
1174
1175impl SerializableStreamSpec {
1180 pub fn compute_content_hash(&self) -> String {
1186 use sha2::{Digest, Sha256};
1187
1188 let mut spec_for_hash = self.clone();
1190 spec_for_hash.content_hash = None;
1191
1192 let json =
1194 serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1195
1196 let mut hasher = Sha256::new();
1198 hasher.update(json.as_bytes());
1199 let result = hasher.finalize();
1200
1201 hex::encode(result)
1203 }
1204
1205 pub fn verify_content_hash(&self) -> bool {
1208 match &self.content_hash {
1209 Some(hash) => {
1210 let computed = self.compute_content_hash();
1211 hash == &computed
1212 }
1213 None => true, }
1215 }
1216
1217 pub fn with_content_hash(mut self) -> Self {
1219 self.content_hash = Some(self.compute_content_hash());
1220 self
1221 }
1222}
1223
1224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1231pub struct PdaDefinition {
1232 pub name: String,
1234
1235 pub seeds: Vec<PdaSeedDef>,
1237
1238 #[serde(default, skip_serializing_if = "Option::is_none")]
1241 pub program_id: Option<String>,
1242}
1243
1244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1246#[serde(tag = "type", rename_all = "camelCase")]
1247pub enum PdaSeedDef {
1248 Literal { value: String },
1250
1251 Bytes { value: Vec<u8> },
1253
1254 ArgRef {
1256 arg_name: String,
1257 #[serde(default, skip_serializing_if = "Option::is_none")]
1259 arg_type: Option<String>,
1260 },
1261
1262 AccountRef { account_name: String },
1264}
1265
1266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1268#[serde(tag = "category", rename_all = "camelCase")]
1269pub enum AccountResolution {
1270 Signer,
1272
1273 Known { address: String },
1275
1276 PdaRef { pda_name: String },
1278
1279 PdaInline {
1281 seeds: Vec<PdaSeedDef>,
1282 #[serde(default, skip_serializing_if = "Option::is_none")]
1283 program_id: Option<String>,
1284 },
1285
1286 UserProvided,
1288}
1289
1290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1292pub struct InstructionAccountDef {
1293 pub name: String,
1295
1296 #[serde(default)]
1298 pub is_signer: bool,
1299
1300 #[serde(default)]
1302 pub is_writable: bool,
1303
1304 pub resolution: AccountResolution,
1306
1307 #[serde(default)]
1309 pub is_optional: bool,
1310
1311 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1313 pub docs: Vec<String>,
1314}
1315
1316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1318pub struct InstructionArgDef {
1319 pub name: String,
1321
1322 #[serde(rename = "type")]
1324 pub arg_type: String,
1325
1326 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1328 pub docs: Vec<String>,
1329}
1330
1331#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1333pub struct InstructionDef {
1334 pub name: String,
1336
1337 pub discriminator: Vec<u8>,
1339
1340 #[serde(default = "default_instruction_discriminant_size")]
1342 pub discriminator_size: usize,
1343
1344 pub accounts: Vec<InstructionAccountDef>,
1346
1347 pub args: Vec<InstructionArgDef>,
1349
1350 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1352 pub errors: Vec<IdlErrorSnapshot>,
1353
1354 #[serde(default, skip_serializing_if = "Option::is_none")]
1356 pub program_id: Option<String>,
1357
1358 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1360 pub docs: Vec<String>,
1361}
1362
1363#[derive(Debug, Clone, Serialize, Deserialize)]
1370pub struct SerializableStackSpec {
1371 #[serde(default = "default_ast_version")]
1374 pub ast_version: String,
1375 pub stack_name: String,
1377 #[serde(default)]
1379 pub program_ids: Vec<String>,
1380 #[serde(default)]
1382 pub idls: Vec<IdlSnapshot>,
1383 pub entities: Vec<SerializableStreamSpec>,
1385 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1388 pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1389 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1391 pub instructions: Vec<InstructionDef>,
1392 #[serde(default, skip_serializing_if = "Option::is_none")]
1394 pub content_hash: Option<String>,
1395}
1396
1397impl SerializableStackSpec {
1398 pub fn normalize_event_names(&mut self) {
1400 for entity in &mut self.entities {
1401 entity.normalize_event_names();
1402 }
1403 }
1404
1405 pub fn compute_content_hash(&self) -> String {
1407 use sha2::{Digest, Sha256};
1408 let mut spec_for_hash = self.clone();
1409 spec_for_hash.content_hash = None;
1410 let json = serde_json::to_string(&spec_for_hash)
1411 .expect("Failed to serialize stack spec for hashing");
1412 let mut hasher = Sha256::new();
1413 hasher.update(json.as_bytes());
1414 hex::encode(hasher.finalize())
1415 }
1416
1417 pub fn with_content_hash(mut self) -> Self {
1418 self.content_hash = Some(self.compute_content_hash());
1419 self
1420 }
1421}
1422
1423#[cfg(test)]
1424mod tests {
1425 use super::{
1426 HookAction, IdentitySpec, InstructionHook, MappingSource, PopulationStrategy,
1427 SerializableFieldMapping, SerializableHandlerSpec, SerializableStackSpec,
1428 SerializableStreamSpec, SourceSpec, TypedStreamSpec, CURRENT_AST_VERSION,
1429 };
1430 use serde_json::Value;
1431
1432 fn legacy_stream_spec() -> SerializableStreamSpec {
1433 SerializableStreamSpec {
1434 ast_version: CURRENT_AST_VERSION.to_string(),
1435 state_name: "PumpfunToken".to_string(),
1436 program_id: None,
1437 idl: None,
1438 identity: IdentitySpec {
1439 primary_keys: vec!["id.mint".to_string()],
1440 lookup_indexes: vec![],
1441 },
1442 handlers: vec![SerializableHandlerSpec {
1443 source: SourceSpec::Source {
1444 program_id: None,
1445 discriminator: None,
1446 type_name: "pump::buyIxState".to_string(),
1447 serialization: None,
1448 is_account: false,
1449 },
1450 key_resolution: super::KeyResolutionStrategy::Embedded {
1451 primary_field: super::FieldPath::new(&["accounts", "mint"]),
1452 },
1453 mappings: vec![SerializableFieldMapping {
1454 target_path: "info.last_buy_at".to_string(),
1455 source: MappingSource::Constant(Value::Null),
1456 transform: None,
1457 population: PopulationStrategy::SetOnce,
1458 condition: None,
1459 when: Some("pump::sellIxState".to_string()),
1460 stop: Some("pump::buy_exact_sol_inIxState".to_string()),
1461 emit: true,
1462 }],
1463 conditions: vec![],
1464 emit: true,
1465 }],
1466 sections: vec![],
1467 field_mappings: Default::default(),
1468 resolver_hooks: vec![],
1469 instruction_hooks: vec![InstructionHook {
1470 instruction_type: "pump::buyIxState".to_string(),
1471 actions: vec![HookAction::SetField {
1472 target_field: "info.last_buy_at".to_string(),
1473 source: MappingSource::Constant(Value::Null),
1474 condition: None,
1475 }],
1476 lookup_by: None,
1477 }],
1478 resolver_specs: vec![],
1479 computed_fields: vec![],
1480 computed_field_specs: vec![],
1481 content_hash: None,
1482 views: vec![],
1483 }
1484 }
1485
1486 #[test]
1487 fn typed_stream_spec_from_serializable_normalizes_legacy_instruction_event_names() {
1488 let typed = TypedStreamSpec::<Value>::from_serializable(legacy_stream_spec());
1489
1490 let handler = &typed.handlers[0];
1491 let SourceSpec::Source { type_name, .. } = &handler.source;
1492 assert_eq!(type_name, "pump::BuyIxState");
1493 assert_eq!(
1494 handler.mappings[0].when.as_deref(),
1495 Some("pump::SellIxState")
1496 );
1497 assert_eq!(
1498 handler.mappings[0].stop.as_deref(),
1499 Some("pump::BuyExactSolInIxState")
1500 );
1501 assert_eq!(
1502 typed.instruction_hooks[0].instruction_type,
1503 "pump::BuyIxState"
1504 );
1505 }
1506
1507 #[test]
1508 fn stack_spec_normalize_event_names_updates_all_entities() {
1509 let mut stack = SerializableStackSpec {
1510 ast_version: CURRENT_AST_VERSION.to_string(),
1511 stack_name: "PumpStack".to_string(),
1512 program_ids: vec![],
1513 idls: vec![],
1514 entities: vec![legacy_stream_spec()],
1515 pdas: Default::default(),
1516 instructions: vec![],
1517 content_hash: None,
1518 };
1519
1520 stack.normalize_event_names();
1521
1522 let SourceSpec::Source { type_name, .. } = &stack.entities[0].handlers[0].source;
1523 assert_eq!(type_name, "pump::BuyIxState");
1524 assert_eq!(
1525 stack.entities[0].instruction_hooks[0].instruction_type,
1526 "pump::BuyIxState"
1527 );
1528 }
1529}
1530
1531#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1537#[serde(rename_all = "lowercase")]
1538pub enum SortOrder {
1539 #[default]
1540 Asc,
1541 Desc,
1542}
1543
1544#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1546pub enum CompareOp {
1547 Eq,
1548 Ne,
1549 Gt,
1550 Gte,
1551 Lt,
1552 Lte,
1553}
1554
1555#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1557pub enum PredicateValue {
1558 Literal(serde_json::Value),
1560 Dynamic(String),
1562 Field(FieldPath),
1564}
1565
1566#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1568pub enum Predicate {
1569 Compare {
1571 field: FieldPath,
1572 op: CompareOp,
1573 value: PredicateValue,
1574 },
1575 And(Vec<Predicate>),
1577 Or(Vec<Predicate>),
1579 Not(Box<Predicate>),
1581 Exists { field: FieldPath },
1583}
1584
1585#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1587pub enum ViewTransform {
1588 Filter { predicate: Predicate },
1590
1591 Sort {
1593 key: FieldPath,
1594 #[serde(default)]
1595 order: SortOrder,
1596 },
1597
1598 Take { count: usize },
1600
1601 Skip { count: usize },
1603
1604 First,
1606
1607 Last,
1609
1610 MaxBy { key: FieldPath },
1612
1613 MinBy { key: FieldPath },
1615}
1616
1617#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1619pub enum ViewSource {
1620 Entity { name: String },
1622 View { id: String },
1624}
1625
1626#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1628pub enum ViewOutput {
1629 #[default]
1631 Collection,
1632 Single,
1634 Keyed { key_field: FieldPath },
1636}
1637
1638#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1640pub struct ViewDef {
1641 pub id: String,
1643
1644 pub source: ViewSource,
1646
1647 #[serde(default)]
1649 pub pipeline: Vec<ViewTransform>,
1650
1651 #[serde(default)]
1653 pub output: ViewOutput,
1654}
1655
1656impl ViewDef {
1657 pub fn list(entity_name: &str) -> Self {
1659 ViewDef {
1660 id: format!("{}/list", entity_name),
1661 source: ViewSource::Entity {
1662 name: entity_name.to_string(),
1663 },
1664 pipeline: vec![],
1665 output: ViewOutput::Collection,
1666 }
1667 }
1668
1669 pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1671 ViewDef {
1672 id: format!("{}/state", entity_name),
1673 source: ViewSource::Entity {
1674 name: entity_name.to_string(),
1675 },
1676 pipeline: vec![],
1677 output: ViewOutput::Keyed {
1678 key_field: FieldPath::new(key_field),
1679 },
1680 }
1681 }
1682
1683 pub fn is_single(&self) -> bool {
1685 matches!(self.output, ViewOutput::Single)
1686 }
1687
1688 pub fn has_single_transform(&self) -> bool {
1690 self.pipeline.iter().any(|t| {
1691 matches!(
1692 t,
1693 ViewTransform::First
1694 | ViewTransform::Last
1695 | ViewTransform::MaxBy { .. }
1696 | ViewTransform::MinBy { .. }
1697 )
1698 })
1699 }
1700}
1701
1702#[macro_export]
1703macro_rules! define_accessor {
1704 ($name:ident, $state:ty, $path:expr) => {
1705 pub struct $name;
1706
1707 impl $crate::ast::FieldAccessor<$state> for $name {
1708 fn path(&self) -> String {
1709 $path.to_string()
1710 }
1711 }
1712 };
1713}