Refactor build steps to generate blueprints instead
The blueprint is generated at the root of the repository and is intended to be versioned with the rest. It acts as a business card that contains many practical information. There's a variety of tools we can then build on top of open-source contracts. And, quite importantly, the blueprint is language-agnostic; it isn't specific to Aiken. So it is really meant as an interop format within the ecosystem.
This commit is contained in:
59
crates/aiken-project/src/blueprint/error.rs
Normal file
59
crates/aiken-project/src/blueprint/error.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use super::schema;
|
||||
use crate::module::CheckedModule;
|
||||
use aiken_lang::ast::{Span, TypedFunction};
|
||||
use miette::{Diagnostic, NamedSource};
|
||||
use std::{fmt::Debug, path::PathBuf};
|
||||
|
||||
#[derive(Debug, thiserror::Error, Diagnostic)]
|
||||
pub enum Error {
|
||||
#[error("Validator functions must return Bool")]
|
||||
ValidatorMustReturnBool {
|
||||
path: PathBuf,
|
||||
src: String,
|
||||
named: NamedSource,
|
||||
location: Span,
|
||||
},
|
||||
#[error("Validator\n\n{name}\n\nrequires at least {at_least} arguments")]
|
||||
WrongValidatorArity {
|
||||
name: String,
|
||||
at_least: u8,
|
||||
location: Span,
|
||||
path: PathBuf,
|
||||
src: String,
|
||||
named: NamedSource,
|
||||
},
|
||||
#[error(transparent)]
|
||||
Schema(schema::Error),
|
||||
}
|
||||
|
||||
pub fn assert_return_bool(module: &CheckedModule, def: &TypedFunction) -> Result<(), Error> {
|
||||
if !def.return_type.is_bool() {
|
||||
Err(Error::ValidatorMustReturnBool {
|
||||
location: def.location,
|
||||
src: module.code.clone(),
|
||||
path: module.input_path.clone(),
|
||||
named: NamedSource::new(module.input_path.display().to_string(), module.code.clone()),
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_min_arity(
|
||||
module: &CheckedModule,
|
||||
def: &TypedFunction,
|
||||
at_least: u8,
|
||||
) -> Result<(), Error> {
|
||||
if def.arguments.len() < at_least as usize {
|
||||
Err(Error::WrongValidatorArity {
|
||||
location: def.location,
|
||||
src: module.code.clone(),
|
||||
path: module.input_path.clone(),
|
||||
named: NamedSource::new(module.input_path.display().to_string(), module.code.clone()),
|
||||
name: def.name.clone(),
|
||||
at_least,
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
141
crates/aiken-project/src/blueprint/mod.rs
Normal file
141
crates/aiken-project/src/blueprint/mod.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
pub mod error;
|
||||
pub mod schema;
|
||||
pub mod validator;
|
||||
|
||||
use crate::{config::Config, module::CheckedModules};
|
||||
use aiken_lang::uplc::CodeGenerator;
|
||||
use error::*;
|
||||
use schema::Schema;
|
||||
use std::fmt::Debug;
|
||||
use validator::{Purpose, Validator};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, serde::Serialize)]
|
||||
pub struct Blueprint {
|
||||
pub preamble: Preamble,
|
||||
pub validators: Vec<validator::Validator>,
|
||||
}
|
||||
|
||||
impl Blueprint {
|
||||
pub fn new(
|
||||
config: &Config,
|
||||
modules: &CheckedModules,
|
||||
generator: &mut CodeGenerator,
|
||||
) -> Result<Self, Error> {
|
||||
let mut validators = Vec::new();
|
||||
|
||||
for (validator, def) in modules.validators() {
|
||||
let purpose: Purpose = def.name.clone().into();
|
||||
|
||||
assert_return_bool(validator, def)?;
|
||||
assert_min_arity(validator, def, purpose.min_arity())?;
|
||||
|
||||
let mut args = def.arguments.iter().rev();
|
||||
let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next());
|
||||
|
||||
validators.push(Validator {
|
||||
description: None,
|
||||
purpose,
|
||||
datum: datum
|
||||
.map(|datum| {
|
||||
Schema::from_type(modules.into(), &datum.arg_name.get_label(), &datum.tipo)
|
||||
.map_err(Error::Schema)
|
||||
})
|
||||
.transpose()?,
|
||||
redeemer: Schema::from_type(
|
||||
modules.into(),
|
||||
&redeemer.arg_name.get_label(),
|
||||
&redeemer.tipo,
|
||||
)
|
||||
.map_err(Error::Schema)?,
|
||||
program: generator
|
||||
.generate(&def.body, &def.arguments, true)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Blueprint {
|
||||
preamble: Preamble::from_config(config),
|
||||
validators,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, serde::Serialize)]
|
||||
pub struct Preamble {
|
||||
pub title: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub version: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub license: Option<String>,
|
||||
}
|
||||
|
||||
impl Preamble {
|
||||
pub fn from_config(config: &Config) -> Self {
|
||||
Preamble {
|
||||
title: config.name.to_string(),
|
||||
description: if config.description.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(config.description.clone())
|
||||
},
|
||||
version: config.version.clone(),
|
||||
license: config.license.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use serde_json::{self, json};
|
||||
|
||||
#[test]
|
||||
fn serialize_no_description() {
|
||||
let blueprint = Blueprint {
|
||||
preamble: Preamble {
|
||||
title: "Foo".to_string(),
|
||||
description: None,
|
||||
version: "1.0.0".to_string(),
|
||||
license: Some("Apache-2.0".to_string()),
|
||||
},
|
||||
validators: vec![],
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&blueprint).unwrap(),
|
||||
json!({
|
||||
"preamble": {
|
||||
"title": "Foo",
|
||||
"version": "1.0.0",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"validators": []
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_with_description() {
|
||||
let blueprint = Blueprint {
|
||||
preamble: Preamble {
|
||||
title: "Foo".to_string(),
|
||||
description: Some("Lorem ipsum".to_string()),
|
||||
version: "1.0.0".to_string(),
|
||||
license: None,
|
||||
},
|
||||
validators: vec![],
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&blueprint).unwrap(),
|
||||
json!({
|
||||
"preamble": {
|
||||
"title": "Foo",
|
||||
"description": "Lorem ipsum",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
"validators": []
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
353
crates/aiken-project/src/blueprint/schema.rs
Normal file
353
crates/aiken-project/src/blueprint/schema.rs
Normal file
@@ -0,0 +1,353 @@
|
||||
use crate::CheckedModule;
|
||||
use aiken_lang::{
|
||||
ast::{DataType, Definition, TypedDefinition},
|
||||
tipo::Type,
|
||||
};
|
||||
use miette::Diagnostic;
|
||||
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
||||
use serde_json;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Display},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Schema {
|
||||
Integer,
|
||||
Bytes,
|
||||
List(Item<Box<Schema>>),
|
||||
Map((Box<Schema>, Box<Schema>)),
|
||||
AnyOf(Vec<Constructor>),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Constructor {
|
||||
pub index: usize,
|
||||
pub fields: Vec<Schema>,
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
pub fn from_type(
|
||||
modules: &HashMap<String, CheckedModule>,
|
||||
name: &str,
|
||||
type_info: &Type,
|
||||
) -> Result<Self, Error> {
|
||||
match type_info {
|
||||
Type::App {
|
||||
module: module_name,
|
||||
name: type_name,
|
||||
..
|
||||
} if module_name.is_empty() => match &type_name[..] {
|
||||
"ByteArray" => Ok(Schema::Bytes),
|
||||
"Integer" => Ok(Schema::Integer),
|
||||
_ => Err(Error::UnsupportedPrimitiveType {
|
||||
type_name: type_name.clone(),
|
||||
}),
|
||||
},
|
||||
Type::App {
|
||||
module: module_name,
|
||||
name: type_name,
|
||||
..
|
||||
} => {
|
||||
let module = modules.get(module_name).unwrap();
|
||||
let constructor = find_definition(type_name, &module.ast.definitions).unwrap();
|
||||
Self::from_data_type(modules, constructor)
|
||||
}
|
||||
Type::Fn { .. } | Type::Var { .. } | Type::Tuple { .. } => {
|
||||
Err(Error::UnsupportedKind {
|
||||
arg_or_field_name: name.to_string(),
|
||||
type_info: type_info.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_data_type(
|
||||
modules: &HashMap<String, CheckedModule>,
|
||||
data_type: &DataType<Arc<Type>>,
|
||||
) -> Result<Self, Error> {
|
||||
let mut variants = vec![];
|
||||
for (index, constructor) in data_type.constructors.iter().enumerate() {
|
||||
let mut fields = vec![];
|
||||
for field in constructor.arguments.iter() {
|
||||
fields.push(Schema::from_type(
|
||||
modules,
|
||||
&field.label.clone().unwrap_or_default(),
|
||||
&field.tipo,
|
||||
)?);
|
||||
}
|
||||
variants.push(Constructor { index, fields });
|
||||
}
|
||||
Ok(Schema::AnyOf(variants))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Schema {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?;
|
||||
f.write_str(&s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Schema {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match self {
|
||||
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::List(items) => {
|
||||
let mut s = serializer.serialize_struct("List", 2)?;
|
||||
s.serialize_field("dataType", "list")?;
|
||||
s.serialize_field("items", &items)?;
|
||||
s.end()
|
||||
}
|
||||
Schema::Map(elements) => {
|
||||
let mut s = serializer.serialize_struct("Map", 2)?;
|
||||
s.serialize_field("dataType", "map")?;
|
||||
s.serialize_field("elements", &elements)?;
|
||||
s.end()
|
||||
}
|
||||
Schema::AnyOf(constructors) => match &constructors[..] {
|
||||
[constructor] => constructor.serialize(serializer),
|
||||
_ => {
|
||||
let mut s = serializer.serialize_struct("AnyOf", 1)?;
|
||||
s.serialize_field("anyOf", &constructors)?;
|
||||
s.end()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Serialize for Constructor {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// Represent a items list in a JSON schema. Can be either a singleton (i.e. a single schema) when
|
||||
// all elements in the list are uniform or a list of schemas.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Item<T> {
|
||||
Singleton(T),
|
||||
Many(Vec<T>),
|
||||
}
|
||||
|
||||
impl<T: Serialize> Serialize for Item<T> {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match self {
|
||||
Item::Singleton(elem) => Serialize::serialize(elem, serializer),
|
||||
Item::Many(elems) => Serialize::serialize(elems, serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, thiserror::Error, Diagnostic)]
|
||||
pub enum Error {
|
||||
#[error("I stumble upon an unsupported kind in a datum or redeemer definition.\n")]
|
||||
UnsupportedKind {
|
||||
arg_or_field_name: String,
|
||||
type_info: Type,
|
||||
},
|
||||
#[error("I discovered an unexpected primitive in a datum or redeemer definition.\n")]
|
||||
UnsupportedPrimitiveType { type_name: String },
|
||||
}
|
||||
|
||||
fn find_definition<'a>(
|
||||
name: &str,
|
||||
definitions: &'a Vec<TypedDefinition>,
|
||||
) -> Option<&'a DataType<Arc<Type>>> {
|
||||
for def in definitions {
|
||||
match def {
|
||||
Definition::DataType(data_type) if name == data_type.name => return Some(data_type),
|
||||
Definition::Fn { .. }
|
||||
| Definition::DataType { .. }
|
||||
| Definition::TypeAlias { .. }
|
||||
| Definition::Use { .. }
|
||||
| Definition::ModuleConstant { .. }
|
||||
| Definition::Test { .. } => continue,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use super::*;
|
||||
use serde_json::{self, json, Value};
|
||||
|
||||
pub fn assert_json(schema: &Schema, expected: Value) {
|
||||
assert_eq!(serde_json::to_value(schema).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_integer() {
|
||||
let schema = Schema::Integer;
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "integer"
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_bytes() {
|
||||
let schema = Schema::Bytes;
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "bytes"
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_list_1() {
|
||||
let schema = Schema::List(Item::Many(vec![]));
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "list",
|
||||
"items": []
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_list_2() {
|
||||
let schema = Schema::List(Item::Singleton(Box::new(Schema::Integer)));
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "list",
|
||||
"items": {
|
||||
"dataType": "integer"
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_list_3() {
|
||||
let schema = Schema::List(Item::Many(vec![
|
||||
Box::new(Schema::Bytes),
|
||||
Box::new(Schema::List(Item::Singleton(Box::new(Schema::Integer)))),
|
||||
]));
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "list",
|
||||
"items": [
|
||||
{
|
||||
"dataType": "bytes"
|
||||
},
|
||||
{
|
||||
"dataType": "list",
|
||||
"items": { "dataType": "integer" }
|
||||
}
|
||||
]
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_map_1() {
|
||||
let schema = Schema::Map((Box::new(Schema::Integer), Box::new(Schema::Bytes)));
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "map",
|
||||
"elements": [
|
||||
{
|
||||
"dataType": "integer"
|
||||
},
|
||||
{
|
||||
"dataType": "bytes"
|
||||
}
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_map_2() {
|
||||
let schema = Schema::Map((
|
||||
Box::new(Schema::Bytes),
|
||||
Box::new(Schema::List(Item::Singleton(Box::new(Schema::Integer)))),
|
||||
));
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "map",
|
||||
"elements": [
|
||||
{
|
||||
"dataType": "bytes"
|
||||
},
|
||||
{
|
||||
"dataType": "list",
|
||||
"items": { "dataType": "integer" }
|
||||
}
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_constr_1() {
|
||||
let schema = Schema::AnyOf(vec![Constructor {
|
||||
index: 0,
|
||||
fields: vec![],
|
||||
}]);
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "constructor",
|
||||
"index": 0,
|
||||
"fields": []
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_constr_2() {
|
||||
let schema = Schema::AnyOf(vec![
|
||||
Constructor {
|
||||
index: 0,
|
||||
fields: vec![Schema::Integer],
|
||||
},
|
||||
Constructor {
|
||||
index: 1,
|
||||
fields: vec![Schema::Bytes],
|
||||
},
|
||||
]);
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"anyOf": [
|
||||
{
|
||||
"dataType": "constructor",
|
||||
"index": 0,
|
||||
"fields": [{ "dataType": "integer" }]
|
||||
},
|
||||
{
|
||||
"dataType": "constructor",
|
||||
"index": 1,
|
||||
"fields": [{ "dataType": "bytes" }]
|
||||
}
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
121
crates/aiken-project/src/blueprint/validator.rs
Normal file
121
crates/aiken-project/src/blueprint/validator.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use super::schema::Schema;
|
||||
use pallas::ledger::primitives::babbage as cardano;
|
||||
use pallas_traverse::ComputeHash;
|
||||
use serde::{
|
||||
self,
|
||||
ser::{Serialize, SerializeStruct, Serializer},
|
||||
};
|
||||
use std::fmt::{self, Display};
|
||||
use uplc::ast::{NamedDeBruijn, Program};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Validator {
|
||||
pub purpose: Purpose,
|
||||
pub description: Option<String>,
|
||||
pub datum: Option<Schema>,
|
||||
pub redeemer: Schema,
|
||||
pub program: Program<NamedDeBruijn>,
|
||||
}
|
||||
|
||||
impl Serialize for Validator {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let cbor = self.program.to_cbor().unwrap();
|
||||
let source_code = hex::encode(&cbor);
|
||||
let mut s = serializer.serialize_struct("Validator", 5)?;
|
||||
s.serialize_field("purpose", &self.purpose)?;
|
||||
let hash = cardano::PlutusV2Script(cbor.into()).compute_hash();
|
||||
s.serialize_field("hash", &hash)?;
|
||||
if let Some { .. } = self.description {
|
||||
s.serialize_field("description", &self.description)?;
|
||||
}
|
||||
if let Some { .. } = self.datum {
|
||||
s.serialize_field("datum", &self.datum)?;
|
||||
}
|
||||
s.serialize_field("redeemer", &self.redeemer)?;
|
||||
s.serialize_field("compiledCode", &source_code)?;
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Purpose {
|
||||
Spend,
|
||||
Mint,
|
||||
Withdraw,
|
||||
Publish,
|
||||
}
|
||||
|
||||
impl Purpose {
|
||||
pub fn min_arity(&self) -> u8 {
|
||||
match self {
|
||||
Purpose::Spend => 3,
|
||||
Purpose::Mint | Purpose::Withdraw | Purpose::Publish => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Purpose {
|
||||
fn from(purpose: String) -> Purpose {
|
||||
match &purpose[..] {
|
||||
"spend" => Purpose::Spend,
|
||||
"mint" => Purpose::Mint,
|
||||
"withdraw" => Purpose::Withdraw,
|
||||
"publish" => Purpose::Publish,
|
||||
unexpected => panic!("Can't turn '{}' into any Purpose", unexpected),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Purpose {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Purpose::Spend => "spend",
|
||||
Purpose::Mint => "mint",
|
||||
Purpose::Withdraw => "withdraw",
|
||||
Purpose::Publish => "publish",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::schema::Constructor;
|
||||
use super::*;
|
||||
use serde_json::{self, json};
|
||||
use uplc::parser;
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let program = parser::program("(program 1.0.0 (con integer 42))")
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let validator = Validator {
|
||||
description: Some("Lorem ipsum".to_string()),
|
||||
purpose: Purpose::Spend,
|
||||
datum: None,
|
||||
redeemer: Schema::AnyOf(vec![Constructor {
|
||||
index: 0,
|
||||
fields: vec![Schema::Bytes],
|
||||
}]),
|
||||
program,
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&validator).unwrap(),
|
||||
json!({
|
||||
"description": "Lorem ipsum",
|
||||
"purpose": "spend",
|
||||
"redeemer": {
|
||||
"dataType": "constructor",
|
||||
"index": 0,
|
||||
"fields": [{
|
||||
"dataType": "bytes"
|
||||
}]
|
||||
},
|
||||
"compiledCode": "46010000481501",
|
||||
"hash": "27dc8e44c17b4ae5f4b9286ab599fffe70e61b49dec61eaca1fc5898"
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user