Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/data-to-string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@evolution-sdk/evolution": minor
---

`toString` and `toJSON` now render Plutus data. `toString` threw `Do not know how to serialize a BigInt` on any `InlineDatum`, `UTxO`, `TxOut`, `TransactionOutput`, `Redeemer`, `RedeemerMap`, `RedeemerArray` or `TransactionWitnessSet` whose data held an integer or a constructor, and so did a template string or `JSON.stringify` holding one. Data without integers rendered wrongly: a byte string as an object of byte values, and a map as `{}`.

The new `Data.toJSON` returns data in its encoded form, as `DataSchema` writes it: an integer as a decimal string, a byte string as hex, a map as `[key, value]` pairs and a constructor as `{ index, fields }`. These classes use it in `toJSON`, so `toJSON().data` is now that encoded form rather than `Data`. `RedeemerValue`, which `RedeemerMap.get` returns, gains `toJSON` and `toString`.

`Value.toJSON` writes `coin` as a decimal string, as `MultiAsset` already writes quantities, so `toString` no longer throws on a `BabbageTransactionOutput`.

`console.log` and `util.inspect` still show the data itself, with `bigint` integers and `Uint8Array` bytes.
8 changes: 8 additions & 0 deletions packages/evolution/src/Data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,14 @@ export const hash = (data: Data): number => {
*/
export const equals: (a: Data, b: Data) => boolean = Schema.equivalence(DataSchema)

/**
* Plutus data in its JSON-safe encoded form.
*
* @since 2.0.0
* @category conversions
*/
export const toJSON: (data: Data) => DataEncoded = Schema.encodeSync(DataSchema)

export const CDDLSchema = CBOR.CBORSchema

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/evolution/src/InlineDatum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class InlineDatum extends Schema.TaggedClass<InlineDatum>()("InlineDatum"
* @category json
*/
toJSON() {
return { _tag: "InlineDatum" as const, data: this.data }
return { _tag: "InlineDatum" as const, data: PlutusData.toJSON(this.data) }
}

/**
Expand All @@ -32,7 +32,7 @@ export class InlineDatum extends Schema.TaggedClass<InlineDatum>()("InlineDatum"
* @category inspect
*/
[Inspectable.NodeInspectSymbol](): unknown {
return this.toJSON()
return { _tag: "InlineDatum" as const, data: this.data }
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/evolution/src/Redeemer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export class Redeemer extends Schema.Class<Redeemer>("Redeemer")({
_tag: "Redeemer",
tag: this.tag,
index: this.index.toString(),
data: this.data,
data: PlutusData.toJSON(this.data),
exUnits: this.exUnits.toJSON()
}
}
Expand All @@ -117,7 +117,7 @@ export class Redeemer extends Schema.Class<Redeemer>("Redeemer")({
* @category conversions
*/
[Inspectable.NodeInspectSymbol](): unknown {
return this.toJSON()
return { ...this.toJSON(), data: this.data }
}

/**
Expand Down
18 changes: 15 additions & 3 deletions packages/evolution/src/Redeemers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ export class RedeemerValue extends Schema.Class<RedeemerValue>("RedeemerValue")(
data: Schema.typeSchema(Data.DataSchema),
exUnits: Redeemer.ExUnits
}) {
toJSON() {
return { data: Data.toJSON(this.data), exUnits: this.exUnits.toJSON() }
}

toString(): string {
return Inspectable.format(this.toJSON())
}

[Inspectable.NodeInspectSymbol](): unknown {
return { data: this.data, exUnits: this.exUnits }
}

[Equal.symbol](that: unknown): boolean {
return (
that instanceof RedeemerValue && Data.equals(this.data, that.data) && Equal.equals(this.exUnits, that.exUnits)
Expand Down Expand Up @@ -144,7 +156,7 @@ export class RedeemerMap extends Schema.TaggedClass<RedeemerMap>()("RedeemerMap"
_tag: "RedeemerMap" as const,
entries: Array.from(this.value.entries()).map(([[tag, index], { data, exUnits }]) => ({
key: { tag, index: index.toString() },
value: { data, exUnits: exUnits.toJSON() }
value: { data: Data.toJSON(data), exUnits: exUnits.toJSON() }
}))
}
}
Expand All @@ -154,7 +166,7 @@ export class RedeemerMap extends Schema.TaggedClass<RedeemerMap>()("RedeemerMap"
}

[Inspectable.NodeInspectSymbol](): unknown {
return this.toJSON()
return { ...this.toJSON(), entries: Array.from(this.value.entries()).map(([key, value]) => ({ key, value })) }
}

[Equal.symbol](that: unknown): boolean {
Expand Down Expand Up @@ -246,7 +258,7 @@ export class RedeemerArray extends Schema.TaggedClass<RedeemerArray>()("Redeemer
}

[Inspectable.NodeInspectSymbol](): unknown {
return this.toJSON()
return { ...this.toJSON(), value: this.value }
}

[Equal.symbol](that: unknown): boolean {
Expand Down
4 changes: 2 additions & 2 deletions packages/evolution/src/TransactionWitnessSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export class TransactionWitnessSet extends Schema.Class<TransactionWitnessSet>("
nativeScripts: this.nativeScripts?.map((s) => Schema.encodeSync(NativeScripts.NativeScript)(s)),
bootstrapWitnesses: this.bootstrapWitnesses?.map((b) => b.toJSON()),
plutusV1Scripts: this.plutusV1Scripts,
plutusData: this.plutusData,
plutusData: this.plutusData?.map(PlutusData.toJSON),
redeemers: this.redeemers?.toJSON(),
plutusV2Scripts: this.plutusV2Scripts,
plutusV3Scripts: this.plutusV3Scripts
Expand All @@ -230,7 +230,7 @@ export class TransactionWitnessSet extends Schema.Class<TransactionWitnessSet>("
* @category inspect
*/
[Inspectable.NodeInspectSymbol](): unknown {
return this.toJSON()
return { ...this.toJSON(), plutusData: this.plutusData, redeemers: this.redeemers }
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/evolution/src/TxOut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class TransactionOutput extends Schema.TaggedClass<TransactionOutput>()("
}

[Inspectable.NodeInspectSymbol](): unknown {
return this.toJSON()
return { ...this.toJSON(), datumOption: this.datumOption }
}

[Equal.symbol](that: unknown): boolean {
Expand Down
8 changes: 4 additions & 4 deletions packages/evolution/src/Value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class OnlyCoin extends Schema.TaggedClass<OnlyCoin>("OnlyCoin")("OnlyCoin
toJSON() {
return {
_tag: this._tag,
coin: this.coin
coin: this.coin.toString()
}
}

Expand All @@ -36,7 +36,7 @@ export class OnlyCoin extends Schema.TaggedClass<OnlyCoin>("OnlyCoin")("OnlyCoin
}

[Inspectable.NodeInspectSymbol](): unknown {
return this.toJSON()
return { ...this.toJSON(), coin: this.coin }
}

[Equal.symbol](that: unknown): boolean {
Expand All @@ -55,7 +55,7 @@ export class WithAssets extends Schema.TaggedClass<WithAssets>("WithAssets")("Wi
toJSON() {
return {
_tag: this._tag,
coin: this.coin,
coin: this.coin.toString(),
assets: this.assets
}
}
Expand All @@ -65,7 +65,7 @@ export class WithAssets extends Schema.TaggedClass<WithAssets>("WithAssets")("Wi
}

[Inspectable.NodeInspectSymbol](): unknown {
return this.toJSON()
return { ...this.toJSON(), coin: this.coin }
}

[Equal.symbol](that: unknown): boolean {
Expand Down
96 changes: 96 additions & 0 deletions packages/evolution/test/Data.ToString.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { inspect } from "node:util"

import { describe, expect, it } from "vitest"

import * as Address from "../src/Address.js"
import * as AddressEras from "../src/AddressEras.js"
import * as AssetName from "../src/AssetName.js"
import * as Assets from "../src/Assets.js"
import * as Bytes from "../src/Bytes.js"
import * as Data from "../src/Data.js"
import * as InlineDatum from "../src/InlineDatum.js"
import * as MultiAsset from "../src/MultiAsset.js"
import * as PolicyId from "../src/PolicyId.js"
import * as Redeemer from "../src/Redeemer.js"
import * as Redeemers from "../src/Redeemers.js"
import * as TransactionHash from "../src/TransactionHash.js"
import * as TransactionOutput from "../src/TransactionOutput.js"
import * as TransactionWitnessSet from "../src/TransactionWitnessSet.js"
import * as TxOut from "../src/TxOut.js"
import * as UTxO from "../src/UTxO.js"
import * as Value from "../src/Value.js"

const data = Data.constr(0n, [Data.int(42n), Data.bytearray("ab"), Data.map([[Data.int(1n), Data.int(2n)]])])
const rendered = { index: "0", fields: ["42", "ab", [["1", "2"]]] }

const addressHex = "60" + "11".repeat(28)
const address = Address.fromBytes(Bytes.fromHex(addressHex))
const datumOption = new InlineDatum.InlineDatum({ data })
const exUnits = new Redeemer.ExUnits({ mem: 1n, steps: 1n })
const redeemer = new Redeemer.Redeemer({ tag: "spend", index: 0n, data, exUnits })

const values = {
InlineDatum: datumOption,
TxOut: new TxOut.TransactionOutput({ address, assets: Assets.fromLovelace(1_000_000n), datumOption }),
UTxO: new UTxO.UTxO({
transactionId: TransactionHash.fromHex("00".repeat(32)),
index: 0n,
address,
assets: Assets.fromLovelace(1_000_000n),
datumOption
}),
BabbageTransactionOutput: new TransactionOutput.BabbageTransactionOutput({
address: AddressEras.fromHex(addressHex),
amount: Value.onlyCoin(1_000_000n),
datumOption
}),
Redeemer: redeemer,
RedeemerMap: Redeemers.makeRedeemerMap([redeemer]),
RedeemerValue: new Redeemers.RedeemerValue({ data, exUnits }),
RedeemerArray: new Redeemers.RedeemerArray({ value: [redeemer] }),
TransactionWitnessSet: new TransactionWitnessSet.TransactionWitnessSet({ plutusData: [data] })
}

describe("toString on values holding Plutus data", () => {
it.each(Object.entries(values))("%s renders its data", (_, value) => {
expect(() => value.toString()).not.toThrow()
expect(() => `${value}`).not.toThrow()
expect(JSON.stringify(value)).toContain(JSON.stringify(rendered))
})

it.each(Object.entries(values))("%s inspects its data as bigints and bytes", (_, value) => {
expect(inspect(value, { depth: null })).toContain("42n")
})

it("renders a bare integer datum", () => {
expect(new InlineDatum.InlineDatum({ data: Data.int(1n) }).toJSON()).toEqual({ _tag: "InlineDatum", data: "1" })
})
})

describe("Data.toJSON", () => {
it.each([
["an integer", Data.int(1n), "1"],
["a negative integer", Data.int(-5n), "-5"],
["an integer past 2^64", Data.int(2n ** 70n), (2n ** 70n).toString()],
["a byte string", Data.bytearray("ab"), "ab"],
["an empty byte string", Data.bytearray(""), ""],
["a list", Data.list([Data.int(1n), Data.bytearray("ab")]), ["1", "ab"]],
["a map", Data.map([[Data.bytearray("ab"), Data.int(2n)]]), [["ab", "2"]]],
["a constructor", Data.constr(7n, [Data.int(1n)]), { index: "7", fields: ["1"] }]
])("renders %s", (_, data, expected) => {
expect(Data.toJSON(data)).toEqual(expected)
})
})

describe("Value.toJSON", () => {
const assets = MultiAsset.singleton(PolicyId.fromHex("0".repeat(56)), AssetName.fromHex("4d79546f6b656e"), 3n)

it.each([
["OnlyCoin", Value.onlyCoin(5n)],
["WithAssets", Value.withAssets(5n, assets)]
])("%s writes coin as a string and inspects it as a bigint", (_, value) => {
expect(value.toJSON().coin).toBe("5")
expect(() => value.toString()).not.toThrow()
expect(inspect(value)).toContain("coin: 5n")
})
})
Loading