1use std::collections::{BTreeMap, BTreeSet};
6
7use arete_artifacts::{
8 decompose_legacy_stack, resolve_stack_composition_v2, LegacyDecomposition, LiveSpecArtifact,
9 LiveSpecArtifactV2, ProgramSpecArtifact, ResolvedLiveSpecV2, StackManifestArtifact,
10 StackManifestArtifactV2,
11};
12use serde::de::DeserializeOwned;
13use serde::Serialize;
14
15use crate::ast::{InstructionDef, PdaDefinition, SerializableStackSpec, CURRENT_AST_VERSION};
16
17#[derive(Debug, Clone)]
18pub struct AliasedStackSpecV2 {
19 pub alias: String,
20 pub live_spec_hash: String,
21 pub stack_spec: SerializableStackSpec,
22}
23
24#[derive(Debug, Clone)]
25pub struct ComposedStackSpecsV2 {
26 pub name: String,
27 pub live_specs: Vec<AliasedStackSpecV2>,
28}
29
30pub fn decompose_stack_spec(
31 stack_spec: &SerializableStackSpec,
32) -> Result<LegacyDecomposition, String> {
33 let bytes = serde_json::to_vec(stack_spec).map_err(|error| error.to_string())?;
34 decompose_legacy_stack(&bytes).map_err(|error| error.to_string())
35}
36
37pub fn stack_spec_from_program_artifacts(
38 name: impl Into<String>,
39 programs: &[ProgramSpecArtifact],
40) -> Result<SerializableStackSpec, String> {
41 for program in programs {
42 program.validate().map_err(|error| error.to_string())?;
43 }
44 let (program_ids, idls, program_specs, pdas, instructions) = program_inputs(programs)?;
45 Ok(SerializableStackSpec {
46 ast_version: CURRENT_AST_VERSION.to_string(),
47 stack_name: name.into(),
48 program_ids,
49 idls,
50 program_specs,
51 entities: Vec::new(),
52 pdas,
53 instructions,
54 content_hash: None,
55 })
56}
57
58pub fn stack_spec_from_artifacts(
59 programs: &[ProgramSpecArtifact],
60 live_spec: &LiveSpecArtifact,
61 manifest: &StackManifestArtifact,
62) -> Result<SerializableStackSpec, String> {
63 live_spec.validate().map_err(|error| error.to_string())?;
64 manifest.validate().map_err(|error| error.to_string())?;
65 let mut stack = stack_spec_from_program_artifacts(&manifest.payload.name, programs)?;
66
67 let program_hashes = programs
68 .iter()
69 .map(|program| program.artifact_hash)
70 .collect::<Vec<_>>();
71 let live_program_hashes = live_spec
72 .payload
73 .programs
74 .iter()
75 .map(|program| program.program_spec_hash)
76 .collect::<Vec<_>>();
77 let manifest_program_hashes = manifest
78 .payload
79 .programs
80 .iter()
81 .map(|program| program.artifact_hash)
82 .collect::<Vec<_>>();
83 if program_hashes != live_program_hashes || program_hashes != manifest_program_hashes {
84 return Err(
85 "ProgramSpec order must match LiveSpec and StackManifest requirements".to_string(),
86 );
87 }
88 if manifest.payload.live_specs.len() != 1
89 || manifest.payload.live_specs[0].artifact_hash != live_spec.artifact_hash
90 {
91 return Err("StackManifest must reference the supplied LiveSpec exactly once".to_string());
92 }
93
94 stack.entities = transcode(&live_spec.payload.entities)?;
95 if let Some(extensions) = &live_spec.payload.legacy_program_extensions {
96 stack.pdas = transcode(&extensions.pdas)?;
97 stack.instructions = transcode(&extensions.instructions)?;
98 }
99 Ok(stack)
100}
101
102pub fn stack_spec_from_artifacts_v2(
105 programs: &[ProgramSpecArtifact],
106 live_spec: &LiveSpecArtifactV2,
107 manifest: &StackManifestArtifactV2,
108) -> Result<SerializableStackSpec, String> {
109 if manifest.payload.live_specs.len() != 1 {
110 return Err("single-live compatibility requires exactly one aliased LiveSpec".to_string());
111 }
112 let alias = manifest.payload.live_specs[0].alias.clone();
113 let lives = vec![(alias, live_spec.clone())];
114 let mut composed = stack_specs_from_artifacts_v2(programs, &lives, manifest)?;
115 Ok(composed.live_specs.remove(0).stack_spec)
116}
117
118pub fn stack_specs_from_artifacts_v2(
122 programs: &[ProgramSpecArtifact],
123 live_specs: &[(String, LiveSpecArtifactV2)],
124 manifest: &StackManifestArtifactV2,
125) -> Result<ComposedStackSpecsV2, String> {
126 let resolved = resolve_stack_composition_v2(manifest, live_specs, programs)
127 .map_err(|error| error.to_string())?;
128 let multiple = resolved.live_specs.len() > 1;
129 let live_specs = resolved
130 .live_specs
131 .into_iter()
132 .map(|live| {
133 let stack_name = if multiple {
134 format!(
135 "{}{}",
136 identifier_pascal_case(&manifest.payload.name),
137 identifier_pascal_case(&live.alias)
138 )
139 } else {
140 manifest.payload.name.clone()
141 };
142 stack_spec_for_live(stack_name, live)
143 })
144 .collect::<Result<Vec<_>, String>>()?;
145 Ok(ComposedStackSpecsV2 {
146 name: manifest.payload.name.clone(),
147 live_specs,
148 })
149}
150
151fn stack_spec_for_live(
152 stack_name: String,
153 live: ResolvedLiveSpecV2<'_>,
154) -> Result<AliasedStackSpecV2, String> {
155 let programs = live
156 .program_specs
157 .iter()
158 .map(|program| (*program).clone())
159 .collect::<Vec<_>>();
160 let mut stack = stack_spec_from_program_artifacts(stack_name, &programs)?;
161 stack.entities = transcode(&live.artifact.payload.entities)?;
162 let selected = live.selected_views.iter().collect::<BTreeSet<_>>();
163 for entity in &mut stack.entities {
164 entity.views.retain(|view| selected.contains(&view.id));
165 }
166
167 for adapter in &live.artifact.payload.program_adapters {
168 let program = programs
169 .iter()
170 .find(|program| program.artifact_hash == adapter.program_spec_hash)
171 .ok_or_else(|| "program adapter references an unknown ProgramSpec".to_string())?;
172 let program_name = &program.payload.idl_snapshot.snapshot.name;
173 let pdas = stack.pdas.entry(program_name.clone()).or_default();
174 for (name, pda) in &adapter.pdas {
175 pdas.insert(name.clone(), transcode(pda)?);
176 }
177 for resolution in &adapter.instruction_resolutions {
178 let instruction = stack
179 .instructions
180 .iter_mut()
181 .find(|instruction| {
182 instruction.name == resolution.instruction
183 && instruction.program_id.as_deref()
184 == Some(program.payload.program_id.as_str())
185 })
186 .ok_or_else(|| {
187 format!(
188 "program adapter references unknown instruction '{}.{}'",
189 program_name, resolution.instruction
190 )
191 })?;
192 for (account_name, account_resolution) in &resolution.accounts {
193 let account = instruction
194 .accounts
195 .iter_mut()
196 .find(|account| account.name == *account_name)
197 .ok_or_else(|| {
198 format!(
199 "program adapter references unknown account '{}.{}.{}'",
200 program_name, resolution.instruction, account_name
201 )
202 })?;
203 account.resolution = transcode(account_resolution)?;
204 }
205 }
206 }
207 Ok(AliasedStackSpecV2 {
208 alias: live.alias,
209 live_spec_hash: live.artifact.artifact_hash.to_string(),
210 stack_spec: stack,
211 })
212}
213
214type ProgramInputs = (
215 Vec<String>,
216 Vec<arete_idl::snapshot::IdlSnapshot>,
217 Vec<arete_hash::ProgramSpecV1>,
218 BTreeMap<String, BTreeMap<String, PdaDefinition>>,
219 Vec<InstructionDef>,
220);
221
222fn program_inputs(programs: &[ProgramSpecArtifact]) -> Result<ProgramInputs, String> {
223 let mut program_ids = Vec::with_capacity(programs.len());
224 let mut idls = Vec::with_capacity(programs.len());
225 let mut program_specs = Vec::with_capacity(programs.len());
226 let mut pdas = BTreeMap::new();
227 let mut instructions = Vec::new();
228 for program in programs {
229 let payload = &program.payload;
230 program_ids.push(payload.program_id.clone());
231 idls.push(payload.idl_snapshot.snapshot.clone());
232 program_specs.push(payload.clone());
233 let program_pdas = transcode(&payload.pdas)?;
234 pdas.insert(payload.idl_snapshot.snapshot.name.clone(), program_pdas);
235 instructions.extend(transcode::<_, Vec<InstructionDef>>(&payload.instructions)?);
236 }
237 Ok((program_ids, idls, program_specs, pdas, instructions))
238}
239
240fn transcode<T: Serialize, U: DeserializeOwned>(value: &T) -> Result<U, String> {
241 serde_json::from_value(serde_json::to_value(value).map_err(|error| error.to_string())?)
242 .map_err(|error| error.to_string())
243}
244
245fn identifier_pascal_case(value: &str) -> String {
246 let mut output = value
247 .split(|character: char| !character.is_ascii_alphanumeric())
248 .filter(|segment| !segment.is_empty())
249 .map(|segment| {
250 let mut characters = segment.chars();
251 characters
252 .next()
253 .map(|first| first.to_ascii_uppercase().to_string() + characters.as_str())
254 .unwrap_or_default()
255 })
256 .collect::<String>();
257 if output
258 .chars()
259 .next()
260 .is_some_and(|character| character.is_ascii_digit())
261 {
262 output.insert(0, 'A');
263 }
264 output
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use arete_hash::{CanonicalIdlDocument, ProgramSpecV1};
271
272 fn program() -> ProgramSpecArtifact {
273 let idl = br#"{
274 "address":"11111111111111111111111111111111",
275 "metadata":{"name":"system","version":"1.0.0","spec":"0.1.0"},
276 "instructions":[],"accounts":[],"types":[],"events":[],"errors":[]
277 }"#;
278 let document = CanonicalIdlDocument::parse(idl, None).unwrap();
279 ProgramSpecArtifact::new(ProgramSpecV1::from_document(&document)).unwrap()
280 }
281
282 #[test]
283 fn program_artifacts_reconstruct_generator_input_without_hosted_release_state() {
284 let program = program();
285 let stack = stack_spec_from_program_artifacts("SystemProgram", &[program.clone()]).unwrap();
286 assert_eq!(stack.program_ids, vec![program.payload.program_id]);
287 assert_eq!(
288 stack.program_specs[0].hash().unwrap(),
289 program.artifact_hash
290 );
291 assert!(stack.entities.is_empty());
292 }
293}