Skip to main content

arete_interpreter/
ast.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::marker::PhantomData;
5
6pub use arete_idl::snapshot::*;
7
8/// Current AST version for SerializableStreamSpec and SerializableStackSpec
9///
10/// ⚠️ IMPORTANT: This constant is duplicated in arete-macros/src/ast/types.rs due to
11/// circular dependency between proc-macro crates and their output crates.
12/// When bumping this version, you MUST also update the constant in the
13/// arete-macros crate. A test in versioned.rs verifies they stay in sync.
14// 0.0.2: enum variants may carry `fields` (data-carrying enum variants).
15// 0.0.3: field metadata may carry integer-kind and raw/canonical name hints.
16// 0.0.4: instruction args may carry amount-hint metadata.
17// 0.0.5: stack specs may carry exact ordered public ProgramSpecV1 values.
18// Additive over 0.0.1 — every older file deserializes unchanged.
19pub const CURRENT_AST_VERSION: &str = "0.0.5";
20
21/// Older versions this build can still deserialize directly (all changes
22/// since were additive with serde defaults).
23pub const COMPATIBLE_AST_VERSIONS: &[&str] = &["0.0.1", "0.0.2", "0.0.3", "0.0.4"];
24
25fn default_ast_version() -> String {
26    CURRENT_AST_VERSION.to_string()
27}
28
29pub fn idl_type_snapshot_to_rust_string(ty: &IdlTypeSnapshot) -> String {
30    match ty {
31        IdlTypeSnapshot::Simple(s) => map_simple_idl_type(s),
32        IdlTypeSnapshot::Array(arr) => {
33            if arr.array.len() == 2 {
34                match (&arr.array[0], &arr.array[1]) {
35                    (IdlArrayElementSnapshot::TypeName(t), IdlArrayElementSnapshot::Size(size)) => {
36                        format!("[{}; {}]", map_simple_idl_type(t), size)
37                    }
38                    (
39                        IdlArrayElementSnapshot::Type(nested),
40                        IdlArrayElementSnapshot::Size(size),
41                    ) => {
42                        format!("[{}; {}]", idl_type_snapshot_to_rust_string(nested), size)
43                    }
44                    _ => "Vec<u8>".to_string(),
45                }
46            } else {
47                "Vec<u8>".to_string()
48            }
49        }
50        IdlTypeSnapshot::Option(opt) => {
51            format!("Option<{}>", idl_type_snapshot_to_rust_string(&opt.option))
52        }
53        IdlTypeSnapshot::Vec(vec) => {
54            let inner = idl_type_snapshot_to_rust_string(&vec.vec);
55            match vec.length_prefix {
56                Some(arete_idl::types::IdlLengthPrefix::U64) => format!("VecU64Len<{}>", inner),
57                _ => format!("Vec<{}>", inner),
58            }
59        }
60        IdlTypeSnapshot::HashMap(map) => {
61            let key_type = idl_type_snapshot_to_rust_string(&map.hash_map.0);
62            let val_type = idl_type_snapshot_to_rust_string(&map.hash_map.1);
63            format!("std::collections::HashMap<{}, {}>", key_type, val_type)
64        }
65        IdlTypeSnapshot::Tuple(tuple) => format!(
66            "({})",
67            tuple
68                .tuple
69                .iter()
70                .map(idl_type_snapshot_to_rust_string)
71                .collect::<Vec<_>>()
72                .join(", ")
73        ),
74        IdlTypeSnapshot::Defined(def) => match &def.defined {
75            IdlDefinedInnerSnapshot::Named { name } => name.clone(),
76            IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
77        },
78    }
79}
80
81fn map_simple_idl_type(idl_type: &str) -> String {
82    match idl_type {
83        "u8" => "u8".to_string(),
84        "u16" => "u16".to_string(),
85        "u32" => "u32".to_string(),
86        "u64" => "u64".to_string(),
87        "u128" => "u128".to_string(),
88        "i8" => "i8".to_string(),
89        "i16" => "i16".to_string(),
90        "i32" => "i32".to_string(),
91        "i64" => "i64".to_string(),
92        "i128" => "i128".to_string(),
93        "bool" => "bool".to_string(),
94        "string" => "String".to_string(),
95        "publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
96        "bytes" => "Vec<u8>".to_string(),
97        _ => idl_type.to_string(),
98    }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102pub struct FieldPath {
103    pub segments: Vec<String>,
104    pub offsets: Option<Vec<usize>>,
105}
106
107impl FieldPath {
108    pub fn new(segments: &[&str]) -> Self {
109        FieldPath {
110            segments: segments.iter().map(|s| s.to_string()).collect(),
111            offsets: None,
112        }
113    }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
117pub enum Transformation {
118    HexEncode,
119    HexDecode,
120    Base58Encode,
121    Base58Decode,
122    ToString,
123    ToNumber,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub enum PopulationStrategy {
128    SetOnce,
129    LastWrite,
130    Append,
131    Merge,
132    Max,
133    /// Sum numeric values (accumulator pattern for aggregations)
134    Sum,
135    /// Count occurrences (increments by 1 for each update)
136    Count,
137    /// Track minimum value
138    Min,
139    /// Track unique values and store the count
140    /// Internally maintains a HashSet, exposes only the count
141    UniqueCount,
142}
143
144// ============================================================================
145// Computed Field Expression AST
146// ============================================================================
147
148/// Specification for a computed/derived field
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ComputedFieldSpec {
151    /// Target field path (e.g., "trading.total_volume")
152    pub target_path: String,
153    /// Expression AST
154    pub expression: ComputedExpr,
155    /// Result type (e.g., "Option<u64>", "Option<f64>")
156    pub result_type: String,
157}
158
159// ============================================================================
160// Resolver Specifications
161// ============================================================================
162
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
164#[serde(rename_all = "lowercase")]
165pub enum ResolverType {
166    Token,
167    Url(UrlResolverConfig),
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
171#[serde(rename_all = "lowercase")]
172pub enum HttpMethod {
173    #[default]
174    Get,
175    Post,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
179pub enum UrlTemplatePart {
180    Literal(String),
181    FieldRef(String),
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
185pub enum UrlSource {
186    FieldPath(String),
187    Template(Vec<UrlTemplatePart>),
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
191pub struct UrlResolverConfig {
192    pub url_source: UrlSource,
193    #[serde(default)]
194    pub method: HttpMethod,
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub extract_path: Option<String>,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
200pub struct ResolverExtractSpec {
201    pub target_path: String,
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub source_path: Option<String>,
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub transform: Option<Transformation>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
209pub enum ResolveStrategy {
210    #[default]
211    SetOnce,
212    LastWrite,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
216pub struct ResolverCondition {
217    pub field_path: String,
218    pub op: ComparisonOp,
219    pub value: Value,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct ResolverSpec {
224    pub resolver: ResolverType,
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub input_path: Option<String>,
227    #[serde(default, skip_serializing_if = "Option::is_none")]
228    pub input_value: Option<Value>,
229    #[serde(default)]
230    pub strategy: ResolveStrategy,
231    pub extracts: Vec<ResolverExtractSpec>,
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    pub condition: Option<ResolverCondition>,
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    pub schedule_at: Option<String>,
236}
237
238/// AST for computed field expressions
239/// Supports a subset of Rust expressions needed for computed fields:
240/// - Field references (possibly from other sections)
241/// - Unwrap with defaults
242/// - Basic arithmetic and comparisons
243/// - Type casts
244/// - Method calls
245/// - Let bindings and conditionals
246/// - Byte array operations
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub enum ComputedExpr {
249    // Existing variants
250    /// Reference to a field: "field_name" or "section.field_name"
251    FieldRef {
252        path: String,
253    },
254
255    /// Unwrap with default: expr.unwrap_or(default)
256    UnwrapOr {
257        expr: Box<ComputedExpr>,
258        default: serde_json::Value,
259    },
260
261    /// Binary operation: left op right
262    Binary {
263        op: BinaryOp,
264        left: Box<ComputedExpr>,
265        right: Box<ComputedExpr>,
266    },
267
268    /// Type cast: expr as type
269    Cast {
270        expr: Box<ComputedExpr>,
271        to_type: String,
272    },
273
274    /// Method call: expr.method(args)
275    MethodCall {
276        expr: Box<ComputedExpr>,
277        method: String,
278        args: Vec<ComputedExpr>,
279    },
280
281    /// Computation provided by a resolver
282    ResolverComputed {
283        resolver: String,
284        method: String,
285        args: Vec<ComputedExpr>,
286    },
287
288    /// Literal value: numbers, booleans, strings
289    Literal {
290        value: serde_json::Value,
291    },
292
293    /// Parenthesized expression for grouping
294    Paren {
295        expr: Box<ComputedExpr>,
296    },
297
298    // Variable reference (for let bindings)
299    Var {
300        name: String,
301    },
302
303    // Let binding: let name = value; body
304    Let {
305        name: String,
306        value: Box<ComputedExpr>,
307        body: Box<ComputedExpr>,
308    },
309
310    // Conditional: if condition { then_branch } else { else_branch }
311    If {
312        condition: Box<ComputedExpr>,
313        then_branch: Box<ComputedExpr>,
314        else_branch: Box<ComputedExpr>,
315    },
316
317    // Option constructors
318    None,
319    Some {
320        value: Box<ComputedExpr>,
321    },
322
323    // Byte/array operations
324    Slice {
325        expr: Box<ComputedExpr>,
326        start: usize,
327        end: usize,
328    },
329    Index {
330        expr: Box<ComputedExpr>,
331        index: usize,
332    },
333
334    // Byte conversion functions
335    U64FromLeBytes {
336        bytes: Box<ComputedExpr>,
337    },
338    U64FromBeBytes {
339        bytes: Box<ComputedExpr>,
340    },
341
342    // Byte array literals: [0u8; 32] or [1, 2, 3]
343    ByteArray {
344        bytes: Vec<u8>,
345    },
346
347    // Closure for map operations: |x| body
348    Closure {
349        param: String,
350        body: Box<ComputedExpr>,
351    },
352
353    // Unary operations
354    Unary {
355        op: UnaryOp,
356        expr: Box<ComputedExpr>,
357    },
358
359    // JSON array to bytes conversion (for working with captured byte arrays)
360    JsonToBytes {
361        expr: Box<ComputedExpr>,
362    },
363
364    // Context access - slot and timestamp from the update that triggered evaluation
365    /// Access the slot number from the current update context
366    ContextSlot,
367    /// Access the unix timestamp from the current update context
368    ContextTimestamp,
369
370    /// Keccak256 hash function for computing Ethereum-compatible hashes
371    /// Takes a byte array expression and returns the 32-byte hash as a Vec<u8>
372    Keccak256 {
373        expr: Box<ComputedExpr>,
374    },
375}
376
377/// Binary operators for computed expressions
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub enum BinaryOp {
380    // Arithmetic
381    Add,
382    Sub,
383    Mul,
384    Div,
385    Mod,
386    // Comparison
387    Gt,
388    Lt,
389    Gte,
390    Lte,
391    Eq,
392    Ne,
393    // Logical
394    And,
395    Or,
396    // Bitwise
397    Xor,
398    BitAnd,
399    BitOr,
400    Shl,
401    Shr,
402}
403
404/// Unary operators for computed expressions
405#[derive(Debug, Clone, Serialize, Deserialize)]
406pub enum UnaryOp {
407    Not,
408    ReverseBits,
409}
410
411/// Serializable version of StreamSpec without phantom types
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct SerializableStreamSpec {
414    /// AST schema version for backward compatibility
415    /// Uses semver format (e.g., "0.0.1")
416    #[serde(default = "default_ast_version")]
417    pub ast_version: String,
418    pub state_name: String,
419    /// Program ID (Solana address) - extracted from IDL
420    #[serde(default)]
421    pub program_id: Option<String>,
422    /// Embedded IDL for AST-only compilation
423    #[serde(default)]
424    pub idl: Option<IdlSnapshot>,
425    pub identity: IdentitySpec,
426    pub handlers: Vec<SerializableHandlerSpec>,
427    pub sections: Vec<EntitySection>,
428    pub field_mappings: BTreeMap<String, FieldTypeInfo>,
429    pub resolver_hooks: Vec<ResolverHook>,
430    pub instruction_hooks: Vec<InstructionHook>,
431    #[serde(default)]
432    pub resolver_specs: Vec<ResolverSpec>,
433    /// Computed field paths (legacy, for backward compatibility)
434    #[serde(default)]
435    pub computed_fields: Vec<String>,
436    /// Computed field specifications with full expression AST
437    #[serde(default)]
438    pub computed_field_specs: Vec<ComputedFieldSpec>,
439    /// Deterministic content hash (SHA256 of canonical JSON, excluding this field)
440    /// Used for deduplication and version tracking
441    #[serde(default, skip_serializing_if = "Option::is_none")]
442    pub content_hash: Option<String>,
443    /// View definitions for derived/projected views
444    #[serde(default)]
445    pub views: Vec<ViewDef>,
446}
447
448impl SerializableStreamSpec {
449    /// Normalize legacy event names emitted by older AST generators.
450    pub fn normalize_event_names(&mut self) {
451        for handler in &mut self.handlers {
452            handler.normalize_event_names();
453        }
454
455        for hook in &mut self.instruction_hooks {
456            hook.instruction_type =
457                crate::event_type_helpers::canonicalize_event_type_name(&hook.instruction_type);
458        }
459    }
460}
461
462#[derive(Debug, Clone)]
463pub struct TypedStreamSpec<S> {
464    pub state_name: String,
465    pub identity: IdentitySpec,
466    pub handlers: Vec<TypedHandlerSpec<S>>,
467    pub sections: Vec<EntitySection>, // NEW: Complete structural information
468    pub field_mappings: BTreeMap<String, FieldTypeInfo>, // NEW: All field type info by target path
469    pub resolver_hooks: Vec<ResolverHook>, // NEW: Resolver hooks for PDA key resolution
470    pub instruction_hooks: Vec<InstructionHook>, // NEW: Instruction hooks for PDA registration
471    pub resolver_specs: Vec<ResolverSpec>,
472    pub computed_fields: Vec<String>, // List of computed field paths
473    _phantom: PhantomData<S>,
474}
475
476impl<S> TypedStreamSpec<S> {
477    pub fn new(
478        state_name: String,
479        identity: IdentitySpec,
480        handlers: Vec<TypedHandlerSpec<S>>,
481    ) -> Self {
482        TypedStreamSpec {
483            state_name,
484            identity,
485            handlers,
486            sections: Vec::new(),
487            field_mappings: BTreeMap::new(),
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    /// Enhanced constructor with type information
497    pub fn with_type_info(
498        state_name: String,
499        identity: IdentitySpec,
500        handlers: Vec<TypedHandlerSpec<S>>,
501        sections: Vec<EntitySection>,
502        field_mappings: BTreeMap<String, FieldTypeInfo>,
503    ) -> Self {
504        TypedStreamSpec {
505            state_name,
506            identity,
507            handlers,
508            sections,
509            field_mappings,
510            resolver_hooks: Vec::new(),
511            instruction_hooks: Vec::new(),
512            resolver_specs: Vec::new(),
513            computed_fields: Vec::new(),
514            _phantom: PhantomData,
515        }
516    }
517
518    pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
519        self.resolver_specs = resolver_specs;
520        self
521    }
522
523    /// Get type information for a specific field path
524    pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
525        self.field_mappings.get(path)
526    }
527
528    /// Get all fields for a specific section
529    pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
530        self.sections
531            .iter()
532            .find(|s| s.name == section_name)
533            .map(|s| &s.fields)
534    }
535
536    /// Get all section names
537    pub fn get_section_names(&self) -> Vec<&String> {
538        self.sections.iter().map(|s| &s.name).collect()
539    }
540
541    /// Convert to serializable format
542    pub fn to_serializable(&self) -> SerializableStreamSpec {
543        let mut spec = SerializableStreamSpec {
544            ast_version: CURRENT_AST_VERSION.to_string(),
545            state_name: self.state_name.clone(),
546            program_id: None,
547            idl: None,
548            identity: self.identity.clone(),
549            handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
550            sections: self.sections.clone(),
551            field_mappings: self.field_mappings.clone(),
552            resolver_hooks: self.resolver_hooks.clone(),
553            instruction_hooks: self.instruction_hooks.clone(),
554            resolver_specs: self.resolver_specs.clone(),
555            computed_fields: self.computed_fields.clone(),
556            computed_field_specs: Vec::new(),
557            content_hash: None,
558            views: Vec::new(),
559        };
560        spec.content_hash = Some(spec.compute_content_hash());
561        spec
562    }
563
564    /// Create from serializable format
565    pub fn from_serializable(mut spec: SerializableStreamSpec) -> Self {
566        spec.normalize_event_names();
567        TypedStreamSpec {
568            state_name: spec.state_name,
569            identity: spec.identity,
570            handlers: spec
571                .handlers
572                .into_iter()
573                .map(|h| TypedHandlerSpec::from_serializable(h))
574                .collect(),
575            sections: spec.sections,
576            field_mappings: spec.field_mappings,
577            resolver_hooks: spec.resolver_hooks,
578            instruction_hooks: spec.instruction_hooks,
579            resolver_specs: spec.resolver_specs,
580            computed_fields: spec.computed_fields,
581            _phantom: PhantomData,
582        }
583    }
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct IdentitySpec {
588    pub primary_keys: Vec<String>,
589    pub lookup_indexes: Vec<LookupIndexSpec>,
590}
591
592#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct LookupIndexSpec {
594    pub field_name: String,
595    pub temporal_field: Option<String>,
596}
597
598// ============================================================================
599// Level 1: Declarative Hook Extensions
600// ============================================================================
601
602/// Declarative resolver hook specification
603#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct ResolverHook {
605    /// Account type this resolver applies to (e.g., "BondingCurveState")
606    pub account_type: String,
607
608    /// Resolution strategy
609    pub strategy: ResolverStrategy,
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub enum ResolverStrategy {
614    /// Look up PDA in reverse lookup table, queue if not found
615    PdaReverseLookup {
616        lookup_name: String,
617        /// Instruction discriminators to queue until (8 bytes each)
618        queue_discriminators: Vec<Vec<u8>>,
619    },
620
621    /// Extract primary key directly from account data (future)
622    DirectField { field_path: FieldPath },
623}
624
625/// Declarative instruction hook specification
626#[derive(Debug, Clone, Serialize, Deserialize)]
627pub struct InstructionHook {
628    /// Instruction type this hook applies to (e.g., "CreateIxState")
629    pub instruction_type: String,
630
631    /// Actions to perform when this instruction is processed
632    pub actions: Vec<HookAction>,
633
634    /// Lookup strategy for finding the entity
635    pub lookup_by: Option<FieldPath>,
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize)]
639pub enum HookAction {
640    /// Register a PDA mapping for reverse lookup
641    RegisterPdaMapping {
642        pda_field: FieldPath,
643        seed_field: FieldPath,
644        lookup_name: String,
645    },
646
647    /// Set a field value (for #[track_from])
648    SetField {
649        target_field: String,
650        source: MappingSource,
651        condition: Option<ConditionExpr>,
652    },
653
654    /// Increment a field value (for conditional aggregations)
655    IncrementField {
656        target_field: String,
657        increment_by: i64,
658        condition: Option<ConditionExpr>,
659    },
660}
661
662/// Simple condition expression (Level 1 - basic comparisons only)
663#[derive(Debug, Clone, Serialize, Deserialize)]
664pub struct ConditionExpr {
665    /// Expression as string (will be parsed and validated)
666    pub expression: String,
667
668    /// Parsed representation (for validation and execution)
669    pub parsed: Option<ParsedCondition>,
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize)]
673pub enum ParsedCondition {
674    /// Binary comparison: field op value
675    Comparison {
676        field: FieldPath,
677        op: ComparisonOp,
678        value: serde_json::Value,
679    },
680
681    /// Logical AND/OR
682    Logical {
683        op: LogicalOp,
684        conditions: Vec<ParsedCondition>,
685    },
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
689pub enum ComparisonOp {
690    Equal,
691    NotEqual,
692    GreaterThan,
693    GreaterThanOrEqual,
694    LessThan,
695    LessThanOrEqual,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
699pub enum LogicalOp {
700    And,
701    Or,
702}
703
704/// Serializable version of HandlerSpec without phantom types
705#[derive(Debug, Clone, Serialize, Deserialize)]
706pub struct SerializableHandlerSpec {
707    pub source: SourceSpec,
708    pub key_resolution: KeyResolutionStrategy,
709    pub mappings: Vec<SerializableFieldMapping>,
710    pub conditions: Vec<Condition>,
711    pub emit: bool,
712}
713
714impl SerializableHandlerSpec {
715    pub fn normalize_event_names(&mut self) {
716        let SourceSpec::Source { type_name, .. } = &mut self.source;
717        *type_name = crate::event_type_helpers::canonicalize_event_type_name(type_name);
718
719        for mapping in &mut self.mappings {
720            if let Some(when) = &mut mapping.when {
721                *when = crate::event_type_helpers::canonicalize_event_type_name(when);
722            }
723            if let Some(stop) = &mut mapping.stop {
724                *stop = crate::event_type_helpers::canonicalize_event_type_name(stop);
725            }
726        }
727    }
728}
729
730#[derive(Debug, Clone)]
731pub struct TypedHandlerSpec<S> {
732    pub source: SourceSpec,
733    pub key_resolution: KeyResolutionStrategy,
734    pub mappings: Vec<TypedFieldMapping<S>>,
735    pub conditions: Vec<Condition>,
736    pub emit: bool,
737    _phantom: PhantomData<S>,
738}
739
740impl<S> TypedHandlerSpec<S> {
741    pub fn new(
742        source: SourceSpec,
743        key_resolution: KeyResolutionStrategy,
744        mappings: Vec<TypedFieldMapping<S>>,
745        emit: bool,
746    ) -> Self {
747        TypedHandlerSpec {
748            source,
749            key_resolution,
750            mappings,
751            conditions: vec![],
752            emit,
753            _phantom: PhantomData,
754        }
755    }
756
757    /// Convert to serializable format
758    pub fn to_serializable(&self) -> SerializableHandlerSpec {
759        SerializableHandlerSpec {
760            source: self.source.clone(),
761            key_resolution: self.key_resolution.clone(),
762            mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
763            conditions: self.conditions.clone(),
764            emit: self.emit,
765        }
766    }
767
768    /// Create from serializable format
769    pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
770        TypedHandlerSpec {
771            source: spec.source,
772            key_resolution: spec.key_resolution,
773            mappings: spec
774                .mappings
775                .into_iter()
776                .map(|m| TypedFieldMapping::from_serializable(m))
777                .collect(),
778            conditions: spec.conditions,
779            emit: spec.emit,
780            _phantom: PhantomData,
781        }
782    }
783}
784
785#[derive(Debug, Clone, Serialize, Deserialize)]
786pub enum KeyResolutionStrategy {
787    Embedded {
788        primary_field: FieldPath,
789    },
790    Lookup {
791        primary_field: FieldPath,
792    },
793    Computed {
794        primary_field: FieldPath,
795        compute_partition: ComputeFunction,
796    },
797    TemporalLookup {
798        lookup_field: FieldPath,
799        timestamp_field: FieldPath,
800        index_name: String,
801    },
802}
803
804#[derive(Debug, Clone, Serialize, Deserialize)]
805pub enum SourceSpec {
806    Source {
807        program_id: Option<String>,
808        discriminator: Option<Vec<u8>>,
809        type_name: String,
810        #[serde(default, skip_serializing_if = "Option::is_none")]
811        serialization: Option<IdlSerializationSnapshot>,
812        /// True when this handler listens to an account-state event (not an
813        /// instruction or custom event).  Set at code-generation time from
814        /// the structural source kind so the compiler does not need to rely
815        /// on naming-convention heuristics.
816        #[serde(default)]
817        is_account: bool,
818    },
819}
820
821/// Serializable version of FieldMapping without phantom types
822#[derive(Debug, Clone, Serialize, Deserialize)]
823pub struct SerializableFieldMapping {
824    pub target_path: String,
825    pub source: MappingSource,
826    pub transform: Option<Transformation>,
827    pub population: PopulationStrategy,
828    #[serde(default, skip_serializing_if = "Option::is_none")]
829    pub condition: Option<ConditionExpr>,
830    #[serde(default, skip_serializing_if = "Option::is_none")]
831    pub when: Option<String>,
832    #[serde(default, skip_serializing_if = "Option::is_none")]
833    pub stop: Option<String>,
834    #[serde(default = "default_emit", skip_serializing_if = "is_true")]
835    pub emit: bool,
836}
837
838fn default_emit() -> bool {
839    true
840}
841
842fn default_instruction_discriminant_size() -> usize {
843    8
844}
845
846fn is_true(value: &bool) -> bool {
847    *value
848}
849
850#[derive(Debug, Clone)]
851pub struct TypedFieldMapping<S> {
852    pub target_path: String,
853    pub source: MappingSource,
854    pub transform: Option<Transformation>,
855    pub population: PopulationStrategy,
856    pub condition: Option<ConditionExpr>,
857    pub when: Option<String>,
858    pub stop: Option<String>,
859    pub emit: bool,
860    _phantom: PhantomData<S>,
861}
862
863impl<S> TypedFieldMapping<S> {
864    pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
865        TypedFieldMapping {
866            target_path,
867            source,
868            transform: None,
869            population,
870            condition: None,
871            when: None,
872            stop: None,
873            emit: true,
874            _phantom: PhantomData,
875        }
876    }
877
878    pub fn with_transform(mut self, transform: Transformation) -> Self {
879        self.transform = Some(transform);
880        self
881    }
882
883    pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
884        self.condition = Some(condition);
885        self
886    }
887
888    pub fn with_when(mut self, when: String) -> Self {
889        self.when = Some(when);
890        self
891    }
892
893    pub fn with_stop(mut self, stop: String) -> Self {
894        self.stop = Some(stop);
895        self
896    }
897
898    pub fn with_emit(mut self, emit: bool) -> Self {
899        self.emit = emit;
900        self
901    }
902
903    /// Convert to serializable format
904    pub fn to_serializable(&self) -> SerializableFieldMapping {
905        SerializableFieldMapping {
906            target_path: self.target_path.clone(),
907            source: self.source.clone(),
908            transform: self.transform.clone(),
909            population: self.population.clone(),
910            condition: self.condition.clone(),
911            when: self.when.clone(),
912            stop: self.stop.clone(),
913            emit: self.emit,
914        }
915    }
916
917    /// Create from serializable format
918    pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
919        TypedFieldMapping {
920            target_path: mapping.target_path,
921            source: mapping.source,
922            transform: mapping.transform,
923            population: mapping.population,
924            condition: mapping.condition,
925            when: mapping.when,
926            stop: mapping.stop,
927            emit: mapping.emit,
928            _phantom: PhantomData,
929        }
930    }
931}
932
933#[derive(Debug, Clone, Serialize, Deserialize)]
934pub enum MappingSource {
935    FromSource {
936        path: FieldPath,
937        default: Option<Value>,
938        transform: Option<Transformation>,
939    },
940    Constant(Value),
941    Computed {
942        inputs: Vec<FieldPath>,
943        function: ComputeFunction,
944    },
945    FromState {
946        path: String,
947    },
948    AsEvent {
949        fields: Vec<Box<MappingSource>>,
950    },
951    WholeSource,
952    /// Similar to WholeSource but with field-level transformations
953    /// Used by #[capture] macro to apply transforms to specific fields in an account
954    AsCapture {
955        field_transforms: BTreeMap<String, Transformation>,
956    },
957    /// From instruction context (timestamp, slot, signature)
958    /// Used by #[track_from] with special fields like __timestamp
959    FromContext {
960        field: String,
961    },
962    /// An expression evaluated against the incoming event when the handler
963    /// runs. Field references read the event; `ContextSlot` and
964    /// `ContextTimestamp` read the update context. Feeds any population
965    /// strategy, and supplies the key when it maps the primary key.
966    FromExpr {
967        expr: ComputedExpr,
968    },
969}
970
971impl MappingSource {
972    pub fn with_transform(self, transform: Transformation) -> Self {
973        match self {
974            MappingSource::FromSource {
975                path,
976                default,
977                transform: _,
978            } => MappingSource::FromSource {
979                path,
980                default,
981                transform: Some(transform),
982            },
983            other => other,
984        }
985    }
986}
987
988#[derive(Debug, Clone, Serialize, Deserialize)]
989pub enum ComputeFunction {
990    Sum,
991    Concat,
992    Format(String),
993    Custom(String),
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize)]
997pub struct Condition {
998    pub field: FieldPath,
999    pub operator: ConditionOp,
1000    pub value: Value,
1001}
1002
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004pub enum ConditionOp {
1005    Equals,
1006    NotEquals,
1007    GreaterThan,
1008    LessThan,
1009    Contains,
1010    Exists,
1011}
1012
1013/// Language-agnostic type information for fields
1014#[derive(Debug, Clone, Serialize, Deserialize)]
1015pub struct FieldTypeInfo {
1016    pub field_name: String,
1017    #[serde(default, skip_serializing_if = "Option::is_none")]
1018    pub raw_name: Option<String>,
1019    #[serde(default, skip_serializing_if = "Option::is_none")]
1020    pub canonical_name: Option<String>,
1021    pub rust_type_name: String, // Full Rust type: "Option<i64>", "Vec<Value>", etc.
1022    pub base_type: BaseType,    // Fundamental type classification
1023    #[serde(default, skip_serializing_if = "Option::is_none")]
1024    pub integer_kind: Option<IntegerKind>,
1025    pub is_optional: bool,           // true for Option<T>
1026    pub is_array: bool,              // true for Vec<T>
1027    pub inner_type: Option<String>,  // For Option<T> or Vec<T>, store the inner type
1028    pub source_path: Option<String>, // Path to source field if this is mapped
1029    /// Resolved type information for complex types (instructions, accounts, custom types)
1030    #[serde(default)]
1031    pub resolved_type: Option<ResolvedStructType>,
1032    #[serde(default = "default_emit", skip_serializing_if = "is_true")]
1033    pub emit: bool,
1034}
1035
1036/// Resolved structure type with field information from IDL
1037#[derive(Debug, Clone, Serialize, Deserialize)]
1038pub struct ResolvedStructType {
1039    pub type_name: String,
1040    pub fields: Vec<ResolvedField>,
1041    pub is_instruction: bool,
1042    pub is_account: bool,
1043    pub is_event: bool,
1044    /// If true, this is an enum type and enum_variants should be used instead of fields
1045    #[serde(default)]
1046    pub is_enum: bool,
1047    /// For enum types, list of variant names
1048    #[serde(default)]
1049    pub enum_variants: Vec<String>,
1050}
1051
1052/// A resolved field within a complex type
1053#[derive(Debug, Clone, Serialize, Deserialize)]
1054pub struct ResolvedField {
1055    pub field_name: String,
1056    #[serde(default, skip_serializing_if = "Option::is_none")]
1057    pub raw_name: Option<String>,
1058    #[serde(default, skip_serializing_if = "Option::is_none")]
1059    pub canonical_name: Option<String>,
1060    pub field_type: String,
1061    pub base_type: BaseType,
1062    #[serde(default, skip_serializing_if = "Option::is_none")]
1063    pub integer_kind: Option<IntegerKind>,
1064    pub is_optional: bool,
1065    pub is_array: bool,
1066}
1067
1068#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1069pub enum IntegerKind {
1070    U8,
1071    U16,
1072    U32,
1073    U64,
1074    U128,
1075    Usize,
1076    I8,
1077    I16,
1078    I32,
1079    I64,
1080    I128,
1081    Isize,
1082}
1083
1084impl IntegerKind {
1085    pub fn from_rust_type(type_str: &str) -> Option<Self> {
1086        match type_str.trim() {
1087            "u8" => Some(Self::U8),
1088            "u16" => Some(Self::U16),
1089            "u32" => Some(Self::U32),
1090            "u64" => Some(Self::U64),
1091            "u128" => Some(Self::U128),
1092            "usize" => Some(Self::Usize),
1093            "i8" => Some(Self::I8),
1094            "i16" => Some(Self::I16),
1095            "i32" => Some(Self::I32),
1096            "i64" => Some(Self::I64),
1097            "i128" => Some(Self::I128),
1098            "isize" => Some(Self::Isize),
1099            _ => None,
1100        }
1101    }
1102
1103    pub fn is_bigint(self) -> bool {
1104        matches!(self, Self::U64 | Self::U128 | Self::I64 | Self::I128)
1105    }
1106}
1107
1108/// Language-agnostic base type classification
1109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1110pub enum BaseType {
1111    // Numeric types
1112    Integer, // i8, i16, i32, i64, u8, u16, u32, u64, usize, isize
1113    Float,   // f32, f64
1114    // Text types
1115    String, // String, &str
1116    // Boolean
1117    Boolean, // bool
1118    // Complex types
1119    Object, // Custom structs, HashMap, etc.
1120    Array,  // Vec<T>, arrays
1121    Binary, // Bytes, binary data
1122    // Special types
1123    Timestamp, // Detected from field names ending in _at, _time, etc.
1124    Pubkey,    // Solana public key (Base58 encoded)
1125    Any,       // serde_json::Value, unknown types
1126}
1127
1128/// Represents a logical section/group of fields in the entity
1129#[derive(Debug, Clone, Serialize, Deserialize)]
1130pub struct EntitySection {
1131    pub name: String,
1132    pub fields: Vec<FieldTypeInfo>,
1133    pub is_nested_struct: bool,
1134    pub parent_field: Option<String>, // If this section comes from a nested struct field
1135}
1136
1137impl FieldTypeInfo {
1138    pub fn new(field_name: String, rust_type_name: String) -> Self {
1139        let (base_type, integer_kind, is_optional, is_array, inner_type) =
1140            Self::analyze_rust_type(&rust_type_name);
1141        let canonical_name = to_camel_case_owned(&field_name);
1142
1143        FieldTypeInfo {
1144            field_name: field_name.clone(),
1145            raw_name: Some(field_name.clone()),
1146            canonical_name: Some(canonical_name),
1147            rust_type_name,
1148            base_type: Self::infer_semantic_type(&field_name, base_type),
1149            integer_kind,
1150            is_optional,
1151            is_array,
1152            inner_type,
1153            source_path: None,
1154            resolved_type: None,
1155            emit: true,
1156        }
1157    }
1158
1159    pub fn with_source_path(mut self, source_path: String) -> Self {
1160        self.source_path = Some(source_path);
1161        self
1162    }
1163
1164    pub fn raw_field_name(&self) -> &str {
1165        self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1166    }
1167
1168    pub fn canonical_field_name(&self) -> String {
1169        self.canonical_name
1170            .clone()
1171            .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1172    }
1173
1174    pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1175        self.integer_kind.or_else(|| {
1176            IntegerKind::from_rust_type(
1177                self.inner_type
1178                    .as_deref()
1179                    .unwrap_or(self.rust_type_name.as_str()),
1180            )
1181        })
1182    }
1183
1184    /// Analyze a Rust type string and extract structural information
1185    fn analyze_rust_type(
1186        rust_type: &str,
1187    ) -> (BaseType, Option<IntegerKind>, bool, bool, Option<String>) {
1188        let type_str = rust_type.trim();
1189
1190        // Handle Option<T>
1191        if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1192            let (inner_base_type, inner_integer_kind, _, inner_is_array, inner_inner_type) =
1193                Self::analyze_rust_type(&inner);
1194            return (
1195                inner_base_type,
1196                inner_integer_kind,
1197                true,
1198                inner_is_array,
1199                inner_inner_type.or(Some(inner)),
1200            );
1201        }
1202
1203        // Handle Vec<T>
1204        if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1205            let (_inner_base_type, inner_integer_kind, inner_is_optional, _, inner_inner_type) =
1206                Self::analyze_rust_type(&inner);
1207            return (
1208                BaseType::Array,
1209                inner_integer_kind,
1210                inner_is_optional,
1211                true,
1212                inner_inner_type.or(Some(inner)),
1213            );
1214        }
1215
1216        // Handle primitive types
1217        let integer_kind = IntegerKind::from_rust_type(type_str);
1218        let base_type = match integer_kind {
1219            Some(_) => BaseType::Integer,
1220            None => match type_str {
1221                "f32" | "f64" => BaseType::Float,
1222                "bool" => BaseType::Boolean,
1223                "String" | "&str" | "str" => BaseType::String,
1224                "Value" | "serde_json::Value" => BaseType::Any,
1225                "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1226                _ => {
1227                    // Check for binary types
1228                    if type_str.contains("Bytes") || type_str.contains("bytes") {
1229                        BaseType::Binary
1230                    } else if type_str.contains("Pubkey") {
1231                        BaseType::Pubkey
1232                    } else {
1233                        BaseType::Object
1234                    }
1235                }
1236            },
1237        };
1238
1239        (base_type, integer_kind, false, false, None)
1240    }
1241
1242    /// Extract inner type from generic like "Option<T>" -> "T"
1243    fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1244        let pattern = format!("{}<", generic_name);
1245        if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1246            let start = pattern.len();
1247            let end = type_str.len() - 1;
1248            if end > start {
1249                return Some(type_str[start..end].trim().to_string());
1250            }
1251        }
1252        None
1253    }
1254
1255    /// Infer semantic type based on field name patterns
1256    fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1257        let lower_name = field_name.to_lowercase();
1258
1259        // If already classified as integer, check if it should be timestamp
1260        if base_type == BaseType::Integer
1261            && (lower_name.ends_with("_at")
1262                || lower_name.ends_with("_time")
1263                || lower_name.contains("timestamp")
1264                || lower_name.contains("created")
1265                || lower_name.contains("settled")
1266                || lower_name.contains("activated"))
1267        {
1268            return BaseType::Timestamp;
1269        }
1270
1271        base_type
1272    }
1273}
1274
1275impl ResolvedField {
1276    pub fn raw_field_name(&self) -> &str {
1277        self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1278    }
1279
1280    pub fn canonical_field_name(&self) -> String {
1281        self.canonical_name
1282            .clone()
1283            .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1284    }
1285
1286    pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1287        self.integer_kind
1288            .or_else(|| IntegerKind::from_rust_type(self.field_type.as_str()))
1289    }
1290}
1291
1292pub(crate) fn to_camel_case_owned(s: &str) -> String {
1293    let mut result = String::new();
1294    let mut uppercase_next = false;
1295
1296    for ch in s.chars() {
1297        if matches!(ch, '_' | '-' | '.') {
1298            uppercase_next = true;
1299            continue;
1300        }
1301
1302        if result.is_empty() {
1303            result.extend(ch.to_lowercase());
1304            continue;
1305        }
1306
1307        if uppercase_next {
1308            result.extend(ch.to_uppercase());
1309            uppercase_next = false;
1310        } else {
1311            result.push(ch);
1312        }
1313    }
1314
1315    result
1316}
1317
1318pub trait FieldAccessor<S> {
1319    fn path(&self) -> String;
1320}
1321
1322// ============================================================================
1323// SerializableStreamSpec Implementation
1324// ============================================================================
1325
1326impl SerializableStreamSpec {
1327    /// Compute deterministic content hash (SHA256 of canonical JSON).
1328    ///
1329    /// The hash is computed over the entire spec except the content_hash field itself,
1330    /// ensuring the same AST always produces the same hash regardless of when it was
1331    /// generated or by whom.
1332    pub fn compute_content_hash(&self) -> String {
1333        use sha2::{Digest, Sha256};
1334
1335        // Clone and clear the hash field for computation
1336        let mut spec_for_hash = self.clone();
1337        spec_for_hash.content_hash = None;
1338
1339        // Serialize to JSON (serde_json produces consistent output for the same struct)
1340        let json =
1341            serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1342
1343        // Compute SHA256 hash
1344        let mut hasher = Sha256::new();
1345        hasher.update(json.as_bytes());
1346        let result = hasher.finalize();
1347
1348        // Return hex-encoded hash
1349        hex::encode(result)
1350    }
1351
1352    /// Verify that the content_hash matches the computed hash.
1353    /// Returns true if hash is valid or not set.
1354    pub fn verify_content_hash(&self) -> bool {
1355        match &self.content_hash {
1356            Some(hash) => {
1357                let computed = self.compute_content_hash();
1358                hash == &computed
1359            }
1360            None => true, // No hash to verify
1361        }
1362    }
1363
1364    /// Set the content_hash field to the computed hash.
1365    pub fn with_content_hash(mut self) -> Self {
1366        self.content_hash = Some(self.compute_content_hash());
1367        self
1368    }
1369}
1370
1371// ============================================================================
1372// PDA and Instruction Types — For SDK code generation
1373// ============================================================================
1374
1375/// PDA (Program-Derived Address) definition for the stack-level registry.
1376/// PDAs defined here can be referenced by instructions via `pdaRef`.
1377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1378pub struct PdaDefinition {
1379    /// Human-readable name (e.g., "miner", "bondingCurve")
1380    pub name: String,
1381
1382    /// Seeds for PDA derivation, in order
1383    pub seeds: Vec<PdaSeedDef>,
1384
1385    /// Program ID that owns this PDA.
1386    /// If None, uses the stack's primary programId.
1387    #[serde(default, skip_serializing_if = "Option::is_none")]
1388    pub program_id: Option<String>,
1389
1390    /// Dynamic program selector for cross-program PDAs. Static programs stay
1391    /// in `program_id` for backward-compatible artifact serialization.
1392    #[serde(default, skip_serializing_if = "Option::is_none")]
1393    pub program: Option<PdaProgramDef>,
1394}
1395
1396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1397#[serde(tag = "type", rename_all = "camelCase")]
1398pub enum PdaProgramDef {
1399    AccountRef { account_name: String },
1400    ArgRef { arg_name: String },
1401}
1402
1403/// Single seed in a PDA derivation.
1404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1405#[serde(tag = "type", rename_all = "camelCase")]
1406pub enum PdaSeedDef {
1407    /// Static string seed: "miner" → "miner".as_bytes()
1408    Literal { value: String },
1409
1410    /// Static byte array (for non-UTF8 seeds)
1411    Bytes { value: Vec<u8> },
1412
1413    /// Reference to an instruction argument: arg("roundId") → args.roundId as bytes
1414    ArgRef {
1415        arg_name: String,
1416        /// Optional type hint for serialization (e.g., "u64", "pubkey")
1417        #[serde(default, skip_serializing_if = "Option::is_none")]
1418        arg_type: Option<String>,
1419    },
1420
1421    /// Reference to another account in the instruction: account("mint") → accounts.mint pubkey
1422    AccountRef { account_name: String },
1423}
1424
1425/// How an instruction account's address is determined.
1426#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1427#[serde(tag = "category", rename_all = "camelCase")]
1428pub enum AccountResolution {
1429    /// Must sign the transaction. Generated handlers require the caller to
1430    /// provide this account unless the account metadata explicitly opts into
1431    /// wallet or generated signer behavior.
1432    Signer,
1433
1434    /// Fixed known address (e.g., System Program, Token Program)
1435    Known { address: String },
1436
1437    /// Reference to a PDA in the stack's pdas registry
1438    PdaRef { pda_name: String },
1439
1440    /// Inline PDA definition (for one-off PDAs not in the registry)
1441    PdaInline {
1442        seeds: Vec<PdaSeedDef>,
1443        #[serde(default, skip_serializing_if = "Option::is_none")]
1444        program_id: Option<String>,
1445        #[serde(default, skip_serializing_if = "Option::is_none")]
1446        program: Option<PdaProgramDef>,
1447    },
1448
1449    /// User must provide at call time via options.accounts
1450    UserProvided,
1451}
1452
1453/// Account metadata for an instruction.
1454#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1455pub struct InstructionAccountDef {
1456    /// Account name (e.g., "user", "mint", "bondingCurve")
1457    pub name: String,
1458
1459    /// Whether this account must sign the transaction
1460    #[serde(default)]
1461    pub is_signer: bool,
1462
1463    /// Whether this account is writable
1464    #[serde(default)]
1465    pub is_writable: bool,
1466
1467    /// How this account's address is resolved
1468    pub resolution: AccountResolution,
1469
1470    /// Whether this account can be omitted (optional accounts)
1471    #[serde(default)]
1472    pub is_optional: bool,
1473
1474    /// Documentation from IDL
1475    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1476    pub docs: Vec<String>,
1477}
1478
1479/// Argument definition for an instruction.
1480#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1481#[serde(rename_all = "camelCase")]
1482pub struct InstructionAmountHint {
1483    pub decimals_source: AmountDecimalsSource,
1484}
1485
1486#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1487#[serde(
1488    tag = "kind",
1489    rename_all = "camelCase",
1490    rename_all_fields = "camelCase"
1491)]
1492pub enum AmountDecimalsSource {
1493    ArgMint { arg_name: String },
1494    ArgDecimals { arg_name: String },
1495    KnownAccount { account_name: String },
1496    Constant { decimals: u8 },
1497}
1498
1499/// Argument definition for an instruction.
1500#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1501pub struct InstructionArgDef {
1502    /// Argument name
1503    pub name: String,
1504
1505    /// Type from IDL (e.g., "u64", "bool", "pubkey", "Option<u64>")
1506    #[serde(rename = "type")]
1507    pub arg_type: String,
1508
1509    /// Documentation from IDL
1510    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1511    pub docs: Vec<String>,
1512
1513    /// Optional amount-resolution metadata for semantic SDK wrappers.
1514    #[serde(default, skip_serializing_if = "Option::is_none")]
1515    pub amount_hint: Option<InstructionAmountHint>,
1516}
1517
1518/// Full instruction definition in the AST.
1519#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1520pub struct InstructionDef {
1521    /// Instruction name (e.g., "buy", "sell", "automate")
1522    pub name: String,
1523
1524    /// Discriminator bytes (8 bytes for Anchor, 1 byte for Steel)
1525    pub discriminator: Vec<u8>,
1526
1527    /// Size of discriminator in bytes (for buffer allocation)
1528    #[serde(default = "default_instruction_discriminant_size")]
1529    pub discriminator_size: usize,
1530
1531    /// Accounts required by this instruction, in order
1532    pub accounts: Vec<InstructionAccountDef>,
1533
1534    /// Arguments for this instruction, in order
1535    pub args: Vec<InstructionArgDef>,
1536
1537    /// Error definitions specific to this instruction
1538    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1539    pub errors: Vec<IdlErrorSnapshot>,
1540
1541    /// Program ID for this instruction (usually same as stack's programId)
1542    #[serde(default, skip_serializing_if = "Option::is_none")]
1543    pub program_id: Option<String>,
1544
1545    /// Documentation from IDL
1546    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1547    pub docs: Vec<String>,
1548}
1549
1550// ============================================================================
1551// Stack Spec — Unified multi-entity AST format
1552// ============================================================================
1553
1554/// A unified stack specification containing all entities.
1555/// Internal compiled stack representation embedded by generated code.
1556#[derive(Debug, Clone, Serialize, Deserialize)]
1557pub struct SerializableStackSpec {
1558    /// AST schema version for backward compatibility
1559    /// Uses semver format (e.g., "0.0.1")
1560    #[serde(default = "default_ast_version")]
1561    pub ast_version: String,
1562    /// Stack name (PascalCase, derived from module ident)
1563    pub stack_name: String,
1564    /// Program IDs (one per IDL, in order)
1565    #[serde(default)]
1566    pub program_ids: Vec<String>,
1567    /// IDL snapshots (one per program)
1568    #[serde(default)]
1569    pub idls: Vec<IdlSnapshot>,
1570    /// Exact public program specifications, in the same order as `program_ids` and `idls`.
1571    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1572    pub program_specs: Vec<arete_hash::ProgramSpecV1>,
1573    /// All entity specifications in this stack
1574    pub entities: Vec<SerializableStreamSpec>,
1575    /// PDA registry - defines all PDAs for the stack, grouped by program name
1576    /// Outer key is program name (e.g., "ore", "entropy"), inner key is PDA name
1577    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1578    pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1579    /// Instruction definitions for SDK code generation
1580    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1581    pub instructions: Vec<InstructionDef>,
1582    /// Deterministic content hash of the entire stack
1583    #[serde(default, skip_serializing_if = "Option::is_none")]
1584    pub content_hash: Option<String>,
1585}
1586
1587impl SerializableStackSpec {
1588    /// Normalize legacy event names emitted by older AST generators.
1589    pub fn normalize_event_names(&mut self) {
1590        for entity in &mut self.entities {
1591            entity.normalize_event_names();
1592        }
1593    }
1594
1595    /// Compute deterministic content hash (SHA256 of canonical JSON).
1596    pub fn compute_content_hash(&self) -> String {
1597        use sha2::{Digest, Sha256};
1598        let mut spec_for_hash = self.clone();
1599        spec_for_hash.content_hash = None;
1600        let json = serde_json::to_string(&spec_for_hash)
1601            .expect("Failed to serialize stack spec for hashing");
1602        let mut hasher = Sha256::new();
1603        hasher.update(json.as_bytes());
1604        hex::encode(hasher.finalize())
1605    }
1606
1607    pub fn with_content_hash(mut self) -> Self {
1608        self.content_hash = Some(self.compute_content_hash());
1609        self
1610    }
1611}
1612
1613#[cfg(test)]
1614mod tests {
1615    use super::{
1616        HookAction, IdentitySpec, InstructionHook, MappingSource, PopulationStrategy,
1617        SerializableFieldMapping, SerializableHandlerSpec, SerializableStackSpec,
1618        SerializableStreamSpec, SourceSpec, TypedStreamSpec, CURRENT_AST_VERSION,
1619    };
1620    use serde_json::Value;
1621
1622    fn legacy_stream_spec() -> SerializableStreamSpec {
1623        SerializableStreamSpec {
1624            ast_version: CURRENT_AST_VERSION.to_string(),
1625            state_name: "PumpfunToken".to_string(),
1626            program_id: None,
1627            idl: None,
1628            identity: IdentitySpec {
1629                primary_keys: vec!["id.mint".to_string()],
1630                lookup_indexes: vec![],
1631            },
1632            handlers: vec![SerializableHandlerSpec {
1633                source: SourceSpec::Source {
1634                    program_id: None,
1635                    discriminator: None,
1636                    type_name: "pump::buyIxState".to_string(),
1637                    serialization: None,
1638                    is_account: false,
1639                },
1640                key_resolution: super::KeyResolutionStrategy::Embedded {
1641                    primary_field: super::FieldPath::new(&["accounts", "mint"]),
1642                },
1643                mappings: vec![SerializableFieldMapping {
1644                    target_path: "info.last_buy_at".to_string(),
1645                    source: MappingSource::Constant(Value::Null),
1646                    transform: None,
1647                    population: PopulationStrategy::SetOnce,
1648                    condition: None,
1649                    when: Some("pump::sellIxState".to_string()),
1650                    stop: Some("pump::buy_exact_sol_inIxState".to_string()),
1651                    emit: true,
1652                }],
1653                conditions: vec![],
1654                emit: true,
1655            }],
1656            sections: vec![],
1657            field_mappings: Default::default(),
1658            resolver_hooks: vec![],
1659            instruction_hooks: vec![InstructionHook {
1660                instruction_type: "pump::buyIxState".to_string(),
1661                actions: vec![HookAction::SetField {
1662                    target_field: "info.last_buy_at".to_string(),
1663                    source: MappingSource::Constant(Value::Null),
1664                    condition: None,
1665                }],
1666                lookup_by: None,
1667            }],
1668            resolver_specs: vec![],
1669            computed_fields: vec![],
1670            computed_field_specs: vec![],
1671            content_hash: None,
1672            views: vec![],
1673        }
1674    }
1675
1676    #[test]
1677    fn typed_stream_spec_from_serializable_normalizes_legacy_instruction_event_names() {
1678        let typed = TypedStreamSpec::<Value>::from_serializable(legacy_stream_spec());
1679
1680        let handler = &typed.handlers[0];
1681        let SourceSpec::Source { type_name, .. } = &handler.source;
1682        assert_eq!(type_name, "pump::BuyIxState");
1683        assert_eq!(
1684            handler.mappings[0].when.as_deref(),
1685            Some("pump::SellIxState")
1686        );
1687        assert_eq!(
1688            handler.mappings[0].stop.as_deref(),
1689            Some("pump::BuyExactSolInIxState")
1690        );
1691        assert_eq!(
1692            typed.instruction_hooks[0].instruction_type,
1693            "pump::BuyIxState"
1694        );
1695    }
1696
1697    #[test]
1698    fn stack_spec_normalize_event_names_updates_all_entities() {
1699        let mut stack = SerializableStackSpec {
1700            ast_version: CURRENT_AST_VERSION.to_string(),
1701            stack_name: "PumpStack".to_string(),
1702            program_ids: vec![],
1703            idls: vec![],
1704            program_specs: vec![],
1705            entities: vec![legacy_stream_spec()],
1706            pdas: Default::default(),
1707            instructions: vec![],
1708            content_hash: None,
1709        };
1710
1711        stack.normalize_event_names();
1712
1713        let SourceSpec::Source { type_name, .. } = &stack.entities[0].handlers[0].source;
1714        assert_eq!(type_name, "pump::BuyIxState");
1715        assert_eq!(
1716            stack.entities[0].instruction_hooks[0].instruction_type,
1717            "pump::BuyIxState"
1718        );
1719    }
1720
1721    #[test]
1722    fn field_type_info_recognizes_128_bit_integers() {
1723        let unsigned = super::FieldTypeInfo::new("amount".to_string(), "u128".to_string());
1724        assert_eq!(unsigned.base_type, super::BaseType::Integer);
1725        assert!(!unsigned.is_optional);
1726        assert_eq!(unsigned.integer_kind, Some(super::IntegerKind::U128));
1727        assert_eq!(unsigned.raw_field_name(), "amount");
1728        assert_eq!(unsigned.canonical_field_name(), "amount");
1729
1730        let signed = super::FieldTypeInfo::new("delta".to_string(), "Option<i128>".to_string());
1731        assert_eq!(signed.base_type, super::BaseType::Integer);
1732        assert!(signed.is_optional);
1733        assert_eq!(signed.integer_kind, Some(super::IntegerKind::I128));
1734        assert_eq!(signed.inner_type.as_deref(), Some("i128"));
1735    }
1736
1737    #[test]
1738    fn field_type_info_preserves_timestamp_integer_kind_and_names() {
1739        let timestamp =
1740            super::FieldTypeInfo::new("last_updated_at".to_string(), "Option<i64>".to_string());
1741
1742        assert_eq!(timestamp.base_type, super::BaseType::Timestamp);
1743        assert_eq!(timestamp.integer_kind, Some(super::IntegerKind::I64));
1744        assert!(timestamp.is_optional);
1745        assert_eq!(timestamp.raw_field_name(), "last_updated_at");
1746        assert_eq!(timestamp.canonical_field_name(), "lastUpdatedAt");
1747    }
1748}
1749
1750// ============================================================================
1751// View Pipeline Types - Composable View Definitions
1752// ============================================================================
1753
1754/// Sort order for view transforms
1755#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1756#[serde(rename_all = "lowercase")]
1757pub enum SortOrder {
1758    #[default]
1759    Asc,
1760    Desc,
1761}
1762
1763/// Comparison operators for predicates
1764#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1765pub enum CompareOp {
1766    Eq,
1767    Ne,
1768    Gt,
1769    Gte,
1770    Lt,
1771    Lte,
1772}
1773
1774/// Value in a predicate comparison
1775#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1776pub enum PredicateValue {
1777    /// Literal JSON value
1778    Literal(serde_json::Value),
1779    /// Dynamic runtime value (e.g., "now()" for current timestamp)
1780    Dynamic(String),
1781    /// Reference to another field
1782    Field(FieldPath),
1783}
1784
1785/// Predicate for filtering entities
1786#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1787pub enum Predicate {
1788    /// Field comparison: field op value
1789    Compare {
1790        field: FieldPath,
1791        op: CompareOp,
1792        value: PredicateValue,
1793    },
1794    /// Logical AND of predicates
1795    And(Vec<Predicate>),
1796    /// Logical OR of predicates
1797    Or(Vec<Predicate>),
1798    /// Negation
1799    Not(Box<Predicate>),
1800    /// Field exists (is not null)
1801    Exists { field: FieldPath },
1802}
1803
1804/// Transform operation in a view pipeline
1805#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1806pub enum ViewTransform {
1807    /// Filter entities matching a predicate
1808    Filter { predicate: Predicate },
1809
1810    /// Sort entities by a field
1811    Sort {
1812        key: FieldPath,
1813        #[serde(default)]
1814        order: SortOrder,
1815    },
1816
1817    /// Take first N entities (after sort)
1818    Take { count: usize },
1819
1820    /// Skip first N entities
1821    Skip { count: usize },
1822
1823    /// Take only the first entity (after sort) - produces Single output
1824    First,
1825
1826    /// Take only the last entity (after sort) - produces Single output
1827    Last,
1828
1829    /// Get entity with maximum value for field - produces Single output
1830    MaxBy { key: FieldPath },
1831
1832    /// Get entity with minimum value for field - produces Single output
1833    MinBy { key: FieldPath },
1834}
1835
1836/// Source for a view definition
1837#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1838pub enum ViewSource {
1839    /// Derive directly from entity mutations
1840    Entity { name: String },
1841    /// Derive from another view's output
1842    View { id: String },
1843}
1844
1845/// Output mode for a view
1846#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1847pub enum ViewOutput {
1848    /// Multiple entities (list-like semantics)
1849    #[default]
1850    Collection,
1851    /// Single entity (state-like semantics)
1852    Single,
1853    /// Keyed lookup by a specific field
1854    Keyed { key_field: FieldPath },
1855}
1856
1857/// Definition of a view in the pipeline
1858#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1859pub struct ViewDef {
1860    /// Unique view identifier (e.g., "OreRound/latest")
1861    pub id: String,
1862
1863    /// Source this view derives from
1864    pub source: ViewSource,
1865
1866    /// Pipeline of transforms to apply (in order)
1867    #[serde(default)]
1868    pub pipeline: Vec<ViewTransform>,
1869
1870    /// Output mode for this view
1871    #[serde(default)]
1872    pub output: ViewOutput,
1873}
1874
1875impl ViewDef {
1876    /// Create a new list view for an entity
1877    pub fn list(entity_name: &str) -> Self {
1878        ViewDef {
1879            id: format!("{}/list", entity_name),
1880            source: ViewSource::Entity {
1881                name: entity_name.to_string(),
1882            },
1883            pipeline: vec![],
1884            output: ViewOutput::Collection,
1885        }
1886    }
1887
1888    /// Create a new state view for an entity
1889    pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1890        ViewDef {
1891            id: format!("{}/state", entity_name),
1892            source: ViewSource::Entity {
1893                name: entity_name.to_string(),
1894            },
1895            pipeline: vec![],
1896            output: ViewOutput::Keyed {
1897                key_field: FieldPath::new(key_field),
1898            },
1899        }
1900    }
1901
1902    /// Check if this view produces a single entity
1903    pub fn is_single(&self) -> bool {
1904        matches!(self.output, ViewOutput::Single)
1905    }
1906
1907    /// Check if any transform in the pipeline produces a single result
1908    pub fn has_single_transform(&self) -> bool {
1909        self.pipeline.iter().any(|t| {
1910            matches!(
1911                t,
1912                ViewTransform::First
1913                    | ViewTransform::Last
1914                    | ViewTransform::MaxBy { .. }
1915                    | ViewTransform::MinBy { .. }
1916            )
1917        })
1918    }
1919}
1920
1921#[macro_export]
1922macro_rules! define_accessor {
1923    ($name:ident, $state:ty, $path:expr) => {
1924        pub struct $name;
1925
1926        impl $crate::ast::FieldAccessor<$state> for $name {
1927            fn path(&self) -> String {
1928                $path.to_string()
1929            }
1930        }
1931    };
1932}