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