Skip to main content
Deno 2 is finally here 🎉️
Learn more
Module

x/lucid/src/plutus/data.ts

Lucid is a library, which allows you to create Cardano transactions and off-chain code for your Plutus contracts in JavaScript, Deno and Node.js.
Very Popular
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
import { Static as _Static, TEnum, TLiteral, TLiteralValue, TProperties, TSchema, Type,} from "https://deno.land/x/typebox@0.25.13/src/typebox.ts";import { C } from "../core/mod.ts";import { Datum, Json, Redeemer } from "../types/mod.ts";import { fromHex, fromText, toHex } from "../utils/utils.ts";
export class Constr<T> { index: number; fields: T[];
constructor(index: number, fields: T[]) { this.index = index; this.fields = fields; }}
export declare namespace Data { export type Static<T extends TSchema, P extends unknown[] = []> = _Static< T, P >;}
export type Data = | bigint // Integer | string // Bytes in hex | Array<Data> | Map<Data, Data> // AssocList | Constr<Data>;
export const Data = { // Types // Note: Recursive types are not supported (yet) Integer: function ( options?: { minimum?: number; maximum?: number; exclusiveMinimum?: number; exclusiveMaximum?: number; }, ) { const integer = Type.Unsafe<bigint>({ dataType: "integer" }); if (options) { Object.entries(options).forEach(([key, value]) => { integer[key] = value; }); } return integer; }, Bytes: function ( options?: { minLength?: number; maxLength?: number; enum?: string[] }, ) { const bytes = Type.Unsafe<string>({ dataType: "bytes" }); if (options) { Object.entries(options).forEach(([key, value]) => { bytes[key] = value; }); } return bytes; }, Boolean: function () { return Type.Unsafe<boolean>({ anyOf: [ { title: "False", dataType: "constructor", index: 0, fields: [], }, { title: "True", dataType: "constructor", index: 1, fields: [], }, ], }); }, Any: function () { return Type.Unsafe<Data>({ description: "Any Data." }); }, Array: function <T extends TSchema>( items: T, options?: { minItems?: number; maxItems?: number; uniqueItems?: boolean }, ) { const array = Type.Array(items); replaceProperties(array, { dataType: "list", items }); if (options) { Object.entries(options).forEach(([key, value]) => { array[key] = value; }); } return array; }, Map: function <T extends TSchema, U extends TSchema>( keys: T, values: U, options?: { minItems?: number; maxItems?: number }, ) { const map = Type.Unsafe<Map<Data.Static<T>, Data.Static<U>>>({ dataType: "map", keys, values, }); if (options) { Object.entries(options).forEach(([key, value]) => { map[key] = value; }); } return map; }, /** * Object applies by default a PlutusData Constr with index 0.\ * Set 'hasConstr' to false to serialize Object as PlutusData List. */ Object: function <T extends TProperties>( properties: T, options?: { hasConstr?: boolean }, ) { const object = Type.Object(properties); replaceProperties(object, { anyOf: [{ dataType: "constructor", index: 0, // Will be replaced when using Data.Enum fields: Object.entries(properties).map(([title, p]) => ({ ...p, title, })), }], }); object.anyOf[0].hasConstr = typeof options?.hasConstr === "undefined" || options.hasConstr; return object; }, Enum: function <T extends TSchema>(items: T[]) { const union = Type.Union(items); replaceProperties( union, { anyOf: items.map((item, index) => item.anyOf[0].fields.length === 0 ? ({ ...item.anyOf[0], index, }) : ({ dataType: "constructor", title: (() => { const title = item.anyOf[0].fields[0].title; if ( (title as string).charAt(0) !== (title as string).charAt(0).toUpperCase() ) { throw new Error( `Enum '${title}' needs to start with an uppercase letter.`, ); } return item.anyOf[0].fields[0].title; })(), index, fields: item.anyOf[0].fields[0].items, }) ), }, ); return union; }, /** * Tuple is by default a PlutusData List.\ * Set 'hasConstr' to true to apply a PlutusData Constr with index 0. */ Tuple: function <T extends TSchema[]>( items: [...T], options?: { hasConstr?: boolean }, ) { const tuple = Type.Tuple(items); replaceProperties(tuple, { dataType: "list", items, }); if (options) { Object.entries(options).forEach(([key, value]) => { tuple[key] = value; }); } return tuple; }, Literal: function <T extends TLiteralValue>(title: T): TLiteral<T> { if ( (title as string).charAt(0) !== (title as string).charAt(0).toUpperCase() ) { throw new Error( `Enum '${title}' needs to start with an uppercase letter.`, ); } const literal = Type.Literal(title); replaceProperties(literal, { anyOf: [{ dataType: "constructor", title, index: 0, // Will be replaced in Data.Enum fields: [], }], }); return literal; }, Nullable: function <T extends TSchema>(item: T) { return Type.Unsafe<Data.Static<T> | null>({ anyOf: [ { title: "Some", description: "An optional value.", dataType: "constructor", index: 0, fields: [ item, ], }, { title: "None", description: "Nothing.", dataType: "constructor", index: 1, fields: [], }, ], }); },
/** * Convert PlutusData to Cbor encoded data.\ * Or apply a shape and convert the provided data struct to Cbor encoded data. */ to, /** Convert Cbor encoded data to PlutusData */ from, /** * Note Constr cannot be used here.\ * Strings prefixed with '0x' are not UTF-8 encoded. */ fromJson, /** * Note Constr cannot be used here, also only bytes/integers as Json keys.\ */ toJson, void: function (): Datum | Redeemer { return "d87980"; }, castFrom, castTo,};
/** * Convert PlutusData to Cbor encoded data.\ * Or apply a shape and convert the provided data struct to Cbor encoded data. */function to<T = Data>(data: T, shape?: Json): Datum | Redeemer { function serialize(data: Data): C.PlutusData { try { if ( typeof data === "bigint" ) { return C.PlutusData.new_integer(C.BigInt.from_str(data.toString())); } else if (typeof data === "string") { return C.PlutusData.new_bytes(fromHex(data)); } else if (data instanceof Constr) { const { index, fields } = data; const plutusList = C.PlutusList.new();
fields.forEach((field) => plutusList.add(serialize(field)));
return C.PlutusData.new_constr_plutus_data( C.ConstrPlutusData.new( C.BigNum.from_str(index.toString()), plutusList, ), ); } else if (data instanceof Array) { const plutusList = C.PlutusList.new();
data.forEach((arg) => plutusList.add(serialize(arg)));
return C.PlutusData.new_list(plutusList); } else if (data instanceof Map) { const plutusMap = C.PlutusMap.new();
for (const [key, value] of data.entries()) { plutusMap.insert(serialize(key), serialize(value)); }
return C.PlutusData.new_map(plutusMap); } throw new Error("Unsupported type"); } catch (error) { throw new Error("Could not serialize the data: " + error); } } const d = shape ? castTo<T>(data, shape) : data as Data; return toHex(serialize(d).to_bytes()) as Datum | Redeemer;}
/** * Convert Cbor encoded data to Data.\ * Or apply a shape and cast the cbor encoded data to a certain type. */function from<T = Data>(raw: Datum | Redeemer, shape?: Json): T { function deserialize(data: C.PlutusData): Data { if (data.kind() === 0) { const constr = data.as_constr_plutus_data()!; const l = constr.data(); const desL = []; for (let i = 0; i < l.len(); i++) { desL.push(deserialize(l.get(i))); } return new Constr(parseInt(constr.alternative().to_str()), desL); } else if (data.kind() === 1) { const m = data.as_map()!; const desM: Map<Data, Data> = new Map(); const keys = m.keys(); for (let i = 0; i < keys.len(); i++) { desM.set(deserialize(keys.get(i)), deserialize(m.get(keys.get(i))!)); } return desM; } else if (data.kind() === 2) { const l = data.as_list()!; const desL = []; for (let i = 0; i < l.len(); i++) { desL.push(deserialize(l.get(i))); } return desL; } else if (data.kind() === 3) { return BigInt(data.as_integer()!.to_str()); } else if (data.kind() === 4) { return toHex(data.as_bytes()!); } throw new Error("Unsupported type"); } const data = deserialize(C.PlutusData.from_bytes(fromHex(raw)));
return shape ? castFrom<T>(data, shape) : data as T;}
/** * Note Constr cannot be used here.\ * Strings prefixed with '0x' are not UTF-8 encoded. */function fromJson(json: Json): Data { function toData(json: Json): Data { if (typeof json === "string") { return json.startsWith("0x") ? toHex(fromHex(json.slice(2))) : fromText(json); } if (typeof json === "number") return BigInt(json); if (typeof json === "bigint") return json; if (json instanceof Array) return json.map((v) => toData(v)); if (json instanceof Object) { const tempMap: Map<Data, Data> = new Map(); Object.entries(json).forEach(([key, value]) => { tempMap.set(toData(key), toData(value)); }); return tempMap as Data; } throw new Error("Unsupported type"); } return toData(json);}
/** * Note Constr cannot be used here, also only bytes/integers as Json keys.\ */function toJson(plutusData: Data): Json { function fromData(data: Data): Json { if ( typeof data === "bigint" || typeof data === "number" || (typeof data === "string" && !isNaN(parseInt(data)) && data.slice(-1) === "n") ) { const bigint = typeof data === "string" ? BigInt(data.slice(0, -1)) : data; return parseInt(bigint.toString()); } if (typeof data === "string") { try { return new TextDecoder(undefined, { fatal: true }).decode( fromHex(data), ); } catch (_) { return "0x" + toHex(fromHex(data)); } } if (data instanceof Array) return data.map((v) => fromData(v)); if (data instanceof Map) { const tempJson: Json = {}; data.forEach((value, key) => { const convertedKey = fromData(key); if ( typeof convertedKey !== "string" && typeof convertedKey !== "number" ) { throw new Error( "Unsupported type (Note: Only bytes or integers can be keys of a JSON object)", ); } tempJson[convertedKey] = fromData(value); }); return tempJson; } throw new Error( "Unsupported type (Note: Constructor cannot be converted to JSON)", ); } return fromData(plutusData);}
function castFrom<T>(data: Data, shape: Json): T { if (!shape) throw new Error("Could not type cast data."); const shapeType = (shape.anyOf ? "enum" : "") || shape.dataType;
switch (shapeType) { case "integer": { if (typeof data !== "bigint") { throw new Error("Could not type cast to integer."); } integerConstraints(data, shape); return data as T; } case "bytes": { if (typeof data !== "string") { throw new Error("Could not type cast to bytes."); } bytesConstraints(data, shape); return data as T; } case "constructor": { if ( data instanceof Constr && data.index === shape.index && (shape.hasConstr || shape.hasConstr === undefined) ) { const fields: Record<string, T> = {}; if (shape.fields.length !== data.fields.length) { throw new Error("Could not ype cast to object. Fields do not match."); } shape.fields.forEach( (field: Json, fieldIndex: number) => { if ((/[A-Z]/.test(field.title[0]))) { throw new Error( "Could not type cast to object. Object properties need to start with a lowercase letter.", ); } fields[field.title] = castFrom<T>( data.fields[fieldIndex], field, ); }, ); return fields as T; } else if ( data instanceof Array && !shape.hasConstr && shape.hasConstr !== undefined ) { const fields: Record<string, T> = {}; if (shape.fields.length !== data.length) { throw new Error("Could not ype cast to object. Fields do not match."); } shape.fields.forEach( (field: Json, fieldIndex: number) => { if ((/[A-Z]/.test(field.title[0]))) { throw new Error( "Could not type cast to object. Object properties need to start with a lowercase letter.", ); } fields[field.title] = castFrom<T>( data[fieldIndex], field, ); }, ); return fields as T; } throw new Error("Could not type cast to object."); } case "enum": { // When enum has only one entry it's a single constructor/record object if (shape.anyOf.length === 1) { return castFrom<T>(data, shape.anyOf[0]); }
if (!(data instanceof Constr)) { throw new Error("Could not type cast to enum."); }
const enumShape = shape.anyOf.find((entry: Json) => entry.index === data.index ); if (!enumShape || enumShape.fields.length !== data.fields.length) { throw new Error("Could not type cast to enum."); } if (isBoolean(shape)) { if (data.fields.length !== 0) { throw new Error("Could not type cast to boolean."); } switch (data.index) { case 0: return false as T; case 1: return true as T; } throw new Error("Could not type cast to boolean."); } else if (isNullable(shape)) { switch (data.index) { case 0: { if ( data.fields.length !== 1 ) { throw new Error("Could not type cast to nullable object."); } return castFrom<T>(data.fields[0], shape.anyOf[0].fields[0]); } case 1: { if ( data.fields.length !== 0 ) { throw new Error("Could not type cast to nullable object."); } return null as T; } } throw new Error("Could not type cast to nullable object."); } switch (enumShape.dataType) { case "constructor": { if (enumShape.fields.length === 0) { if ( /[A-Z]/.test(enumShape.title[0]) ) { return enumShape.title as T; } throw new Error("Could not type cast to enum."); } else { if (!(/[A-Z]/.test(enumShape.title))) { throw new Error( "Could not type cast to enum. Enums need to start with an uppercase letter.", ); }
if (enumShape.fields.length !== data.fields.length) { throw new Error("Could not type cast to enum."); }
return { [enumShape.title]: enumShape.fields.map(( field: Json, index: number, ) => castFrom<T>(data.fields[index], field)), } as T; } } } throw new Error("Could not type cast to enum."); } case "list": { if (shape.items instanceof Array) { // tuple if ( data instanceof Constr && data.index === 0 && shape.hasConstr ) { return data.fields.map((field, index) => castFrom<T>(field, shape.items[index]) ) as T; } else if (data instanceof Array && !shape.hasConstr) { return data.map((field, index) => castFrom<T>(field, shape.items[index]) ) as T; }
throw new Error("Could not type cast to tuple."); } else { // array if (!(data instanceof Array)) { throw new Error("Could not type cast to array."); } listConstraints(data, shape);
return data.map((field) => castFrom<T>(field, shape.items)) as T; } } case "map": { if (!(data instanceof Map)) { throw new Error("Could not type cast to map."); } mapConstraints(data, shape); const map = new Map(); for ( const [key, value] of (data) .entries() ) { map.set(castFrom<T>(key, shape.keys), castFrom<T>(value, shape.values)); } return map as T; } case undefined: { return data as T; } } throw new Error("Could not type cast data.");}
function castTo<T>(struct: T, shape: Json): Data { if (!shape) throw new Error("Could not type cast struct."); const shapeType = (shape.anyOf ? "enum" : "") || shape.dataType;
switch (shapeType) { case "integer": { if (typeof struct !== "bigint") { throw new Error("Could not type cast to integer."); } integerConstraints(struct, shape); return struct as bigint; } case "bytes": { if (typeof struct !== "string") { throw new Error("Could not type cast to bytes."); } bytesConstraints(struct, shape); return struct as string; } case "constructor": { if (typeof struct !== "object" || struct === null) { throw new Error("Could not type cast to constructor."); } const fields = shape.fields.map((field: Json) => castTo<T>((struct as Record<string, T>)[field.title], field) ); return (shape.hasConstr || shape.hasConstr === undefined) ? new Constr(shape.index, fields) : fields; } case "enum": { // When enum has only one entry it's a single constructor/record object if (shape.anyOf.length === 1) { return castTo<T>(struct, shape.anyOf[0]); }
if (isBoolean(shape)) { if (typeof struct !== "boolean") { throw new Error( "Could not type cast to boolean.", ); } return new Constr(struct ? 1 : 0, []); } else if (isNullable(shape)) { if (struct === null) return new Constr(1, []); else { const fields = shape.anyOf[0].fields; if (fields.length !== 1) { throw new Error("Could not type cast to nullable object."); } return new Constr(0, [ castTo<T>(struct, fields[0]), ]); } } switch (typeof struct) { case "string": { if (!(/[A-Z]/.test(struct[0]))) { throw new Error( "Could not type cast to enum. Enum needs to start with an uppercase letter.", ); } const enumIndex = (shape as TEnum).anyOf.findIndex(( s: TLiteral, ) => s.dataType === "constructor" && s.fields.length === 0 && s.title === struct ); if (enumIndex === -1) throw new Error("Could not type cast to enum."); return new Constr(enumIndex, []); } case "object": { if (struct === null) throw new Error("Could not type cast to enum."); const structTitle = Object.keys(struct)[0];
if (!(/[A-Z]/.test(structTitle))) { throw new Error( "Could not type cast to enum. Enum needs to start with an uppercase letter.", ); } const enumEntry = shape.anyOf.find((s: Json) => s.dataType === "constructor" && s.title === structTitle );
if (!enumEntry) throw new Error("Could not type cast to enum.");
return new Constr( enumEntry.index, (struct as Record<string, T[]>)[structTitle].map((item, index) => castTo<T>(item, enumEntry.fields[index]) ), ); } } throw new Error("Could not type cast to enum."); } case "list": { if (!(struct instanceof Array)) { throw new Error("Could not type cast to array/tuple."); } if (shape.items instanceof Array) { // tuple const fields = struct.map((item, index) => castTo<T>(item, shape.items[index]) ); return shape.hasConstr ? new Constr(0, fields) : fields; } else { // array listConstraints(struct, shape); return struct.map((item) => castTo<T>(item, shape.items)); } } case "map": { if (!(struct instanceof Map)) { throw new Error("Could not type cast to map."); }
mapConstraints(struct, shape);
const map = new Map<Data, Data>(); for ( const [key, value] of (struct) .entries() ) { map.set(castTo<T>(key, shape.keys), castTo<T>(value, shape.values)); } return map; } case undefined: { return struct as Data; } } throw new Error("Could not type cast struct.");}
function integerConstraints(integer: bigint, shape: TSchema) { if (shape.minimum && integer < BigInt(shape.minimum)) { throw new Error( `Integer ${integer} is below the minimum ${shape.minimum}.`, ); } if (shape.maximum && integer > BigInt(shape.maximum)) { throw new Error( `Integer ${integer} is above the maxiumum ${shape.maximum}.`, ); } if (shape.exclusiveMinimum && integer <= BigInt(shape.exclusiveMinimum)) { throw new Error( `Integer ${integer} is below the exclusive minimum ${shape.exclusiveMinimum}.`, ); } if (shape.exclusiveMaximum && integer >= BigInt(shape.exclusiveMaximum)) { throw new Error( `Integer ${integer} is above the exclusive maximum ${shape.exclusiveMaximum}.`, ); }}
function bytesConstraints(bytes: string, shape: TSchema) { if ( shape.enum && !shape.enum.some((keyword: string) => keyword === bytes) ) throw new Error(`None of the keywords match with '${bytes}'.`); if (shape.minLength && bytes.length / 2 < shape.minLength) { throw new Error( `Bytes need to have a length of at least ${shape.minLength} bytes.`, ); }
if (shape.maxLength && bytes.length / 2 > shape.maxLength) { throw new Error( `Bytes can have a length of at most ${shape.minLength} bytes.`, ); }}
function listConstraints(list: Array<unknown>, shape: TSchema) { if (shape.minItems && list.length < shape.minItems) { throw new Error( `Array needs to contain at least ${shape.minItems} items.`, ); } if (shape.maxItems && list.length > shape.maxItems) { throw new Error( `Array can contain at most ${shape.maxItems} items.`, ); } if (shape.uniqueItems && (new Set(list)).size !== list.length) { // Note this only works for primitive types like string and bigint. throw new Error( "Array constains duplicates.", ); }}
function mapConstraints(map: Map<unknown, unknown>, shape: TSchema) { if (shape.minItems && map.size < shape.minItems) { throw new Error( `Map needs to contain at least ${shape.minItems} items.`, ); }
if (shape.maxItems && map.size > shape.maxItems) { throw new Error( `Map can contain at most ${shape.maxItems} items.`, ); }}
function isBoolean(shape: TSchema): boolean { return shape.anyOf && shape.anyOf[0]?.title === "False" && shape.anyOf[1]?.title === "True";}
function isNullable(shape: TSchema): boolean { return shape.anyOf && shape.anyOf[0]?.title === "Some" && shape.anyOf[1]?.title === "None";}
function replaceProperties(object: Json, properties: Json) { Object.keys(object).forEach((key) => { delete object[key]; }); Object.assign(object, properties);}