1pub mod definitions;
2pub mod error;
3mod memo_program;
4pub mod parameter;
5pub mod schema;
6pub mod validator;
7
8use crate::{
9 config::{self, PlutusVersion, ProjectConfig},
10 module::CheckedModules,
11};
12use aiken_lang::gen_uplc::CodeGenerator;
13use definitions::Definitions;
14pub use error::Error;
15use pallas_addresses::ScriptHash;
16use schema::{Annotated, Schema};
17use std::{
18 collections::{BTreeSet, HashMap},
19 fmt::Debug,
20};
21use uplc::{PlutusData, ast::SerializableProgram, tx::script_context::PlutusScript};
22use validator::Validator;
23
24#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
25pub struct Blueprint {
26 pub preamble: Preamble,
27 pub validators: Vec<Validator<SerializableProgram>>,
28 #[serde(skip_serializing_if = "Definitions::is_empty", default)]
29 pub definitions: Definitions<Annotated<Schema>>,
30}
31
32#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct Preamble {
35 pub title: String,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub description: Option<String>,
39
40 pub version: String,
41
42 pub plutus_version: PlutusVersion,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub compiler: Option<Compiler>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub license: Option<String>,
49}
50
51#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct Compiler {
54 pub name: String,
55 pub version: String,
56}
57
58#[derive(Debug, PartialEq, Clone)]
59pub enum LookupResult<'a, T> {
60 One(String, &'a T),
61 Many,
62}
63
64impl Blueprint {
65 pub fn new(
66 config: &ProjectConfig,
67 modules: &CheckedModules,
68 generator: &mut CodeGenerator,
69 ) -> Result<Self, Error> {
70 let preamble = config.into();
71
72 let mut definitions = Definitions::new();
73
74 let validators: Result<Vec<_>, Error> = modules
75 .validators()
76 .map(|(validator, def)| {
77 Ok(Validator::from_checked_module(
78 modules,
79 generator,
80 validator,
81 def,
82 &config.plutus,
83 )?
84 .into_iter()
85 .map(|mut schema| {
86 definitions.merge(&mut schema.definitions);
87 schema.definitions = Definitions::new();
88 schema
89 })
90 .collect::<Vec<_>>())
91 })
92 .collect();
93
94 Ok(Blueprint {
95 preamble,
96 validators: validators?.into_iter().flatten().collect(),
97 definitions,
98 })
99 }
100}
101
102impl Blueprint {
103 pub fn lookup(
104 &self,
105 want_module_name: Option<&str>,
106 want_validator_name: Option<&str>,
107 ) -> Option<LookupResult<'_, Validator<SerializableProgram>>> {
108 let mut validator = None;
109
110 for v in self.validators.iter() {
111 let (known_module_name, known_validator_name) = v.get_module_and_name();
112
113 let is_target = match (want_module_name, want_validator_name) {
114 (None, None) => true,
115 (Some(want_module_name), None) => want_module_name == known_module_name,
116 (None, Some(want_validator_name)) => want_validator_name == known_validator_name,
117 (Some(want_module_name), Some(want_validator_name)) => {
118 want_module_name == known_module_name
119 && want_validator_name == known_validator_name
120 }
121 };
122
123 let title = format!("{known_module_name}.{known_validator_name}");
124
125 if is_target {
126 match validator {
127 Some(LookupResult::Many) => (),
128 None => {
129 validator = Some(LookupResult::One(title, v));
130 }
131 Some(LookupResult::One(ref known_title, _)) => {
132 if title.as_str() != known_title {
133 validator = Some(LookupResult::Many)
134 }
135 }
136 }
137 }
138 }
139
140 validator
141 }
142
143 pub fn construct_parameter_incrementally<F>(
144 &self,
145 module_name: Option<&str>,
146 validator_name: Option<&str>,
147 ask: F,
148 ) -> Result<PlutusData, Error>
149 where
150 F: Fn(&Annotated<Schema>, &Definitions<Annotated<Schema>>) -> Result<PlutusData, Error>,
151 {
152 let when_too_many =
154 |known_validators| Error::MoreThanOneValidatorFound { known_validators };
155 let when_missing = |known_validators| Error::NoValidatorNotFound { known_validators };
156
157 self.with_validator(
158 module_name,
159 validator_name,
160 when_too_many,
161 when_missing,
162 |validator| validator.ask_next_parameter(&self.definitions, &ask),
163 )
164 }
165
166 pub fn apply_parameter(
167 &mut self,
168 module_name: Option<&str>,
169 validator_name: Option<&str>,
170 param: &PlutusData,
171 ) -> Result<(), Error> {
172 let when_too_many =
173 |known_validators| Error::MoreThanOneValidatorFound { known_validators };
174 let when_missing = |known_validators| Error::NoValidatorNotFound { known_validators };
175
176 let applied_validator = self.with_validator(
177 module_name,
178 validator_name,
179 when_too_many,
180 when_missing,
181 |validator| validator.clone().apply(&self.definitions, param),
182 )?;
183
184 let prefix = |v: &str| v.split('.').take(2).collect::<Vec<&str>>().join(".");
186 for validator in &mut self.validators {
187 if prefix(&applied_validator.title) == prefix(&validator.title) {
188 validator.program = applied_validator.program.clone();
189 validator.parameters = applied_validator.parameters.clone();
190 }
191 }
192
193 Ok(())
194 }
195
196 pub fn with_validator<F, A, E>(
197 &self,
198 module_name: Option<&str>,
199 validator_name: Option<&str>,
200 when_too_many: fn(BTreeSet<(String, String, bool)>) -> E,
201 when_missing: fn(BTreeSet<(String, String, bool)>) -> E,
202 action: F,
203 ) -> Result<A, E>
204 where
205 F: Fn(&Validator<SerializableProgram>) -> Result<A, E>,
206 {
207 match self.lookup(module_name, validator_name) {
208 Some(LookupResult::One(_, validator)) => action(validator),
209 Some(LookupResult::Many) => Err(when_too_many(
210 self.validators
211 .iter()
212 .filter_map(|v| {
213 let (l, r) = v.get_module_and_name();
214
215 if let Some(module_name) = module_name {
216 if l != module_name {
217 return None;
218 }
219 }
220
221 if let Some(validator_name) = validator_name {
222 if r != validator_name {
223 return None;
224 }
225 }
226
227 Some((l.to_string(), r.to_string(), !v.parameters.is_empty()))
228 })
229 .collect(),
230 )),
231 None => Err(when_missing(
232 self.validators
233 .iter()
234 .map(|v| {
235 let (l, r) = v.get_module_and_name();
236 (l.to_string(), r.to_string(), !v.parameters.is_empty())
237 })
238 .collect(),
239 )),
240 }
241 }
242}
243
244impl From<Blueprint> for HashMap<ScriptHash, PlutusScript> {
245 fn from(blueprint: Blueprint) -> Self {
246 blueprint
247 .validators
248 .iter()
249 .map(|validator| validator.program.compiled_code_and_hash())
250 .collect()
251 }
252}
253impl From<&ProjectConfig> for Preamble {
254 fn from(config: &ProjectConfig) -> Self {
255 Preamble {
256 title: config.name.to_string(),
257 description: if config.description.is_empty() {
258 None
259 } else {
260 Some(config.description.clone())
261 },
262 compiler: Some(Compiler {
263 name: "Aiken".to_string(),
264 version: config::compiler_version(true),
265 }),
266 plutus_version: config.plutus,
267 version: config.version.clone(),
268 license: config.license.clone(),
269 }
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use aiken_lang::tipo::Type;
277 use schema::{Data, Declaration, Items, Schema};
278 use serde_json::{self, json};
279 use std::collections::HashMap;
280
281 #[test]
282 fn serialize_no_description() {
283 let blueprint = Blueprint {
284 preamble: Preamble {
285 title: "Foo".to_string(),
286 description: None,
287 version: "1.0.0".to_string(),
288 plutus_version: PlutusVersion::V2,
289 compiler: Some(Compiler {
290 name: "Aiken".to_string(),
291 version: "1.0.0".to_string(),
292 }),
293 license: Some("Apache-2.0".to_string()),
294 },
295 validators: vec![],
296 definitions: Definitions::new(),
297 };
298 assert_eq!(
299 serde_json::to_value(&blueprint).unwrap(),
300 json!({
301 "preamble": {
302 "title": "Foo",
303 "version": "1.0.0",
304 "plutusVersion": "v2",
305 "compiler": {
306 "name": "Aiken",
307 "version": "1.0.0"
308 },
309 "license": "Apache-2.0"
310 },
311 "validators": []
312 }),
313 );
314 }
315
316 #[test]
317 fn serialize_with_description() {
318 let blueprint = Blueprint {
319 preamble: Preamble {
320 title: "Foo".to_string(),
321 description: Some("Lorem ipsum".to_string()),
322 version: "1.0.0".to_string(),
323 plutus_version: PlutusVersion::V2,
324 compiler: None,
325 license: None,
326 },
327 validators: vec![],
328 definitions: Definitions::new(),
329 };
330 assert_eq!(
331 serde_json::to_value(&blueprint).unwrap(),
332 json!({
333 "preamble": {
334 "title": "Foo",
335 "description": "Lorem ipsum",
336 "version": "1.0.0",
337 "plutusVersion": "v2"
338 },
339 "validators": []
340 }),
341 );
342 }
343
344 #[test]
345 fn serialize_with_definitions() {
346 let mut definitions = Definitions::new();
347 definitions
348 .register::<_, Error>(&Type::int(), &HashMap::new(), |_| {
349 Ok(Schema::Data(Data::Integer).into())
350 })
351 .unwrap();
352 definitions
353 .register::<_, Error>(
354 &Type::list(Type::byte_array()),
355 &HashMap::new(),
356 |definitions| {
357 let ref_bytes = definitions.register::<_, Error>(
358 &Type::byte_array(),
359 &HashMap::new(),
360 |_| Ok(Schema::Data(Data::Bytes).into()),
361 )?;
362 Ok(
363 Schema::Data(Data::List(Items::One(Declaration::Referenced(ref_bytes))))
364 .into(),
365 )
366 },
367 )
368 .unwrap();
369
370 let blueprint = Blueprint {
371 preamble: Preamble {
372 title: "Foo".to_string(),
373 description: None,
374 version: "1.0.0".to_string(),
375 plutus_version: PlutusVersion::V2,
376 compiler: None,
377 license: None,
378 },
379 validators: vec![],
380 definitions,
381 };
382 assert_eq!(
383 serde_json::to_value(&blueprint).unwrap(),
384 json!({
385 "preamble": {
386 "title": "Foo",
387 "version": "1.0.0",
388 "plutusVersion": "v2"
389 },
390 "validators": [],
391 "definitions": {
392 "ByteArray": {
393 "dataType": "bytes"
394 },
395 "Int": {
396 "dataType": "integer"
397 },
398 "List<ByteArray>": {
399 "dataType": "list",
400 "items": {
401 "$ref": "#/definitions/ByteArray"
402 }
403 }
404 }
405 }),
406 );
407 }
408}