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}