use crate::{ blueprint::definitions::{Definitions, Reference}, CheckedModule, }; use aiken_lang::{ ast::{Definition, TypedDataType, TypedDefinition}, builtins::wrapped_redeemer, tipo::{pretty, Type, TypeVar}, }; use owo_colors::{OwoColorize, Stream::Stdout}; use serde::{ self, de::{self, Deserialize, Deserializer, MapAccess, Visitor}, ser::{Serialize, SerializeStruct, Serializer}, }; use serde_json as json; use std::{collections::HashMap, fmt, ops::Deref, rc::Rc}; // NOTE: Can be anything BUT 0 pub const REDEEMER_DISCRIMINANT: usize = 1; #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] pub struct Annotated { #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(flatten)] pub annotated: T, } #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] #[serde(untagged)] pub enum Declaration { Referenced(Reference), Inline(Box), } impl<'a, T> Declaration { pub fn reference(&'a self) -> Option<&'a Reference> { match self { Declaration::Referenced(reference) => Some(reference), Declaration::Inline(..) => None, } } fn try_schema( &'a self, definitions: &'a Definitions>, cast: fn(&'a Schema) -> Option<&'a T>, ) -> Option<&'a T> { match self { Declaration::Inline(inner) => Some(inner.deref()), Declaration::Referenced(reference) => definitions .lookup(reference) .and_then(|s| cast(&s.annotated)), } } } impl<'a> Declaration { pub fn schema(&'a self, definitions: &'a Definitions>) -> Option<&'a Data> { self.try_schema(definitions, |s| match s { Schema::Data(data) => Some(data), _ => None, }) } } impl<'a> Declaration { pub fn schema(&'a self, definitions: &'a Definitions>) -> Option<&'a Schema> { self.try_schema(definitions, Some) } } /// A schema for low-level UPLC primitives. #[derive(Debug, PartialEq, Eq, Clone)] pub enum Schema { Unit, Boolean, Integer, Bytes, String, Pair(Declaration, Declaration), List(Items), Data(Data), } /// A schema for Plutus' Data. #[derive(Debug, PartialEq, Eq, Clone)] pub enum Data { Integer, Bytes, List(Items), Map(Declaration, Declaration), AnyOf(Vec>), Opaque, } /// A structure that represents either one or many elements. #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] #[serde(untagged)] pub enum Items { One(Declaration), Many(Vec>), } /// Captures a single UPLC constructor with its #[derive(Debug, PartialEq, Eq, Clone)] pub struct Constructor { pub index: usize, pub fields: Vec>>, } impl From for Annotated { fn from(annotated: T) -> Self { Annotated { title: None, description: None, annotated, } } } impl Annotated { pub fn as_wrapped_redeemer( definitions: &mut Definitions>, schema: Reference, type_info: Rc, ) -> Reference { definitions .register( &wrapped_redeemer(type_info), &HashMap::new(), |_| { Ok::<_, Error>(Annotated { title: Some("Wrapped Redeemer".to_string()), description: Some("A redeemer wrapped in an extra constructor to make multi-validator detection possible on-chain.".to_string()), annotated: Schema::Data(Data::AnyOf(vec![Constructor { index: REDEEMER_DISCRIMINANT, fields: vec![Declaration::Referenced(schema).into()], } .into()])), }) }, ) .expect("cannot fail because Ok") } pub fn from_type( modules: &HashMap, type_info: &Type, definitions: &mut Definitions, ) -> Result { Annotated::do_from_type(type_info, modules, &mut HashMap::new(), definitions) } fn do_from_type( type_info: &Type, modules: &HashMap, type_parameters: &mut HashMap>, definitions: &mut Definitions, ) -> Result { match type_info { Type::App { module: module_name, name: type_name, args, .. } if module_name.is_empty() => { definitions.register(type_info, &type_parameters.clone(), |definitions| { match &type_name[..] { "Data" => Ok(Annotated { title: Some("Data".to_string()), description: Some("Any Plutus data.".to_string()), annotated: Schema::Data(Data::Opaque), }), "ByteArray" => Ok(Schema::Data(Data::Bytes).into()), "Int" => Ok(Schema::Data(Data::Integer).into()), "String" => Ok(Schema::String.into()), "Void" => Ok(Annotated { title: Some("Unit".to_string()), description: Some("The nullary constructor.".to_string()), annotated: Schema::Data(Data::AnyOf(vec![Annotated { title: None, description: None, annotated: Constructor { index: 0, fields: vec![], }, }])), }), "Bool" => Ok(Annotated { title: Some("Bool".to_string()), description: None, annotated: Schema::Data(Data::AnyOf(vec![ Annotated { title: Some("False".to_string()), description: None, annotated: Constructor { index: 0, fields: vec![], }, }, Annotated { title: Some("True".to_string()), description: None, annotated: Constructor { index: 1, fields: vec![], }, }, ])), }), "Ordering" => Ok(Annotated { title: Some("Ordering".to_string()), description: None, annotated: Schema::Data(Data::AnyOf(vec![ Annotated { title: Some("Less".to_string()), description: None, annotated: Constructor { index: 0, fields: vec![], }, }, Annotated { title: Some("Equal".to_string()), description: None, annotated: Constructor { index: 1, fields: vec![], }, }, Annotated { title: Some("Greater".to_string()), description: None, annotated: Constructor { index: 2, fields: vec![], }, }, ])), }), "Option" => { let generic = Annotated::do_from_type( args.first() .expect("Option types have always one generic argument"), modules, type_parameters, definitions, )?; Ok(Annotated { title: Some("Optional".to_string()), description: None, annotated: Schema::Data(Data::AnyOf(vec![ Annotated { title: Some("Some".to_string()), description: Some("An optional value.".to_string()), annotated: Constructor { index: 0, fields: vec![Declaration::Referenced(generic).into()], }, }, Annotated { title: Some("None".to_string()), description: Some("Nothing.".to_string()), annotated: Constructor { index: 1, fields: vec![], }, }, ])), }) } "List" => { let generic = Annotated::do_from_type( args.first() .expect("List types have always one generic argument"), modules, type_parameters, definitions, )?; // NOTE: Lists of 2-tuples are treated as Maps. This is an oddity we inherit // from the PlutusTx / LedgerApi Haskell codebase, which encodes some elements // as such. We don't have a concept of language maps in Aiken, so we simply // make all types abide by this convention. let data = match definitions.try_lookup(&generic).cloned() { Some(Annotated { annotated: Schema::Data(Data::List(Items::Many(xs))), .. }) if xs.len() == 2 => { definitions.remove(&generic); Data::Map( xs.first() .expect("length (== 2) checked in pattern clause") .to_owned(), xs.last() .expect("length (== 2) checked in pattern clause") .to_owned(), ) } _ => Data::List(Items::One(Declaration::Referenced(generic))), }; Ok(Schema::Data(data).into()) } _ => Err(Error::new(ErrorContext::UnsupportedType, type_info)), } }) } Type::App { name, module, args, .. } => definitions.register(type_info, &type_parameters.clone(), |definitions| { let module = modules .get(module) .unwrap_or_else(|| panic!("unknown module '{module}'\n\n{modules:?}")); let data_type = find_data_type(name, &module.ast.definitions).unwrap_or_else(|| { panic!( "unknown data-type for '{name:?}' \n\n{definitions:?}", definitions = module.ast.definitions ) }); collect_type_parameters(type_parameters, &data_type.typed_parameters, args); let annotated = Schema::Data( Data::from_data_type(&data_type, modules, type_parameters, definitions) .map_err(|e| e.backtrack(type_info))?, ); Ok(Annotated { title: Some(data_type.name.clone()), description: data_type.doc.clone().map(|s| s.trim().to_string()), annotated, }) }), Type::Tuple { elems, .. } => { definitions.register(type_info, &type_parameters.clone(), |definitions| { let elems = elems .iter() .map(|elem| { Annotated::do_from_type(elem, modules, type_parameters, definitions) .map(Declaration::Referenced) }) .collect::, _>>() .map_err(|e| e.backtrack(type_info))?; Ok(Annotated { title: Some("Tuple".to_owned()), description: None, annotated: Schema::Data(Data::List(Items::Many(elems))), }) }) } Type::Var { tipo, .. } => match tipo.borrow().deref() { TypeVar::Link { tipo } => { Annotated::do_from_type(tipo, modules, type_parameters, definitions) } TypeVar::Generic { id } => { let tipo = type_parameters .get(id) .ok_or_else(|| Error::new(ErrorContext::FreeTypeVariable, type_info))? .clone(); Annotated::do_from_type(&tipo, modules, type_parameters, definitions) } TypeVar::Unbound { .. } => { Err(Error::new(ErrorContext::UnboundTypeVariable, type_info)) } }, Type::Fn { .. } => unreachable!(), Type::Pair { .. } => unreachable!(), } } } impl Data { fn from_data_type( data_type: &TypedDataType, modules: &HashMap, type_parameters: &mut HashMap>, definitions: &mut Definitions>, ) -> Result { if data_type.opaque { // NOTE: No breadcrumbs here which is *okay*, as the caller will backtrack // and add the necessary type information. return Err(Error { context: ErrorContext::IllegalOpaqueType, breadcrumbs: vec![], }); } let mut variants = vec![]; for (index, constructor) in data_type.constructors.iter().enumerate() { let mut fields = vec![]; for field in constructor.arguments.iter() { let reference = Annotated::do_from_type(&field.tipo, modules, type_parameters, definitions)?; fields.push(Annotated { title: field.label.clone(), description: field.doc.clone().map(|s| s.trim().to_string()), annotated: Declaration::Referenced(reference), }); } let variant = Annotated { title: Some(constructor.name.clone()), description: constructor.doc.clone().map(|s| s.trim().to_string()), annotated: Constructor { index, fields }, }; variants.push(variant); } Ok(Data::AnyOf(variants)) } } fn collect_type_parameters<'a>( type_parameters: &'a mut HashMap>, generics: &'a [Rc], applications: &'a [Rc], ) { for (index, generic) in generics.iter().enumerate() { match &**generic { Type::Var { tipo, .. } => match *tipo.borrow() { TypeVar::Generic { id } => { type_parameters.insert( id, applications .get(index) .unwrap_or_else(|| panic!("Couldn't find generic identifier ({id}) in applied types: {applications:?}")) .to_owned() ); } _ => unreachable!(), }, _ => unreachable!(), } } } fn find_data_type(name: &str, definitions: &[TypedDefinition]) -> Option { for def in definitions { match def { Definition::DataType(data_type) if name == data_type.name => { return Some(data_type.clone()); } Definition::Fn { .. } | Definition::Validator { .. } | Definition::DataType { .. } | Definition::TypeAlias { .. } | Definition::Use { .. } | Definition::ModuleConstant { .. } | Definition::Test { .. } => continue, } } None } // Needed because of Blueprint's default, but actually never used. impl Default for Annotated { fn default() -> Self { Schema::Data(Data::Opaque).into() } } impl Serialize for Schema { fn serialize(&self, serializer: S) -> Result { match self { Schema::Unit => { let mut s = serializer.serialize_struct("Unit", 1)?; s.serialize_field("dataType", "#unit")?; s.end() } Schema::Boolean => { let mut s = serializer.serialize_struct("Boolean", 1)?; s.serialize_field("dataType", "#boolean")?; s.end() } Schema::Integer => { let mut s = serializer.serialize_struct("Integer", 1)?; s.serialize_field("dataType", "#integer")?; s.end() } Schema::Bytes => { let mut s = serializer.serialize_struct("Bytes", 1)?; s.serialize_field("dataType", "#bytes")?; s.end() } Schema::String => { let mut s = serializer.serialize_struct("String", 1)?; s.serialize_field("dataType", "#string")?; s.end() } Schema::Pair(left, right) => { let mut s = serializer.serialize_struct("Pair", 3)?; s.serialize_field("dataType", "#pair")?; s.serialize_field("left", &left)?; s.serialize_field("right", &right)?; s.end() } Schema::List(items) => { let mut s = serializer.serialize_struct("List", 2)?; s.serialize_field("dataType", "#list")?; s.serialize_field("items", &items)?; s.end() } Schema::Data(data) => data.serialize(serializer), } } } fn visit_schema<'a, V>(mut map: V) -> Result where V: MapAccess<'a>, { #[derive(serde::Deserialize)] #[serde(field_identifier, rename_all = "camelCase")] enum Field { DataType, Items, Keys, Values, Left, Right, AnyOf, OneOf, } let mut data_type: Option = None; let mut items: Option = None; // defer items deserialization to later let mut keys = None; let mut left = None; let mut right = None; let mut values = None; let mut any_of = None; while let Some(key) = map.next_key()? { match key { Field::DataType => { if data_type.is_some() { return Err(de::Error::duplicate_field("dataType")); } data_type = Some(map.next_value()?); } Field::Items => { if items.is_some() { return Err(de::Error::duplicate_field("items")); } items = Some(map.next_value()?); } Field::Keys => { if keys.is_some() { return Err(de::Error::duplicate_field("keys")); } keys = Some(map.next_value()?); } Field::Values => { if values.is_some() { return Err(de::Error::duplicate_field("values")); } values = Some(map.next_value()?); } Field::Left => { if left.is_some() { return Err(de::Error::duplicate_field("left")); } left = Some(map.next_value()?); } Field::Right => { if right.is_some() { return Err(de::Error::duplicate_field("right")); } right = Some(map.next_value()?); } Field::AnyOf => { if any_of.is_some() { return Err(de::Error::duplicate_field("anyOf/oneOf")); } any_of = Some(map.next_value()?); } Field::OneOf => { if any_of.is_some() { return Err(de::Error::duplicate_field("anyOf/oneOf")); } any_of = Some(map.next_value()?); } } } let expect_data_items = || match &items { Some(items) => serde_json::from_value::>(items.clone()) .map_err(|e| de::Error::custom(e.to_string())), None => Err(de::Error::missing_field("items")), }; let expect_schema_items = || match &items { Some(items) => serde_json::from_value::>(items.clone()) .map_err(|e| de::Error::custom(e.to_string())), None => Err(de::Error::missing_field("items")), }; let expect_no_items = || { if items.is_some() { return Err(de::Error::custom( "unexpected fields 'items' for non-list data-type", )); } Ok(()) }; let expect_no_keys = || { if keys.is_some() { return Err(de::Error::custom( "unexpected fields 'keys' for non-map data-type", )); } Ok(()) }; let expect_no_values = || { if values.is_some() { return Err(de::Error::custom( "unexpected fields 'values' for non-map data-type", )); } Ok(()) }; let expect_no_any_of = || { if any_of.is_some() { return Err(de::Error::custom( "unexpected fields 'anyOf' or 'oneOf'; applicators must singletons", )); } Ok(()) }; let expect_no_left_or_right = || { if left.is_some() || right.is_some() { return Err(de::Error::custom( "unexpected field(s) 'left' and/or 'right' for a non-pair data-type", )); } Ok(()) }; match data_type { None => { expect_no_items()?; expect_no_keys()?; expect_no_values()?; expect_no_left_or_right()?; match any_of { None => Ok(Schema::Data(Data::Opaque)), Some(constructors) => Ok(Schema::Data(Data::AnyOf(constructors))), } } Some(data_type) if data_type == "list" => { expect_no_keys()?; expect_no_values()?; expect_no_any_of()?; expect_no_left_or_right()?; let items = expect_data_items()?; Ok(Schema::Data(Data::List(items))) } Some(data_type) if data_type == "#list" => { expect_no_keys()?; expect_no_values()?; expect_no_any_of()?; expect_no_left_or_right()?; let items = expect_schema_items()?; Ok(Schema::List(items)) } Some(data_type) if data_type == "map" => { expect_no_items()?; expect_no_any_of()?; expect_no_left_or_right()?; match (keys, values) { (Some(keys), Some(values)) => Ok(Schema::Data(Data::Map(keys, values))), (None, _) => Err(de::Error::missing_field("keys")), (Some(..), None) => Err(de::Error::missing_field("values")), } } Some(data_type) if data_type == "#pair" => { expect_no_items()?; expect_no_keys()?; expect_no_values()?; expect_no_any_of()?; match (left, right) { (Some(left), Some(right)) => Ok(Schema::Pair(left, right)), (None, _) => Err(de::Error::missing_field("left")), (Some(..), None) => Err(de::Error::missing_field("right")), } } Some(data_type) => { expect_no_items()?; expect_no_keys()?; expect_no_values()?; expect_no_any_of()?; expect_no_left_or_right()?; if data_type == "bytes" { Ok(Schema::Data(Data::Bytes)) } else if data_type == "integer" { Ok(Schema::Data(Data::Integer)) } else if data_type == "#unit" { Ok(Schema::Unit) } else if data_type == "#integer" { Ok(Schema::Integer) } else if data_type == "#bytes" { Ok(Schema::Bytes) } else if data_type == "#boolean" { Ok(Schema::Boolean) } else if data_type == "#string" { Ok(Schema::String) } else { Err(de::Error::custom("unknown data-type")) } } } } impl<'a> Deserialize<'a> for Schema { fn deserialize>(deserializer: D) -> Result { struct SchemaVisitor; impl<'a> Visitor<'a> for SchemaVisitor { type Value = Schema; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("Schema") } fn visit_map(self, mut map: V) -> Result where V: MapAccess<'a>, { visit_schema(&mut map) } } deserializer.deserialize_struct( "Schema", &[ "dataType", "items", "keys", "values", "anyOf", "oneOf", "left", "right", ], SchemaVisitor, ) } } impl Serialize for Data { fn serialize(&self, serializer: S) -> Result { match self { Data::Opaque => { let s = serializer.serialize_struct("Opaque", 0)?; s.end() } Data::Integer => { let mut s = serializer.serialize_struct("Integer", 1)?; s.serialize_field("dataType", "integer")?; s.end() } Data::Bytes => { let mut s = serializer.serialize_struct("Bytes", 1)?; s.serialize_field("dataType", "bytes")?; s.end() } Data::List(items) => { let mut s = serializer.serialize_struct("List", 2)?; s.serialize_field("dataType", "list")?; s.serialize_field("items", &items)?; s.end() } Data::Map(keys, values) => { let mut s = serializer.serialize_struct("Map", 3)?; s.serialize_field("dataType", "map")?; s.serialize_field("keys", &keys)?; s.serialize_field("values", &values)?; s.end() } Data::AnyOf(constructors) => { let mut s = serializer.serialize_struct("AnyOf", 1)?; s.serialize_field("anyOf", &constructors)?; s.end() } } } } impl<'a> Deserialize<'a> for Data { fn deserialize>(deserializer: D) -> Result { struct DataVisitor; impl<'a> Visitor<'a> for DataVisitor { type Value = Data; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("Data") } fn visit_map(self, mut map: V) -> Result where V: MapAccess<'a>, { let schema = visit_schema(&mut map)?; match schema { Schema::Data(data) => Ok(data), _ => Err(de::Error::custom("not a valid 'data'")), } } } deserializer.deserialize_struct( "Data", &["dataType", "items", "keys", "values", "anyOf", "oneOf"], DataVisitor, ) } } impl Serialize for Constructor { fn serialize(&self, serializer: S) -> Result { let mut s = serializer.serialize_struct("Constructor", 3)?; s.serialize_field("dataType", "constructor")?; s.serialize_field("index", &self.index)?; s.serialize_field("fields", &self.fields)?; s.end() } } impl<'a> Deserialize<'a> for Constructor { fn deserialize>(deserializer: D) -> Result { #[derive(serde::Deserialize)] #[serde(field_identifier, rename_all = "camelCase")] enum Field { Index, Fields, } const FIELDS: &[&str] = &["index", "fields"]; struct ConstructorVisitor; impl<'a> Visitor<'a> for ConstructorVisitor { type Value = Constructor; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("Constructor") } fn visit_map(self, mut map: V) -> Result where V: MapAccess<'a>, { let mut index = None; let mut fields = None; while let Some(key) = map.next_key()? { match key { Field::Index => { if index.is_some() { return Err(de::Error::duplicate_field(FIELDS[0])); } index = Some(map.next_value()?); } Field::Fields => { if fields.is_some() { return Err(de::Error::duplicate_field(FIELDS[1])); } fields = Some(map.next_value()?); } } } Ok(Constructor { index: index.ok_or_else(|| de::Error::missing_field(FIELDS[0]))?, fields: fields.ok_or_else(|| de::Error::missing_field(FIELDS[1]))?, }) } } deserializer.deserialize_struct("Constructor", FIELDS, ConstructorVisitor) } } #[derive(Debug, PartialEq, Clone, thiserror::Error)] #[error("{}", context)] pub struct Error { context: ErrorContext, breadcrumbs: Vec, } #[derive(Debug, PartialEq, Clone, thiserror::Error)] pub enum ErrorContext { #[error( "I failed at my own job and couldn't figure out how to generate a specification for a type." )] UnsupportedType, #[error("I discovered a type hole where I would expect a concrete type.")] UnboundTypeVariable, #[error("I caught a free variable in the contract's interface boundary.")] FreeTypeVariable, #[error("I had the misfortune to find an invalid type in an interface boundary.")] ExpectedData, #[error("I figured you tried to export a function in your contract's binary interface.")] UnexpectedFunction, #[error("I caught an opaque type trying to escape")] IllegalOpaqueType, } impl Error { pub fn new(context: ErrorContext, type_info: &Type) -> Self { Error { context, breadcrumbs: vec![type_info.clone()], } } pub fn backtrack(self, type_info: &Type) -> Self { let mut breadcrumbs = vec![type_info.clone()]; breadcrumbs.extend(self.breadcrumbs); Error { context: self.context, breadcrumbs, } } pub fn help(&self) -> String { match self.context { ErrorContext::IllegalOpaqueType => format!( r#"Opaque types cannot figure anywhere in an outward-facing type like a validator's redeemer or datum. This is because an {opaque} type hides its implementation details, and likely enforce invariants that cannot be expressed only structurally. In particular, the {opaque} type {signature} cannot be safely constructed from any Plutus Data. Hence, {opaque} types are forbidden from interface points with the off-chain world. Instead, use an intermediate representation and construct the {opaque} type at runtime using constructors and methods provided for that type (e.g. {Dict}.{from_list}, {Rational}.{new}, ...)."#, opaque = "opaque".if_supports_color(Stdout, |s| s.purple()), signature = Error::fmt_breadcrumbs(&[self .breadcrumbs .last() .expect("always at least one breadcrumb") .to_owned()]), Dict = "Dict" .if_supports_color(Stdout, |s| s.bright_blue()) .if_supports_color(Stdout, |s| s.bold()), from_list = "from_list".if_supports_color(Stdout, |s| s.blue()), Rational = "Rational" .if_supports_color(Stdout, |s| s.bright_blue()) .if_supports_color(Stdout, |s| s.bold()), new = "new".if_supports_color(Stdout, |s| s.blue()), ), ErrorContext::UnsupportedType => format!( r#"I do not know how to generate a portable Plutus specification for the following type: ╰─▶ {signature} This is likely a bug. I should know. May you be kind enough and report this on ."#, signature = Error::fmt_breadcrumbs(&[self .breadcrumbs .last() .expect("always at least one breadcrumb") .to_owned()]), ), ErrorContext::FreeTypeVariable => format!( r#"There can't be any free type variable at the contract's boundary (i.e. in types used as datum and/or redeemer). Indeed, the validator can only be invoked with (very) concrete types. Since there's no reflexion possible inside a validator, it simply isn't possible to have any remaining free type variable in any of the datum or redeemer. I got there when trying to generate a blueprint specification of the following type: ╰─▶ {breadcrumbs}"#, breadcrumbs = Error::fmt_breadcrumbs(&self.breadcrumbs) ), ErrorContext::UnboundTypeVariable => format!( r#"There cannot be any unbound type variable at the contract's boundary (i.e. in types used as datum and/or redeemer). Indeed, in order to generate an outward-facing specification of the contract's interface, I need to know what concrete representations will the datum and/or the redeemer have. If your contract doesn't need datum or redeemer, you can always give them the type {type_Void} to indicate this. It is very concrete and will help me progress forward."#, type_Void = "Void" .if_supports_color(Stdout, |s| s.bright_blue()) .if_supports_color(Stdout, |s| s.bold()) ), ErrorContext::ExpectedData => format!( r#"While figuring out the outward-facing specification for your contract, I found a type that cannot actually be represented as valid Untyped Plutus Core (the low-level language Cardano uses to execute smart-contracts. For example, it isn't possible to have a list or a tuple of {type_String} because the underlying execution engine doesn't allow it. There are few restrictions like this one. In this instance, here's the types I followed and that led me to this problem: ╰─▶ {breadcrumbs}"#, type_String = "String" .if_supports_color(Stdout, |s| s.bright_blue()) .if_supports_color(Stdout, |s| s.bold()), breadcrumbs = Error::fmt_breadcrumbs(&self.breadcrumbs) ), ErrorContext::UnexpectedFunction => format!( r#"I can't allow that. Functions aren't serializable as data on-chain and thus cannot be used within your datum and/or redeemer types. Here's the types I followed and that led me to this problem: ╰─▶ {breadcrumbs}"#, breadcrumbs = Error::fmt_breadcrumbs(&self.breadcrumbs) ), } } fn fmt_breadcrumbs(breadcrumbs: &[Type]) -> String { breadcrumbs .iter() .map(|type_info| { pretty::Printer::new() .print(type_info) .to_pretty_string(70) .if_supports_color(Stdout, |s| s.bright_blue()) .if_supports_color(Stdout, |s| s.bold()) .to_string() }) .collect::>() .join(" → ") } } #[cfg(test)] pub mod tests { use super::*; use proptest::prelude::*; use serde_json::{self, json, Value}; pub fn assert_json(schema: &impl Serialize, expected: Value) { assert_eq!(serde_json::to_value(schema).unwrap(), expected); } #[test] fn serialize_data_integer() { let schema = Schema::Data(Data::Integer); assert_json( &schema, json!({ "dataType": "integer" }), ); } #[test] fn serialize_data_bytes() { let schema = Schema::Data(Data::Bytes); assert_json( &schema, json!({ "dataType": "bytes" }), ); } #[test] fn serialize_data_list_1() { let ref_integer = Reference::new("Int"); let schema = Schema::Data(Data::List(Items::One(Declaration::Referenced(ref_integer)))); assert_json( &schema, json!({ "dataType": "list", "items": { "$ref": "#/definitions/Int" } }), ); } #[test] fn serialize_data_list_2() { let ref_list_integer = Reference::new("List$Int"); let schema = Schema::Data(Data::List(Items::One(Declaration::Referenced( ref_list_integer, )))); assert_json( &schema, json!({ "dataType": "list", "items": { "$ref": "#/definitions/List$Int" } }), ); } #[test] fn serialize_data_map_1() { let ref_integer = Declaration::Referenced(Reference::new("Int")); let ref_bytes = Declaration::Referenced(Reference::new("ByteArray")); let schema = Schema::Data(Data::Map(ref_integer, ref_bytes)); assert_json( &schema, json!({ "dataType": "map", "keys": { "$ref": "#/definitions/Int" }, "values": { "$ref": "#/definitions/ByteArray" } }), ) } #[test] fn serialize_data_constr_1() { let schema = Schema::Data(Data::AnyOf(vec![Constructor { index: 0, fields: vec![], } .into()])); assert_json( &schema, json!({ "anyOf": [{ "dataType": "constructor", "index": 0, "fields": [] }] }), ) } #[test] fn serialize_data_constr_2() { let schema = Schema::Data(Data::AnyOf(vec![ Constructor { index: 0, fields: vec![Declaration::Referenced(Reference::new("Int")).into()], } .into(), Constructor { index: 1, fields: vec![Declaration::Referenced(Reference::new("Bytes")).into()], } .into(), ])); assert_json( &schema, json!({ "anyOf": [ { "dataType": "constructor", "index": 0, "fields": [{ "$ref": "#/definitions/Int" }] }, { "dataType": "constructor", "index": 1, "fields": [{ "$ref": "#/definitions/Bytes" }] } ] }), ) } #[test] fn serialize_empty_data() { let schema = Schema::Data(Data::Opaque); assert_json(&schema, json!({})) } #[test] fn serialize_annotated_1() { let schema = Annotated { title: Some("foo".to_string()), description: None, annotated: Schema::Integer, }; assert_json( &schema, json!({ "title": "foo", "dataType": "#integer" }), ) } #[test] fn serialize_annotated_2() { let schema = Annotated { title: Some("foo".to_string()), description: Some("Lorem Ipsum".to_string()), annotated: Schema::String, }; assert_json( &schema, json!({ "title": "foo", "description": "Lorem Ipsum", "dataType": "#string" }), ) } #[test] fn deserialize_data_opaque() { assert_eq!(Data::Opaque, serde_json::from_value(json!({})).unwrap()) } #[test] fn deserialize_data_integer() { assert_eq!( Data::Integer, serde_json::from_value(json!({ "dataType": "integer", })) .unwrap() ) } #[test] fn deserialize_data_bytes() { assert_eq!( Data::Bytes, serde_json::from_value(json!({ "dataType": "bytes", })) .unwrap() ) } #[test] fn deserialize_data_list_one() { assert_eq!( Data::List(Items::One(Declaration::Referenced(Reference::new("foo")))), serde_json::from_value(json!({ "dataType": "list", "items": { "$ref": "#/definitions/foo" } })) .unwrap() ) } #[test] fn deserialize_data_list_many() { assert_eq!( Data::List(Items::Many(vec![ Declaration::Referenced(Reference::new("foo")), Declaration::Referenced(Reference::new("bar")) ])), serde_json::from_value(json!({ "dataType": "list", "items": [ { "$ref": "#/definitions/foo" }, { "$ref": "#/definitions/bar" } ], })) .unwrap() ) } #[test] fn deserialize_data_map() { assert_eq!( Data::Map( Declaration::Referenced(Reference::new("foo")), Declaration::Referenced(Reference::new("bar")) ), serde_json::from_value(json!({ "dataType": "map", "keys": { "$ref": "#/definitions/foo" }, "values": { "$ref": "#/definitions/bar" } })) .unwrap() ) } #[test] fn deserialize_any_of() { assert_eq!( Data::AnyOf(vec![Constructor { index: 0, fields: vec![ Declaration::Referenced(Reference::new("foo")).into(), Declaration::Referenced(Reference::new("bar")).into() ], } .into()]), serde_json::from_value(json!({ "anyOf": [{ "index": 0, "fields": [ { "$ref": "#/definitions/foo", }, { "$ref": "#/definitions/bar", } ] }] })) .unwrap() ) } #[test] fn deserialize_one_of() { assert_eq!( Data::AnyOf(vec![Constructor { index: 0, fields: vec![ Declaration::Referenced(Reference::new("foo")).into(), Declaration::Referenced(Reference::new("bar")).into() ], } .into()]), serde_json::from_value(json!({ "oneOf": [{ "index": 0, "fields": [ { "$ref": "#/definitions/foo", }, { "$ref": "#/definitions/bar", } ] }] })) .unwrap() ) } #[allow(clippy::arc_with_non_send_sync)] fn arbitrary_data() -> impl Strategy { let leaf = prop_oneof![Just(Data::Opaque), Just(Data::Bytes), Just(Data::Integer)]; leaf.prop_recursive(3, 8, 3, |inner| { let r = prop_oneof![ ".*".prop_map(|s| Declaration::Referenced(Reference::new(&s))), inner.prop_map(|s| Declaration::Inline(Box::new(s))) ]; let constructor = (0..3usize, prop::collection::vec(r.clone(), 0..3)).prop_map(|(index, fields)| { Constructor { index, fields: fields.into_iter().map(|f| f.into()).collect(), } .into() }); prop_oneof![ (r.clone(), r.clone()).prop_map(|(k, v)| Data::Map(k, v)), r.clone().prop_map(|x| Data::List(Items::One(x))), prop::collection::vec(r, 1..3).prop_map(|xs| Data::List(Items::Many(xs))), prop::collection::vec(constructor, 1..3).prop_map(Data::AnyOf) ] }) } #[allow(clippy::arc_with_non_send_sync)] fn arbitrary_schema() -> impl Strategy { prop_compose! { fn data_strategy()(data in arbitrary_data()) -> Schema { Schema::Data(data) } } let leaf = prop_oneof![ Just(Schema::Unit), Just(Schema::Boolean), Just(Schema::Bytes), Just(Schema::Integer), Just(Schema::String), data_strategy(), ]; leaf.prop_recursive(3, 8, 3, |inner| { let r = prop_oneof![ ".*".prop_map(|s| Declaration::Referenced(Reference::new(&s))), inner.prop_map(|s| Declaration::Inline(Box::new(s))) ]; prop_oneof![ (r.clone(), r.clone()).prop_map(|(l, r)| Schema::Pair(l, r)), r.clone().prop_map(|x| Schema::List(Items::One(x))), prop::collection::vec(r, 1..3).prop_map(|xs| Schema::List(Items::Many(xs))), ] }) } proptest! { #[test] fn data_serialization_roundtrip(data in arbitrary_data()) { let json = serde_json::to_value(data); let pretty = json .as_ref() .map(|v| serde_json::to_string_pretty(v).unwrap()) .unwrap_or_else(|_| "invalid".to_string()); assert!( matches!( json.and_then(serde_json::from_value::), Ok{..} ), "\ncounterexample: {pretty}\n", ) } } proptest! { #[test] fn schema_serialization_roundtrip(schema in arbitrary_schema()) { let json = serde_json::to_value(schema); let pretty = json .as_ref() .map(|v| serde_json::to_string_pretty(v).unwrap()) .unwrap_or_else(|_| "invalid".to_string()); assert!( matches!( json.and_then(serde_json::from_value::), Ok{..} ), "\ncounterexample: {pretty}\n", ) } } }