Skip to main content

arete_artifacts/
live.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use arete_hash::{hash_jcs, AccountResolutionV1, HashId, LiveSpec, PdaDefinitionV1, ProgramSpec};
4use arete_idl::snapshot::IdlSerializationSnapshot;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::{
9    json_error, reject_private_fields, validate_envelope_version, validate_kind, ArtifactError,
10    ARTIFACT_VERSION_V1, LIVE_COMPILER_CONTRACT_V1, LIVE_SPEC_KIND, LIVE_SPEC_SCHEMA_V2,
11    LIVE_WIRE_CONTRACT_V1,
12};
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct PortableFieldPath {
16    pub segments: Vec<String>,
17    pub offsets: Option<Vec<usize>>,
18}
19
20impl PortableFieldPath {
21    pub fn new(segments: impl IntoIterator<Item = impl Into<String>>) -> Self {
22        Self {
23            segments: segments.into_iter().map(Into::into).collect(),
24            offsets: None,
25        }
26    }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub enum PortableTransformation {
31    HexEncode,
32    HexDecode,
33    Base58Encode,
34    Base58Decode,
35    ToString,
36    ToNumber,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40pub enum PortablePopulationStrategy {
41    SetOnce,
42    LastWrite,
43    Append,
44    Merge,
45    Max,
46    Sum,
47    Count,
48    Min,
49    UniqueCount,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
53pub struct PortableComputedFieldSpec {
54    pub target_path: String,
55    pub expression: PortableComputedExpr,
56    pub result_type: String,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
60pub enum PortableComputedExpr {
61    FieldRef {
62        path: String,
63    },
64    UnwrapOr {
65        expr: Box<Self>,
66        default: Value,
67    },
68    Binary {
69        op: PortableBinaryOp,
70        left: Box<Self>,
71        right: Box<Self>,
72    },
73    Cast {
74        expr: Box<Self>,
75        to_type: String,
76    },
77    MethodCall {
78        expr: Box<Self>,
79        method: String,
80        args: Vec<Self>,
81    },
82    ResolverComputed {
83        resolver: String,
84        method: String,
85        args: Vec<Self>,
86    },
87    Literal {
88        value: Value,
89    },
90    Paren {
91        expr: Box<Self>,
92    },
93    Var {
94        name: String,
95    },
96    Let {
97        name: String,
98        value: Box<Self>,
99        body: Box<Self>,
100    },
101    If {
102        condition: Box<Self>,
103        then_branch: Box<Self>,
104        else_branch: Box<Self>,
105    },
106    None,
107    Some {
108        value: Box<Self>,
109    },
110    Slice {
111        expr: Box<Self>,
112        start: usize,
113        end: usize,
114    },
115    Index {
116        expr: Box<Self>,
117        index: usize,
118    },
119    U64FromLeBytes {
120        bytes: Box<Self>,
121    },
122    U64FromBeBytes {
123        bytes: Box<Self>,
124    },
125    ByteArray {
126        bytes: Vec<u8>,
127    },
128    Closure {
129        param: String,
130        body: Box<Self>,
131    },
132    Unary {
133        op: PortableUnaryOp,
134        expr: Box<Self>,
135    },
136    JsonToBytes {
137        expr: Box<Self>,
138    },
139    ContextSlot,
140    ContextTimestamp,
141    Keccak256 {
142        expr: Box<Self>,
143    },
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
147pub enum PortableBinaryOp {
148    Add,
149    Sub,
150    Mul,
151    Div,
152    Mod,
153    Gt,
154    Lt,
155    Gte,
156    Lte,
157    Eq,
158    Ne,
159    And,
160    Or,
161    Xor,
162    BitAnd,
163    BitOr,
164    Shl,
165    Shr,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
169pub enum PortableUnaryOp {
170    Not,
171    ReverseBits,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
175#[serde(rename_all = "lowercase")]
176pub enum PortableResolverType {
177    Token,
178    Url(PortableUrlResolverConfig),
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
182#[serde(rename_all = "lowercase")]
183pub enum PortableHttpMethod {
184    #[default]
185    Get,
186    Post,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
190pub enum PortableUrlTemplatePart {
191    Literal(String),
192    FieldRef(String),
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
196pub enum PortableUrlSource {
197    FieldPath(String),
198    Template(Vec<PortableUrlTemplatePart>),
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
202pub struct PortableUrlResolverConfig {
203    pub url_source: PortableUrlSource,
204    #[serde(default)]
205    pub method: PortableHttpMethod,
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub extract_path: Option<String>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
211pub struct PortableResolverExtractSpec {
212    pub target_path: String,
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub source_path: Option<String>,
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub transform: Option<PortableTransformation>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
220pub enum PortableResolveStrategy {
221    #[default]
222    SetOnce,
223    LastWrite,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
227pub struct PortableResolverCondition {
228    pub field_path: String,
229    pub op: PortableComparisonOp,
230    pub value: Value,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
234pub struct PortableResolverSpec {
235    pub resolver: PortableResolverType,
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub input_path: Option<String>,
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub input_value: Option<Value>,
240    #[serde(default)]
241    pub strategy: PortableResolveStrategy,
242    pub extracts: Vec<PortableResolverExtractSpec>,
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    pub condition: Option<PortableResolverCondition>,
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub schedule_at: Option<String>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
250pub struct PortableIdentitySpec {
251    pub primary_keys: Vec<String>,
252    pub lookup_indexes: Vec<PortableLookupIndexSpec>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
256pub struct PortableLookupIndexSpec {
257    pub field_name: String,
258    pub temporal_field: Option<String>,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
262pub struct PortableHandlerSpec {
263    pub source: PortableSourceSpec,
264    pub key_resolution: PortableKeyResolutionStrategy,
265    pub mappings: Vec<PortableFieldMapping>,
266    pub conditions: Vec<PortableCondition>,
267    pub emit: bool,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
271pub enum PortableKeyResolutionStrategy {
272    Embedded {
273        primary_field: PortableFieldPath,
274    },
275    Lookup {
276        primary_field: PortableFieldPath,
277    },
278    Computed {
279        primary_field: PortableFieldPath,
280        compute_partition: PortableComputeFunction,
281    },
282    TemporalLookup {
283        lookup_field: PortableFieldPath,
284        timestamp_field: PortableFieldPath,
285        index_name: String,
286    },
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
290pub enum PortableSourceSpec {
291    Source {
292        program_id: Option<String>,
293        discriminator: Option<Vec<u8>>,
294        type_name: String,
295        #[serde(default, skip_serializing_if = "Option::is_none")]
296        serialization: Option<IdlSerializationSnapshot>,
297        #[serde(default)]
298        is_account: bool,
299    },
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
303pub struct PortableFieldMapping {
304    pub target_path: String,
305    pub source: PortableMappingSource,
306    pub transform: Option<PortableTransformation>,
307    pub population: PortablePopulationStrategy,
308    #[serde(default, skip_serializing_if = "Option::is_none")]
309    pub condition: Option<PortableConditionExpr>,
310    #[serde(default, skip_serializing_if = "Option::is_none")]
311    pub when: Option<String>,
312    #[serde(default, skip_serializing_if = "Option::is_none")]
313    pub stop: Option<String>,
314    #[serde(default = "default_true", skip_serializing_if = "is_true")]
315    pub emit: bool,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
319pub enum PortableMappingSource {
320    FromSource {
321        path: PortableFieldPath,
322        default: Option<Value>,
323        transform: Option<PortableTransformation>,
324    },
325    Constant(Value),
326    Computed {
327        inputs: Vec<PortableFieldPath>,
328        function: PortableComputeFunction,
329    },
330    FromState {
331        path: String,
332    },
333    AsEvent {
334        fields: Vec<PortableMappingSource>,
335    },
336    WholeSource,
337    AsCapture {
338        field_transforms: BTreeMap<String, PortableTransformation>,
339    },
340    FromContext {
341        field: String,
342    },
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
346pub enum PortableComputeFunction {
347    Sum,
348    Concat,
349    Format(String),
350    Custom(String),
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
354pub struct PortableCondition {
355    pub field: PortableFieldPath,
356    pub operator: PortableConditionOp,
357    pub value: Value,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
361pub enum PortableConditionOp {
362    Equals,
363    NotEquals,
364    GreaterThan,
365    LessThan,
366    Contains,
367    Exists,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
371pub struct PortableEntitySection {
372    pub name: String,
373    pub fields: Vec<PortableFieldTypeInfo>,
374    #[serde(default)]
375    pub is_nested_struct: bool,
376    #[serde(default)]
377    pub parent_field: Option<String>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
381pub struct PortableFieldTypeInfo {
382    pub field_name: String,
383    #[serde(default, skip_serializing_if = "Option::is_none")]
384    pub raw_name: Option<String>,
385    #[serde(default, skip_serializing_if = "Option::is_none")]
386    pub canonical_name: Option<String>,
387    pub rust_type_name: String,
388    pub base_type: PortableBaseType,
389    #[serde(default, skip_serializing_if = "Option::is_none")]
390    pub integer_kind: Option<PortableIntegerKind>,
391    pub is_optional: bool,
392    pub is_array: bool,
393    #[serde(default)]
394    pub inner_type: Option<String>,
395    #[serde(default)]
396    pub source_path: Option<String>,
397    #[serde(default)]
398    pub resolved_type: Option<PortableResolvedStructType>,
399    #[serde(default = "default_true", skip_serializing_if = "is_true")]
400    pub emit: bool,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
404pub struct PortableResolvedStructType {
405    pub type_name: String,
406    pub fields: Vec<PortableResolvedField>,
407    pub is_instruction: bool,
408    pub is_account: bool,
409    pub is_event: bool,
410    #[serde(default)]
411    pub is_enum: bool,
412    #[serde(default)]
413    pub enum_variants: Vec<String>,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
417pub struct PortableResolvedField {
418    pub field_name: String,
419    #[serde(default, skip_serializing_if = "Option::is_none")]
420    pub raw_name: Option<String>,
421    #[serde(default, skip_serializing_if = "Option::is_none")]
422    pub canonical_name: Option<String>,
423    pub field_type: String,
424    pub base_type: PortableBaseType,
425    #[serde(default, skip_serializing_if = "Option::is_none")]
426    pub integer_kind: Option<PortableIntegerKind>,
427    pub is_optional: bool,
428    pub is_array: bool,
429}
430
431#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
432pub enum PortableIntegerKind {
433    U8,
434    U16,
435    U32,
436    U64,
437    U128,
438    Usize,
439    I8,
440    I16,
441    I32,
442    I64,
443    I128,
444    Isize,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
448pub enum PortableBaseType {
449    Integer,
450    Float,
451    String,
452    Boolean,
453    Object,
454    Array,
455    Binary,
456    Timestamp,
457    Pubkey,
458    Any,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
462pub struct PortableResolverHook {
463    pub account_type: String,
464    pub strategy: PortableResolverStrategy,
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
468pub enum PortableResolverStrategy {
469    PdaReverseLookup {
470        lookup_name: String,
471        queue_discriminators: Vec<Vec<u8>>,
472    },
473    DirectField {
474        field_path: PortableFieldPath,
475    },
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
479pub struct PortableInstructionHook {
480    pub instruction_type: String,
481    pub actions: Vec<PortableHookAction>,
482    pub lookup_by: Option<PortableFieldPath>,
483}
484
485#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
486pub enum PortableHookAction {
487    RegisterPdaMapping {
488        pda_field: PortableFieldPath,
489        seed_field: PortableFieldPath,
490        lookup_name: String,
491    },
492    SetField {
493        target_field: String,
494        source: PortableMappingSource,
495        condition: Option<PortableConditionExpr>,
496    },
497    IncrementField {
498        target_field: String,
499        increment_by: i64,
500        condition: Option<PortableConditionExpr>,
501    },
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
505pub struct PortableConditionExpr {
506    pub expression: String,
507    pub parsed: Option<PortableParsedCondition>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
511pub enum PortableParsedCondition {
512    Comparison {
513        field: PortableFieldPath,
514        op: PortableComparisonOp,
515        value: Value,
516    },
517    Logical {
518        op: PortableLogicalOp,
519        conditions: Vec<Self>,
520    },
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
524pub enum PortableComparisonOp {
525    Equal,
526    NotEqual,
527    GreaterThan,
528    GreaterThanOrEqual,
529    LessThan,
530    LessThanOrEqual,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
534pub enum PortableLogicalOp {
535    And,
536    Or,
537}
538
539#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
540#[serde(rename_all = "lowercase")]
541pub enum PortableSortOrder {
542    #[default]
543    Asc,
544    Desc,
545}
546
547#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
548pub enum PortableCompareOp {
549    Eq,
550    Ne,
551    Gt,
552    Gte,
553    Lt,
554    Lte,
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
558pub enum PortablePredicateValue {
559    Literal(Value),
560    Dynamic(String),
561    Field(PortableFieldPath),
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
565pub enum PortablePredicate {
566    Compare {
567        field: PortableFieldPath,
568        op: PortableCompareOp,
569        value: PortablePredicateValue,
570    },
571    And(Vec<Self>),
572    Or(Vec<Self>),
573    Not(Box<Self>),
574    Exists {
575        field: PortableFieldPath,
576    },
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
580pub enum PortableViewTransform {
581    Filter {
582        predicate: PortablePredicate,
583    },
584    Sort {
585        key: PortableFieldPath,
586        #[serde(default)]
587        order: PortableSortOrder,
588    },
589    Take {
590        count: usize,
591    },
592    Skip {
593        count: usize,
594    },
595    First,
596    Last,
597    MaxBy {
598        key: PortableFieldPath,
599    },
600    MinBy {
601        key: PortableFieldPath,
602    },
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
606pub enum PortableViewSource {
607    Entity { name: String },
608    View { id: String },
609}
610
611#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
612pub enum PortableViewOutput {
613    #[default]
614    Collection,
615    Single,
616    Keyed {
617        key_field: PortableFieldPath,
618    },
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
622pub struct PortableView {
623    pub id: String,
624    pub source: PortableViewSource,
625    #[serde(default)]
626    pub pipeline: Vec<PortableViewTransform>,
627    #[serde(default)]
628    pub output: PortableViewOutput,
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
632pub struct PortableEntity {
633    pub state_name: String,
634    #[serde(default, skip_serializing_if = "Option::is_none")]
635    pub program_id: Option<String>,
636    pub identity: PortableIdentitySpec,
637    pub handlers: Vec<PortableHandlerSpec>,
638    pub sections: Vec<PortableEntitySection>,
639    pub field_mappings: BTreeMap<String, PortableFieldTypeInfo>,
640    pub resolver_hooks: Vec<PortableResolverHook>,
641    pub instruction_hooks: Vec<PortableInstructionHook>,
642    #[serde(default)]
643    pub resolver_specs: Vec<PortableResolverSpec>,
644    #[serde(default)]
645    pub computed_fields: Vec<String>,
646    #[serde(default)]
647    pub computed_field_specs: Vec<PortableComputedFieldSpec>,
648    #[serde(default)]
649    pub views: Vec<PortableView>,
650}
651
652impl PortableEntity {
653    pub fn new(state_name: impl Into<String>, primary_key: impl Into<String>) -> Self {
654        Self {
655            state_name: state_name.into(),
656            program_id: None,
657            identity: PortableIdentitySpec {
658                primary_keys: vec![primary_key.into()],
659                lookup_indexes: Vec::new(),
660            },
661            handlers: Vec::new(),
662            sections: Vec::new(),
663            field_mappings: BTreeMap::new(),
664            resolver_hooks: Vec::new(),
665            instruction_hooks: Vec::new(),
666            resolver_specs: Vec::new(),
667            computed_fields: Vec::new(),
668            computed_field_specs: Vec::new(),
669            views: Vec::new(),
670        }
671    }
672
673    pub fn validate(&self) -> Result<(), ArtifactError> {
674        if self.state_name.is_empty() {
675            return Err(ArtifactError::InvalidArtifact(
676                "entity stateName must not be empty".to_string(),
677            ));
678        }
679        let primary_keys = self
680            .identity
681            .primary_keys
682            .iter()
683            .map(String::as_str)
684            .collect::<BTreeSet<_>>();
685        if primary_keys.len() != self.identity.primary_keys.len() || primary_keys.contains("") {
686            return Err(ArtifactError::InvalidArtifact(format!(
687                "entity '{}' primary keys must be unique and non-empty",
688                self.state_name
689            )));
690        }
691        let mut view_ids = BTreeSet::new();
692        for view in &self.views {
693            if view.id.is_empty() || !view_ids.insert(view.id.as_str()) {
694                return Err(ArtifactError::InvalidArtifact(format!(
695                    "entity '{}' contains an empty or duplicate view ID",
696                    self.state_name
697                )));
698            }
699            if !matches!(
700                &view.source,
701                PortableViewSource::Entity { name } if name == &self.state_name
702            ) && !matches!(&view.source, PortableViewSource::View { .. })
703            {
704                return Err(ArtifactError::InvalidArtifact(format!(
705                    "view '{}' references a different entity",
706                    view.id
707                )));
708            }
709        }
710        Ok(())
711    }
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
715#[serde(rename_all = "camelCase", deny_unknown_fields)]
716pub struct ProgramRequirementV2 {
717    pub program_id: String,
718    pub program_spec_hash: HashId<ProgramSpec>,
719}
720
721#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
722#[serde(rename_all = "camelCase", deny_unknown_fields)]
723pub struct InstructionResolutionAdapterV2 {
724    pub instruction: String,
725    pub accounts: BTreeMap<String, AccountResolutionV1>,
726}
727
728#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
729#[serde(rename_all = "camelCase", deny_unknown_fields)]
730pub struct ProgramAdapterV2 {
731    pub program_spec_hash: HashId<ProgramSpec>,
732    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
733    pub pdas: BTreeMap<String, PdaDefinitionV1>,
734    #[serde(default, skip_serializing_if = "Vec::is_empty")]
735    pub instruction_resolutions: Vec<InstructionResolutionAdapterV2>,
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
739#[serde(rename_all = "camelCase", deny_unknown_fields)]
740pub struct LiveSpecV2 {
741    pub schema: String,
742    pub compiler_contract_version: String,
743    pub wire_contract_version: String,
744    pub programs: Vec<ProgramRequirementV2>,
745    pub entities: Vec<PortableEntity>,
746    #[serde(default, skip_serializing_if = "Vec::is_empty")]
747    pub program_adapters: Vec<ProgramAdapterV2>,
748}
749
750impl LiveSpecV2 {
751    pub fn new(
752        programs: Vec<ProgramRequirementV2>,
753        entities: Vec<PortableEntity>,
754        program_adapters: Vec<ProgramAdapterV2>,
755    ) -> Self {
756        Self {
757            schema: LIVE_SPEC_SCHEMA_V2.to_string(),
758            compiler_contract_version: LIVE_COMPILER_CONTRACT_V1.to_string(),
759            wire_contract_version: LIVE_WIRE_CONTRACT_V1.to_string(),
760            programs,
761            entities,
762            program_adapters,
763        }
764    }
765
766    pub fn validate(&self) -> Result<(), ArtifactError> {
767        if self.schema != LIVE_SPEC_SCHEMA_V2 {
768            return Err(ArtifactError::UnsupportedVersion {
769                artifact: LIVE_SPEC_KIND,
770                version: self.schema.clone(),
771            });
772        }
773        if self.compiler_contract_version.is_empty() || self.wire_contract_version.is_empty() {
774            return Err(ArtifactError::InvalidArtifact(
775                "live compiler and wire contract versions must not be empty".to_string(),
776            ));
777        }
778        let mut program_hashes = BTreeSet::new();
779        for program in &self.programs {
780            if program.program_id.is_empty()
781                || !program_hashes.insert(program.program_spec_hash.to_string())
782            {
783                return Err(ArtifactError::InvalidArtifact(
784                    "live program requirements must have unique hashes and non-empty program IDs"
785                        .to_string(),
786                ));
787            }
788        }
789        let mut entity_names = BTreeSet::new();
790        let mut all_view_ids = BTreeSet::new();
791        for entity in &self.entities {
792            entity.validate()?;
793            if !entity_names.insert(entity.state_name.as_str()) {
794                return Err(ArtifactError::InvalidArtifact(format!(
795                    "duplicate entity '{}'",
796                    entity.state_name
797                )));
798            }
799            for view in &entity.views {
800                if !all_view_ids.insert(view.id.as_str()) {
801                    return Err(ArtifactError::InvalidArtifact(format!(
802                        "duplicate LiveSpec view ID '{}'",
803                        view.id
804                    )));
805                }
806            }
807        }
808        let mut adapter_hashes = BTreeSet::new();
809        for adapter in &self.program_adapters {
810            if !program_hashes.contains(&adapter.program_spec_hash.to_string())
811                || !adapter_hashes.insert(adapter.program_spec_hash.to_string())
812            {
813                return Err(ArtifactError::InvalidArtifact(
814                    "program adapters must uniquely reference a required ProgramSpec".to_string(),
815                ));
816            }
817            let mut instruction_names = BTreeSet::new();
818            for instruction in &adapter.instruction_resolutions {
819                if instruction.instruction.is_empty()
820                    || instruction.accounts.is_empty()
821                    || !instruction_names.insert(instruction.instruction.as_str())
822                {
823                    return Err(ArtifactError::InvalidArtifact(
824                        "instruction adapters must have unique names and non-empty account refinements"
825                            .to_string(),
826                    ));
827                }
828            }
829        }
830        reject_private_fields(&serde_json::to_value(self).map_err(json_error)?)
831    }
832
833    pub fn view_ids(&self) -> BTreeSet<&str> {
834        self.entities
835            .iter()
836            .flat_map(|entity| entity.views.iter().map(|view| view.id.as_str()))
837            .collect()
838    }
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
842#[serde(rename_all = "camelCase", deny_unknown_fields)]
843pub struct LiveSpecArtifactV2 {
844    pub artifact_version: String,
845    pub kind: String,
846    pub artifact_hash: HashId<LiveSpec>,
847    pub payload: LiveSpecV2,
848}
849
850impl LiveSpecArtifactV2 {
851    pub fn new(payload: LiveSpecV2) -> Result<Self, ArtifactError> {
852        payload.validate()?;
853        let artifact_hash = hash_jcs(&LiveSpecProjection {
854            artifact_version: ARTIFACT_VERSION_V1,
855            kind: LIVE_SPEC_KIND,
856            payload: &payload,
857        })?;
858        Ok(Self {
859            artifact_version: ARTIFACT_VERSION_V1.to_string(),
860            kind: LIVE_SPEC_KIND.to_string(),
861            artifact_hash,
862            payload,
863        })
864    }
865
866    pub fn validate(&self) -> Result<(), ArtifactError> {
867        validate_envelope_version(&self.artifact_version, LIVE_SPEC_KIND)?;
868        validate_kind(&self.kind, LIVE_SPEC_KIND)?;
869        self.payload.validate()?;
870        let expected = hash_jcs(&LiveSpecProjection {
871            artifact_version: &self.artifact_version,
872            kind: LIVE_SPEC_KIND,
873            payload: &self.payload,
874        })?;
875        if expected != self.artifact_hash {
876            return Err(ArtifactError::HashMismatch);
877        }
878        Ok(())
879    }
880
881    pub fn canonical_bytes(&self) -> Result<Vec<u8>, ArtifactError> {
882        self.validate()?;
883        arete_hash::canonicalize_jcs(self).map_err(Into::into)
884    }
885}
886
887#[derive(Serialize)]
888#[serde(rename_all = "camelCase")]
889struct LiveSpecProjection<'a> {
890    artifact_version: &'a str,
891    kind: &'static str,
892    payload: &'a LiveSpecV2,
893}
894
895pub fn load_live_spec_v2(
896    bytes: &[u8],
897) -> Result<crate::LoadedArtifact<LiveSpecArtifactV2>, ArtifactError> {
898    let value = arete_hash::parse_json_bytes_strict(bytes)?;
899    let artifact: LiveSpecArtifactV2 = serde_json::from_value(value).map_err(json_error)?;
900    artifact.validate()?;
901    Ok(crate::LoadedArtifact {
902        artifact,
903        original_bytes: bytes.to_vec(),
904        source_hash: arete_hash::hash_raw_bytes(bytes)?,
905    })
906}
907
908fn default_true() -> bool {
909    true
910}
911
912fn is_true(value: &bool) -> bool {
913    *value
914}