diff --git a/packages/core/src/Serialization/PlutusData.ts b/packages/core/src/Serialization/PlutusData.ts deleted file mode 100644 index 9bfa9636cda..00000000000 --- a/packages/core/src/Serialization/PlutusData.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as Cardano from '../Cardano'; -import { CML, cmlToCore, coreToCml } from '../CML'; -import { HexBlob, usingAutoFree } from '@cardano-sdk/util'; -import { isAnyPlutusDataCollection } from '../Cardano/util'; - -export class PlutusData { - #corePlutusData: Cardano.PlutusData; - - constructor(corePlutusData: Cardano.PlutusData) { - this.#corePlutusData = corePlutusData; - } - - static fromCbor(cbor: HexBlob): PlutusData { - return new PlutusData( - usingAutoFree((scope) => cmlToCore.plutusData(scope.manage(CML.PlutusData.from_bytes(Buffer.from(cbor, 'hex'))))) - ); - } - - static fromCore(plutusData: Cardano.PlutusData) { - return new PlutusData(plutusData); - } - - toCbor(): HexBlob { - if (isAnyPlutusDataCollection(this.#corePlutusData) && this.#corePlutusData.cbor) { - return this.#corePlutusData.cbor; - } - - return HexBlob( - Buffer.from(usingAutoFree((scope) => coreToCml.plutusData(scope, this.#corePlutusData).to_bytes())).toString( - 'hex' - ) - ); - } - - toCore(): Cardano.PlutusData { - return this.#corePlutusData; - } -} diff --git a/packages/core/src/Serialization/PlutusData/ConstrPlutusData.ts b/packages/core/src/Serialization/PlutusData/ConstrPlutusData.ts new file mode 100644 index 00000000000..63bd2e62669 --- /dev/null +++ b/packages/core/src/Serialization/PlutusData/ConstrPlutusData.ts @@ -0,0 +1,136 @@ +import { CborReader, CborWriter } from '../CBOR'; +import { HexBlob } from '@cardano-sdk/util'; +import { PlutusList } from './PlutusList'; +import { hexToBytes } from '../../util/misc'; + +const GENERAL_FORM_TAG = 102n; +const ALTERNATIVE_TAG_OFFSET = 7n; + +/** + * The main datatype `Constr` represents the nth constructor + * along with its arguments. + * + * Remark: We don't directly serialize the alternative in the tag, + * instead the scheme is: + * + * - Alternatives 0-6 -> tags 121-127, followed by the arguments in a list. + * - Alternatives 7-127 -> tags 1280-1400, followed by the arguments in a list. + * - Any alternatives, including those that don't fit in the above -> tag 102 followed by a list containing + * an unsigned integer for the actual alternative, and then the arguments in a (nested!) list. + */ +export class ConstrPlutusData { + private readonly _alternative: bigint = 0n; + private readonly _data = new PlutusList(); + + /** + * Initializes a new instance of the ConstrPlutusData class. + * + * @param alternative Get the Constr alternative. The alternative represents the nth + * constructor of a 'Sum Type'. + * @param data Gets the list of arguments of the 'Sum Type' as a 'PlutusList'. + */ + constructor(alternative: bigint, data: PlutusList) { + this._alternative = alternative; + this._data = data; + } + + /** + * Serializes this ConstrPlutusData instance into its CBOR representation as a Uint8Array. + * + * @returns The CBOR representation of this instance as a Uint8Array. + */ + toCbor(): HexBlob { + const writer = new CborWriter(); + const compactTag = ConstrPlutusData.alternativeToCompactCborTag(this._alternative); + + writer.writeTag(Number(compactTag)); + + if (compactTag !== GENERAL_FORM_TAG) { + writer.writeEncodedValue(hexToBytes(this._data.toCbor())); + } else { + writer.writeStartArray(2); + writer.writeInt(this._alternative); + writer.writeEncodedValue(hexToBytes(this._data.toCbor())); + } + + return HexBlob.fromBytes(writer.encode()); + } + + /** + * Deserializes a ConstrPlutusData instance from its CBOR representation. + * + * @param cbor The CBOR representation of this instance as a Uint8Array. + * @returns A ConstrPlutusData instance. + */ + static fromCbor(cbor: HexBlob): ConstrPlutusData { + const reader = new CborReader(cbor); + + const tag = reader.readTag(); + + if (tag === Number(GENERAL_FORM_TAG)) { + reader.readStartArray(); + + const alternative = reader.readInt(); + const data = reader.readEncodedValue(); + const plutusList = PlutusList.fromCbor(HexBlob.fromBytes(data)); + + reader.readEndArray(); + + return new ConstrPlutusData(alternative, plutusList); + } + + const alternative = ConstrPlutusData.compactCborTagToAlternative(BigInt(tag)); + const data = reader.readEncodedValue(); + const plutusList = PlutusList.fromCbor(HexBlob.fromBytes(data)); + + return new ConstrPlutusData(alternative, plutusList); + } + + /** + * Gets the ConstrPlutusData alternative. The alternative represents the nth + * constructor of a 'Sum Type'. + * + * @returns The alternative constructor of the 'Sum Type'. + */ + getAlternative(): bigint { + return this._alternative; + } + + /** + * The list of arguments of the 'Sum Type' as a 'PlutusList'. + * + * @returns The list of arguments. + */ + getData(): PlutusList { + return this._data; + } + + // Mapping functions to and from alternative to and from CBOR tags. + // See https://github.com/input-output-hk/plutus/blob/1f31e640e8a258185db01fa899da63f9018c0e85/plutus-core/plutus-core/src/PlutusCore/Data.hs#L69-L72 + + /** + * Converts a CBOR compact tag to a Constr alternative. + * + * @param tag The tag to be converted. + * @returns The Constr alternative. + */ + private static compactCborTagToAlternative(tag: bigint): bigint { + if (tag >= 121n && tag <= 127) return tag - 121n; + if (tag >= 1280n && tag <= 1400) return tag - 1280n + ALTERNATIVE_TAG_OFFSET; + + return GENERAL_FORM_TAG; + } + + /** + * Converts the constructor alternative to its CBOR compact tag. + * + * @param alternative The Constr alternative to be converted. + * @returns The compact CBOR tag. + */ + private static alternativeToCompactCborTag(alternative: bigint): bigint { + if (alternative <= 6n) return 121n + alternative; + if (alternative >= 7n && alternative <= 127n) return 1280n - ALTERNATIVE_TAG_OFFSET + alternative; + + return GENERAL_FORM_TAG; + } +} diff --git a/packages/core/src/Serialization/PlutusData/PlutusData.ts b/packages/core/src/Serialization/PlutusData/PlutusData.ts new file mode 100644 index 00000000000..6f9fa9b4615 --- /dev/null +++ b/packages/core/src/Serialization/PlutusData/PlutusData.ts @@ -0,0 +1,418 @@ +import * as Cardano from '../../Cardano'; +import { CborReader, CborReaderState, CborTag, CborWriter } from '../CBOR'; +import { ConstrPlutusData } from './ConstrPlutusData'; +import { HexBlob } from '@cardano-sdk/util'; +import { NotImplementedError } from '../../errors'; +import { PlutusDataKind } from './PlutusDataKind'; +import { PlutusList } from './PlutusList'; +import { PlutusMap } from './PlutusMap'; +import { bytesToHex } from '../../util/misc'; + +const MAX_WORD64 = 18_446_744_073_709_551_615n; +const INDEFINITE_BYTE_STRING = new Uint8Array([95]); +const MAX_BYTE_STRING_CHUNK_SIZE = 64; + +/** + * A type corresponding to the Plutus Core Data datatype. + * + * The point of this type is to be opaque as to ensure that it is only used in ways + * that plutus scripts can handle. + * + * Use this type to build any data structures that you want to be representable on-chain. + */ +export class PlutusData { + private _map: PlutusMap | undefined = undefined; + private _list: PlutusList | undefined = undefined; + private _integer: bigint | undefined = undefined; + private _bytes: Uint8Array | undefined = undefined; + private _constr: ConstrPlutusData | undefined = undefined; + private _kind: PlutusDataKind = PlutusDataKind.ConstrPlutusData; + private _originalBytes: HexBlob | undefined = undefined; + + /** + * Serializes this PlutusData instance into its CBOR representation as a Uint8Array. + * + * @returns The CBOR representation of this instance as a Uint8Array. + */ + // eslint-disable-next-line complexity + toCbor(): HexBlob { + if (this._originalBytes) return this._originalBytes; + + let cbor: HexBlob; + + switch (this._kind) { + case PlutusDataKind.ConstrPlutusData: { + cbor = this._constr!.toCbor(); + break; + } + case PlutusDataKind.Map: { + cbor = this._map!.toCbor(); + break; + } + case PlutusDataKind.List: { + cbor = this._list!.toCbor(); + break; + } + // Note [The 64-byte limit]: See https://github.com/input-output-hk/plutus/blob/1f31e640e8a258185db01fa899da63f9018c0e85/plutus-core/plutus-core/src/PlutusCore/Data.hs#L61-L105 + // If the bytestring is >64bytes, we encode it as indefinite-length bytestrings with 64-byte chunks. We have to write + // our own encoders/decoders so we can produce chunks of the right size and check + // the sizes when we decode. + case PlutusDataKind.Bytes: { + const writer = new CborWriter(); + + if (this._bytes!.length <= MAX_BYTE_STRING_CHUNK_SIZE) { + writer.writeByteString(this._bytes!); + } else { + writer.writeEncodedValue(INDEFINITE_BYTE_STRING); + + for (let i = 0; i < this._bytes!.length; i += MAX_BYTE_STRING_CHUNK_SIZE) { + const chunk = this._bytes!.slice(i, i + MAX_BYTE_STRING_CHUNK_SIZE); + writer.writeByteString(chunk); + } + + writer.writeEndArray(); + } + + cbor = bytesToHex(writer.encode()); + break; + } + // For integers, we have two cases. Small integers (<64bits) can be encoded normally. Big integers are already + // encoded *with a byte string*. The spec allows this to be an indefinite-length bytestring. Again, we need to + // write some manual encoders/decoders. + case PlutusDataKind.Integer: { + const writer = new CborWriter(); + // If it fits in a Word64, then it's less than 64 bits for sure, and we can just send it off + // as a normal integer. + if ( + (this._integer! >= 0 && this._integer! <= MAX_WORD64) || + (this._integer! < 0 && this._integer! >= -1n - MAX_WORD64) + ) { + writer.writeInt(this._integer!); + } else { + // Otherwise, it would be encoded as a bignum anyway, so we manually do the bignum + // encoding with a bytestring inside. + writer.writeBigInteger(this._integer!); + } + + cbor = bytesToHex(writer.encode()); + break; + } + default: + throw new Error('Unsupported PlutusData kind'); + } + + return cbor; + } + + /** + * Deserializes a PlutusData instance from its CBOR representation. + * + * @param cbor The CBOR representation of this instance as a Uint8Array. + * @returns A PlutusData instance. + */ + // eslint-disable-next-line max-statements + static fromCbor(cbor: HexBlob): PlutusData { + const data = new PlutusData(); + const reader = new CborReader(cbor); + + const peekTokenType = reader.peekState(); + + switch (peekTokenType) { + case CborReaderState.Tag: { + const tag = reader.peekTag(); + + // eslint-disable-next-line sonarjs/no-nested-switch + switch (tag) { + case CborTag.UnsignedBigNum: { + reader.readTag(); + const bytes = reader.readByteString(); + data._integer = PlutusData.bufferToBigint(bytes); + data._kind = PlutusDataKind.Integer; + break; + } + case CborTag.NegativeBigNum: { + reader.readTag(); + const bytes = reader.readByteString(); + data._integer = PlutusData.bufferToBigint(bytes) * -1n; + data._kind = PlutusDataKind.Integer; + break; + } + default: { + data._constr = ConstrPlutusData.fromCbor(HexBlob.fromBytes(reader.readEncodedValue())); + data._kind = PlutusDataKind.ConstrPlutusData; + } + } + break; + } + case CborReaderState.NegativeInteger: + case CborReaderState.UnsignedInteger: { + data._integer = reader.readInt(); + data._kind = PlutusDataKind.Integer; + break; + } + case CborReaderState.StartIndefiniteLengthByteString: + case CborReaderState.ByteString: { + data._bytes = reader.readByteString(); + data._kind = PlutusDataKind.Bytes; + break; + } + case CborReaderState.StartArray: { + data._list = PlutusList.fromCbor(HexBlob.fromBytes(reader.readEncodedValue())); + data._kind = PlutusDataKind.List; + break; + } + case CborReaderState.StartMap: { + data._map = PlutusMap.fromCbor(HexBlob.fromBytes(reader.readEncodedValue())); + data._kind = PlutusDataKind.Map; + break; + } + default: { + throw new Error('Invalid Plutus Data'); + } + } + + data._originalBytes = cbor; + + return data; + } + + /** + * Creates a Core Tx object from the current PlutusData object. + * + * @returns The PlutusData object. + */ + toCore(): Cardano.PlutusData { + switch (this._kind) { + case PlutusDataKind.Bytes: + return this._bytes!; + case PlutusDataKind.ConstrPlutusData: { + const constrPlutusData = this._constr; + return { + cbor: this.toCbor(), + constructor: constrPlutusData!.getAlternative(), + fields: PlutusData.mapToCorePlutusList(constrPlutusData!.getData()) + } as Cardano.ConstrPlutusData; + } + case PlutusDataKind.Integer: + return this._integer!; + case PlutusDataKind.List: + return PlutusData.mapToCorePlutusList(this._list!); + case PlutusDataKind.Map: { + const plutusMap = this._map!; + const coreMap = new Map(); + const keys = plutusMap.getKeys(); + for (let i = 0; i < keys.getLength(); i++) { + const key = keys.get(i); + coreMap.set(key.toCore(), plutusMap.get(key)!.toCore()); + } + return { cbor: this.toCbor(), data: coreMap } as Cardano.PlutusMap; + } + default: + throw new NotImplementedError(`PlutusData mapping for kind ${this._kind}`); // Probably can't happen + } + } + + /** + * Creates a PlutusData object from the given Core PlutusData object. + * + * @param data The core PlutusData object. + */ + static fromCore(data: Cardano.PlutusData) { + if (Cardano.util.isPlutusBoundedBytes(data)) { + return PlutusData.newBytes(data); + } else if (Cardano.util.isPlutusBigInt(data)) { + return PlutusData.newInteger(data); + } + + if (data.cbor) return PlutusData.fromCbor(data.cbor); + + if (Cardano.util.isPlutusList(data)) { + return PlutusData.newList(PlutusData.mapToPlutusList(data.items)); + } else if (Cardano.util.isPlutusMap(data)) { + const plutusMap = new PlutusMap(); + for (const [key, val] of data.data) { + plutusMap.insert(PlutusData.fromCore(key), PlutusData.fromCore(val)); + } + return PlutusData.newMap(plutusMap); + } else if (Cardano.util.isConstrPlutusData(data)) { + const alternative = data.constructor; + const constrPlutusData = new ConstrPlutusData(alternative, PlutusData.mapToPlutusList(data.fields.items)); + + return PlutusData.newConstrPlutusData(constrPlutusData); + } + + throw new NotImplementedError('PlutusData type not implemented'); + } + + /** + * Create a PlutusData type from the given ConstrPlutusData. + * + * @param constrPlutusData The ConstrPlutusData to be 'cast' as PlutusData. + * @returns The ConstrPlutusData as a PlutusData object. + */ + static newConstrPlutusData(constrPlutusData: ConstrPlutusData): PlutusData { + const data = new PlutusData(); + + data._constr = constrPlutusData; + data._kind = PlutusDataKind.ConstrPlutusData; + + return data; + } + + /** + * Create a PlutusData type from the given PlutusMap. + * + * @param map The PlutusMap to be 'cast' as PlutusData. + * @returns The PlutusMap as a PlutusData object. + */ + static newMap(map: PlutusMap): PlutusData { + const data = new PlutusData(); + + data._map = map; + data._kind = PlutusDataKind.Map; + + return data; + } + + /** + * Create a PlutusData type from the given PlutusList. + * + * @param list The PlutusList to be 'cast' as PlutusData. + * @returns The PlutusMap as a PlutusList object. + */ + static newList(list: PlutusList): PlutusData { + const data = new PlutusData(); + + data._list = list; + data._kind = PlutusDataKind.List; + + return data; + } + + /** + * Create a PlutusData type from the given bigint. + * + * @param integer The bigint to be 'cast' as PlutusData. + * @returns The bigint as a PlutusList object. + */ + static newInteger(integer: bigint): PlutusData { + const data = new PlutusData(); + + data._integer = integer; + data._kind = PlutusDataKind.Integer; + + return data; + } + + /** + * Create a PlutusData type from the given Uint8Array. + * + * @param bytes The Uint8Array to be 'cast' as PlutusData. + * @returns The Uint8Array as a PlutusList object. + */ + static newBytes(bytes: Uint8Array): PlutusData { + const data = new PlutusData(); + + data._bytes = bytes; + data._kind = PlutusDataKind.Bytes; + + return data; + } + + /** + * Gets the underlying type of this PlutusData instance. + * + * @returns The underlying type. + */ + getKind(): PlutusDataKind { + return this._kind; + } + + /** + * Down casts this PlutusData instance as a ConstrPlutusData instance. + * + * @returns The ConstrPlutusData instance or undefined if it can not be 'down cast'. + */ + asConstrPlutusData(): ConstrPlutusData | undefined { + return this._constr; + } + + /** + * Down casts this PlutusData instance as a PlutusMap instance. + * + * @returns The PlutusMap instance or undefined if it can not be 'down cast'. + */ + asMap(): PlutusMap | undefined { + return this._map; + } + + /** + * Down casts this PlutusData instance as a PlutusList instance. + * + * @returns The PlutusList instance or undefined if it can not be 'down cast'. + */ + asList(): PlutusList | undefined { + return this._list; + } + + /** + * Down casts this PlutusData instance as a bigint instance. + * + * @returns The bigint value or undefined if it can not be 'down cast'. + */ + asInteger(): bigint | undefined { + return this._integer; + } + + /** + * Down casts this PlutusData instance as a Uint8Array instance. + * + * @returns The Uint8Array or undefined if it can not be 'down cast'. + */ + asBoundedBytes(): Uint8Array | undefined { + return this._bytes; + } + + /** + * Maps to PlutusList from a core plutus list. + * + * @param list The core plutus list. + */ + private static mapToPlutusList(list: Cardano.PlutusData[]): PlutusList { + const plutusList = new PlutusList(); + for (const listItem of list) { + plutusList.add(PlutusData.fromCore(listItem)); + } + return plutusList; + } + + /** + * Maps to Core plutus list from PlutusList. + * + * @param list The PlutusList + */ + private static mapToCorePlutusList(list: PlutusList): Cardano.PlutusList { + const items: Cardano.PlutusData[] = []; + for (let i = 0; i < list.getLength(); i++) { + const element = list.get(i); + items.push(element.toCore()); + } + return { cbor: list.toCbor(), items }; + } + + /** + * Converts an Uint8Array to a bigint. + * + * @param buffer The buffer to be converted to bigint. + * @returns The resulting bigint; + */ + private static bufferToBigint(buffer: Uint8Array): bigint { + let ret = 0n; + for (const i of buffer.values()) { + const bi = BigInt(i); + // eslint-disable-next-line no-bitwise + ret = (ret << 8n) + bi; + } + return ret; + } +} diff --git a/packages/core/src/Serialization/PlutusData/PlutusDataKind.ts b/packages/core/src/Serialization/PlutusData/PlutusDataKind.ts new file mode 100644 index 00000000000..c83db1e8b56 --- /dev/null +++ b/packages/core/src/Serialization/PlutusData/PlutusDataKind.ts @@ -0,0 +1,29 @@ +/** + * The plutus data type kind. + */ +export enum PlutusDataKind { + /** + * Represents a specific constructor of a 'Sum Type' along with its arguments. + */ + ConstrPlutusData, + + /** + * A map of PlutusData as both key and values. + */ + Map, + + /** + * A list of PlutusData. + */ + List, + + /** + * An integer. + */ + Integer, + + /** + * Bounded bytes. + */ + Bytes +} diff --git a/packages/core/src/Serialization/PlutusData/PlutusList.ts b/packages/core/src/Serialization/PlutusData/PlutusList.ts new file mode 100644 index 00000000000..c35a23aecc9 --- /dev/null +++ b/packages/core/src/Serialization/PlutusData/PlutusList.ts @@ -0,0 +1,85 @@ +import { CborReader, CborReaderState, CborWriter } from '../CBOR'; +import { HexBlob } from '@cardano-sdk/util'; +import { PlutusData } from './PlutusData'; +import { bytesToHex, hexToBytes } from '../../util/misc'; + +/** + * A list of plutus data. + */ +export class PlutusList { + private readonly _array = new Array(); + private _useIndefiniteEncoding = true; + + /** + * Serializes this PlutusList instance into its CBOR representation as a Uint8Array. + * + * @returns The CBOR representation of this instance as a Uint8Array. + */ + toCbor(): HexBlob { + const writer = new CborWriter(); + + if (this._useIndefiniteEncoding) { + writer.writeStartArray(); + } else { + writer.writeStartArray(this._array.length); + } + + for (const elem of this._array) { + writer.writeEncodedValue(hexToBytes(elem.toCbor())); + } + + if (this._useIndefiniteEncoding) writer.writeEndArray(); + + return HexBlob.fromBytes(writer.encode()); + } + + /** + * Deserializes a PlutusList instance from its CBOR representation. + * + * @param cbor The CBOR representation of this instance as a Uint8Array. + * @returns A PlutusList instance. + */ + static fromCbor(cbor: HexBlob): PlutusList { + const list = new PlutusList(); + const reader = new CborReader(cbor); + + const length = reader.readStartArray(); + + if (length === null) list._useIndefiniteEncoding = true; + + while (reader.peekState() !== CborReaderState.EndArray) { + list.add(PlutusData.fromCbor(bytesToHex(reader.readEncodedValue()))); + } + + reader.readEndArray(); + + return list; + } + + /** + * Gets the length of the list. + * + * @returns the length of the list. + */ + getLength(): number { + return this._array.length; + } + + /** + * Gets an element from the list. + * + * @param index The index in the list of the element to get. + */ + get(index: number): PlutusData { + return this._array[index]; + } + + /** + * Adds an element to the Plutus List. + * + * @param elem The element to be added. + */ + add(elem: PlutusData): void { + this._array.push(elem); + } +} diff --git a/packages/core/src/Serialization/PlutusData/PlutusMap.ts b/packages/core/src/Serialization/PlutusData/PlutusMap.ts new file mode 100644 index 00000000000..470ed8150b9 --- /dev/null +++ b/packages/core/src/Serialization/PlutusData/PlutusMap.ts @@ -0,0 +1,108 @@ +import { CborReader, CborReaderState, CborWriter } from '../CBOR'; +import { HexBlob } from '@cardano-sdk/util'; +import { PlutusData } from './PlutusData'; +import { PlutusList } from './PlutusList'; +import { bytesToHex, hexToBytes } from '../../util/misc'; + +/** + * Represents a Map of Plutus data. + */ +export class PlutusMap { + private readonly _map = new Map(); + private _useIndefiniteEncoding = false; + + /** + * Serializes this PlutusMap instance into its CBOR representation as a Uint8Array. + * + * @returns The CBOR representation of this instance as a Uint8Array. + */ + toCbor(): HexBlob { + const writer = new CborWriter(); + + if (this._useIndefiniteEncoding) { + writer.writeStartMap(); + } else { + writer.writeStartMap(this._map.size); + } + + for (const [key, value] of this._map.entries()) { + writer.writeEncodedValue(hexToBytes(key.toCbor())); + writer.writeEncodedValue(hexToBytes(value.toCbor())); + } + + if (this._useIndefiniteEncoding) writer.writeEndMap(); + + return HexBlob.fromBytes(writer.encode()); + } + + /** + * Deserializes a PlutusMap instance from its CBOR representation. + * + * @param cbor The CBOR representation of this instance as a Uint8Array. + * @returns A PlutusMap instance. + */ + static fromCbor(cbor: HexBlob): PlutusMap { + const map = new PlutusMap(); + const reader = new CborReader(cbor); + + const size = reader.readStartMap(); + + if (size === null) map._useIndefiniteEncoding = true; + + while (reader.peekState() !== CborReaderState.EndMap) { + const key = PlutusData.fromCbor(bytesToHex(reader.readEncodedValue())); + const value = PlutusData.fromCbor(bytesToHex(reader.readEncodedValue())); + + map.insert(key, value); + } + + reader.readEndMap(); + + return map; + } + + /** + * Gets the length of the map. + * + * @returns the length of the map. + */ + getLength(): number { + return this._map.size; + } + + /** + * Adds an element to the map. + * + * @param key The key of the element in the map. + * @param value The value of the element. + */ + insert(key: PlutusData, value: PlutusData) { + this._map.set(key, value); + } + + /** + * Returns the specified element from the map. + * + * @param key The key of the element to return from the map. + * @returns The element associated with the specified key in the map, or undefined + * if there is no element with the given key. + */ + get(key: PlutusData): PlutusData | undefined { + return this._map.get(key); + } + + /** + * Gets all the keys from the map as a plutus list. + * + * @returns The keys of the map as a plutus list. + */ + getKeys(): PlutusList { + const list = new PlutusList(); + + for (const elem of this._map.keys()) { + list.add(elem); + } + + return list; + } +} diff --git a/packages/core/src/Serialization/PlutusData/index.ts b/packages/core/src/Serialization/PlutusData/index.ts new file mode 100644 index 00000000000..c4ad07d5ba3 --- /dev/null +++ b/packages/core/src/Serialization/PlutusData/index.ts @@ -0,0 +1,5 @@ +export * from './ConstrPlutusData'; +export * from './PlutusData'; +export * from './PlutusDataKind'; +export * from './PlutusList'; +export * from './PlutusMap'; diff --git a/packages/core/test/Serialization/PlutusData.test.ts b/packages/core/test/Serialization/PlutusData.test.ts index 224025e4bb1..a0396f59542 100644 --- a/packages/core/test/Serialization/PlutusData.test.ts +++ b/packages/core/test/Serialization/PlutusData.test.ts @@ -10,7 +10,7 @@ describe('PlutusData', () => { expect(fromCbor.toCore()).toEqual(plutusData); }); - it.skip('converts (TODO: describe is special about this that fails) inline datum', () => { + it('converts (TODO: describe is special about this that fails) inline datum', () => { // tx: https://preprod.cexplorer.io/tx/32d2b9062680c7ef5673114abce804d8b854f54440518e48a6db3e555f3a84d2 // parsed datum: https://preprod.cexplorer.io/datum/f20e5a0a42a9015cd4e53f8b8c020e535957f782ea3231453fe4cf46a52d07c9 const cbor = HexBlob( @@ -18,4 +18,169 @@ describe('PlutusData', () => { ); expect(() => Serialization.PlutusData.fromCbor(cbor)).not.toThrowError(); }); + + describe('Integer', () => { + it('can encode a positive integer', () => { + const data = Serialization.PlutusData.newInteger(5n); + expect(data.toCbor()).toEqual('05'); + }); + + it('can encode a negative integer', () => { + const data = Serialization.PlutusData.newInteger(-5n); + expect(data.toCbor()).toEqual('24'); + }); + + it('can encode an integer bigger than unsigned 64bits', () => { + const data = Serialization.PlutusData.newInteger(18_446_744_073_709_551_616n); + expect(data.toCbor()).toEqual('c249010000000000000000'); + }); + + it('can encode a negative integer bigger than unsigned 64bits', () => { + const data = Serialization.PlutusData.newInteger(-18_446_744_073_709_551_616n); + expect(data.toCbor()).toEqual('3bffffffffffffffff'); + }); + + it('can decode a positive integer', () => { + const data = Serialization.PlutusData.fromCbor(HexBlob('05')); + expect(data.asInteger()).toEqual(5n); + }); + + it('can decode a negative integer', () => { + const data = Serialization.PlutusData.fromCbor(HexBlob('24')); + expect(data.asInteger()).toEqual(-5n); + }); + + it('can decode an integer bigger than unsigned 64bits', () => { + const data = Serialization.PlutusData.fromCbor(HexBlob('c249010000000000000000')); + expect(data.asInteger()).toEqual(18_446_744_073_709_551_616n); + }); + + it('can decode a negative integer bigger than unsigned 64bits', () => { + const data = Serialization.PlutusData.fromCbor(HexBlob('3bffffffffffffffff')); + expect(data.asInteger()).toEqual(-18_446_744_073_709_551_616n); + }); + }); + + describe('Bytes', () => { + it('can encode a small byte string (less than 64 bytes)', () => { + const data = Serialization.PlutusData.newBytes(new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06])); + expect(data.toCbor()).toEqual('46010203040506'); + }); + + it('can decode a small byte string (less than 64 bytes)', () => { + const data = Serialization.PlutusData.fromCbor(HexBlob('46010203040506')); + expect(HexBlob.fromBytes(data.asBoundedBytes()!)).toEqual('010203040506'); + }); + + it('can encode a big byte string (more than 64 bytes)', () => { + const payload = new Uint8Array([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08 + ]); + + const data = Serialization.PlutusData.newBytes(payload); + expect(data.toCbor()).toEqual( + '5f584001020304050607080102030405060708010203040506070801020304050607080102030405060708010203040506070801020304050607080102030405060708584001020304050607080102030405060708010203040506070801020304050607080102030405060708010203040506070801020304050607080102030405060708584001020304050607080102030405060708010203040506070801020304050607080102030405060708010203040506070801020304050607080102030405060708584001020304050607080102030405060708010203040506070801020304050607080102030405060708010203040506070801020304050607080102030405060708ff' + ); + }); + + it('can decode a big byte string (more than 64 bytes)', () => { + const payload = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08 + ]; + + const data = Serialization.PlutusData.fromCbor( + HexBlob( + '5f584001020304050607080102030405060708010203040506070801020304050607080102030405060708010203040506070801020304050607080102030405060708584001020304050607080102030405060708010203040506070801020304050607080102030405060708010203040506070801020304050607080102030405060708584001020304050607080102030405060708010203040506070801020304050607080102030405060708010203040506070801020304050607080102030405060708584001020304050607080102030405060708010203040506070801020304050607080102030405060708010203040506070801020304050607080102030405060708ff' + ) + ); + expect([...data.asBoundedBytes()!]).toEqual(payload); + }); + }); + + describe('List', () => { + it('can encode simple plutus list', () => { + const data = new Serialization.PlutusList(); + + data.add(Serialization.PlutusData.newInteger(1n)); + data.add(Serialization.PlutusData.newInteger(2n)); + data.add(Serialization.PlutusData.newInteger(3n)); + data.add(Serialization.PlutusData.newInteger(4n)); + data.add(Serialization.PlutusData.newInteger(5n)); + + expect(data.toCbor()).toEqual('9f0102030405ff'); + }); + + it('can encode a list of plutus list', () => { + const innerList = new Serialization.PlutusList(); + + innerList.add(Serialization.PlutusData.newInteger(1n)); + innerList.add(Serialization.PlutusData.newInteger(2n)); + innerList.add(Serialization.PlutusData.newInteger(3n)); + innerList.add(Serialization.PlutusData.newInteger(4n)); + innerList.add(Serialization.PlutusData.newInteger(5n)); + + const outer = new Serialization.PlutusList(); + + outer.add(Serialization.PlutusData.newInteger(1n)); + outer.add(Serialization.PlutusData.newInteger(2n)); + outer.add(Serialization.PlutusData.newList(innerList)); + outer.add(Serialization.PlutusData.newList(innerList)); + outer.add(Serialization.PlutusData.newInteger(5n)); + + expect(outer.toCbor()).toEqual('9f01029f0102030405ff9f0102030405ff05ff'); + }); + }); + + describe('Map', () => { + it('can encode simple plutus map', () => { + const data = new Serialization.PlutusMap(); + + data.insert(Serialization.PlutusData.newInteger(1n), Serialization.PlutusData.newInteger(2n)); + + expect(data.toCbor()).toEqual('a10102'); + }); + }); + + describe('Constr', () => { + it('can encode simple Constr', () => { + const args = new Serialization.PlutusList(); + args.add(Serialization.PlutusData.newInteger(1n)); + args.add(Serialization.PlutusData.newInteger(2n)); + args.add(Serialization.PlutusData.newInteger(3n)); + args.add(Serialization.PlutusData.newInteger(4n)); + args.add(Serialization.PlutusData.newInteger(5n)); + + const data = new Serialization.ConstrPlutusData(0n, args); + + expect(data.toCbor()).toEqual('d8799f0102030405ff'); + }); + }); }); diff --git a/packages/ogmios/test/CardanoNode/__snapshots__/ObservableOgmiosCardanoNode.test.ts.snap b/packages/ogmios/test/CardanoNode/__snapshots__/ObservableOgmiosCardanoNode.test.ts.snap index a74c1cc3388..7709f1437ee 100644 --- a/packages/ogmios/test/CardanoNode/__snapshots__/ObservableOgmiosCardanoNode.test.ts.snap +++ b/packages/ogmios/test/CardanoNode/__snapshots__/ObservableOgmiosCardanoNode.test.ts.snap @@ -73,15 +73,21 @@ Array [ "witness": Object { "bootstrap": Array [], "datums": Array [ - Uint8Array [], + Object { + "data": Array [], + "type": "Buffer", + }, ], "redeemers": Array [ Object { - "data": Uint8Array [ - 77, - 53, - 122, - ], + "data": Object { + "data": Array [ + 77, + 53, + 122, + ], + "type": "Buffer", + }, "executionUnits": Object { "memory": 8959327235984229000n, "steps": 4473903472119932000n, @@ -106,15 +112,21 @@ Array [ "cbor": "9f2341e14478c0fcf5ff", "items": Array [ -4n, - Uint8Array [ - 225, - ], - Uint8Array [ - 120, - 192, - 252, - 245, - ], + Object { + "data": Array [ + 225, + ], + "type": "Buffer", + }, + Object { + "data": Array [ + 120, + 192, + 252, + 245, + ], + "type": "Buffer", + }, ], }, }, @@ -125,11 +137,14 @@ Array [ "fields": Object { "cbor": "9f438002ab0104ff", "items": Array [ - Uint8Array [ - 128, - 2, - 171, - ], + Object { + "data": Array [ + 128, + 2, + 171, + ], + "type": "Buffer", + }, 1n, 4n, ], @@ -138,12 +153,15 @@ Array [ Object { "cbor": "a1448858788900", "data": Map { - Uint8Array [ - 136, - 88, - 120, - 137, - ] => 0n, + Object { + "data": Array [ + 136, + 88, + 120, + 137, + ], + "type": "Buffer", + } => 0n, }, }, ], @@ -157,7 +175,10 @@ Array [ "cbor": "a0", "data": Map {}, }, - Uint8Array [], + Object { + "data": Array [], + "type": "Buffer", + }, ], }, ], diff --git a/packages/ogmios/test/ogmiosToCore/__snapshots__/block.test.ts.snap b/packages/ogmios/test/ogmiosToCore/__snapshots__/block.test.ts.snap index 561223cd9b7..62d949eab24 100644 --- a/packages/ogmios/test/ogmiosToCore/__snapshots__/block.test.ts.snap +++ b/packages/ogmios/test/ogmiosToCore/__snapshots__/block.test.ts.snap @@ -138,6 +138,12 @@ Object { "data": Object { "cbor": "a34160029f20a3419c204386a48244e21bd936202004a0ffa3a4435aed90040242b07a210523409f414d429b3f0440ffa09f034001ffa222234041b4d8668218da9f2341440302413eff4204e1a1d866821901939f23ffa0", "data": Map { + Object { + "data": Array [ + 96, + ], + "type": "Buffer", + } => 2n, Object { "cbor": "9f20a3419c204386a48244e21bd936202004a0ff", "items": Array [ @@ -145,20 +151,29 @@ Object { Object { "cbor": "a3419c204386a48244e21bd9362020", "data": Map { + Object { + "data": Array [ + 156, + ], + "type": "Buffer", + } => -1n, + Object { + "data": Array [ + 134, + 164, + 130, + ], + "type": "Buffer", + } => Object { + "data": Array [ + 226, + 27, + 217, + 54, + ], + "type": "Buffer", + }, -1n => -1n, - Uint8Array [ - 134, - 164, - 130, - ] => Uint8Array [ - 226, - 27, - 217, - 54, - ], - Uint8Array [ - 156, - ] => -1n, }, }, 4n, @@ -170,53 +185,80 @@ Object { } => Object { "cbor": "a3a4435aed90040242b07a210523409f414d429b3f0440ffa09f034001ffa222234041b4d8668218da9f2341440302413eff", "data": Map { - Object { - "cbor": "a0", - "data": Map {}, - } => Object { - "cbor": "9f034001ff", - "items": Array [ - 3n, - Uint8Array [], - 1n, - ], - }, Object { "cbor": "a4435aed90040242b07a21052340", "data": Map { - -4n => Uint8Array [], + Object { + "data": Array [ + 90, + 237, + 144, + ], + "type": "Buffer", + } => 4n, + 2n => Object { + "data": Array [ + 176, + 122, + ], + "type": "Buffer", + }, -2n => 5n, - 2n => Uint8Array [ - 176, - 122, - ], - Uint8Array [ - 90, - 237, - 144, - ] => 4n, + -4n => Object { + "data": Array [], + "type": "Buffer", + }, }, } => Object { "cbor": "9f414d429b3f0440ff", "items": Array [ - Uint8Array [ - 77, - ], - Uint8Array [ - 155, - 63, - ], + Object { + "data": Array [ + 77, + ], + "type": "Buffer", + }, + Object { + "data": Array [ + 155, + 63, + ], + "type": "Buffer", + }, 4n, - Uint8Array [], + Object { + "data": Array [], + "type": "Buffer", + }, + ], + }, + Object { + "cbor": "a0", + "data": Map {}, + } => Object { + "cbor": "9f034001ff", + "items": Array [ + 3n, + Object { + "data": Array [], + "type": "Buffer", + }, + 1n, ], }, Object { "cbor": "a222234041b4", "data": Map { -3n => -4n, - Uint8Array [] => Uint8Array [ - 180, - ], + Object { + "data": Array [], + "type": "Buffer", + } => Object { + "data": Array [ + 180, + ], + "type": "Buffer", + }, }, } => Object { "cbor": "d8668218da9f2341440302413eff", @@ -225,23 +267,32 @@ Object { "cbor": "9f2341440302413eff", "items": Array [ -4n, - Uint8Array [ - 68, - ], + Object { + "data": Array [ + 68, + ], + "type": "Buffer", + }, 3n, 2n, - Uint8Array [ - 62, - ], + Object { + "data": Array [ + 62, + ], + "type": "Buffer", + }, ], }, }, }, }, - Uint8Array [ - 4, - 225, - ] => Object { + Object { + "data": Array [ + 4, + 225, + ], + "type": "Buffer", + } => Object { "cbor": "a1d866821901939f23ffa0", "data": Map { Object { @@ -259,9 +310,6 @@ Object { }, }, }, - Uint8Array [ - 96, - ] => 2n, }, }, "executionUnits": Object {