diff --git a/.changeset/uplc-bool-js-boolean.md b/.changeset/uplc-bool-js-boolean.md new file mode 100644 index 00000000..0f828324 --- /dev/null +++ b/.changeset/uplc-bool-js-boolean.md @@ -0,0 +1,5 @@ +--- +"@evolution-sdk/evolution": patch +--- + +Fix the UPLC flat encoder encoding a Bool constant given as a plain JS boolean `false` as true. `ConstantValue` permits a JS boolean, but the Bool encoder read `.index` off the value, which a boolean does not have, so `con bool false` and `con bool true` produced identical flat bytes. The encoder now handles the boolean form directly; the decoded `{ index }` form encodes exactly as before. diff --git a/packages/evolution/src/UPLC.ts b/packages/evolution/src/UPLC.ts index 760f9ef8..ef8d89d3 100644 --- a/packages/evolution/src/UPLC.ts +++ b/packages/evolution/src/UPLC.ts @@ -950,8 +950,11 @@ const encodeData = (state: EncodeState, dataType: DataType, value: ConstantValue } else if (dataType === "Unit") { // No encoding needed } else if (dataType === "Bool") { - const boolVal = value as { index: bigint } - pushBit(state, boolVal.index === 0n ? 0 : 1) + // ConstantValue permits a plain JS boolean as well as the decoded + // { index } form; a boolean has no .index, so reading it would + // encode false as true. + const bit = typeof value === "boolean" ? (value ? 1 : 0) : (value as { index: bigint }).index === 0n ? 0 : 1 + pushBit(state, bit) } else if (dataType === "Data") { encodeByteString(state, value as Uint8Array) } else if ("list" in dataType) { diff --git a/packages/evolution/test/UPLC.test.ts b/packages/evolution/test/UPLC.test.ts index 349ea76f..720473cb 100644 --- a/packages/evolution/test/UPLC.test.ts +++ b/packages/evolution/test/UPLC.test.ts @@ -167,6 +167,51 @@ describe("UPLC Module", () => { const decoded2 = UPLC.fromFlatBytes(reencoded) expect(decoded2.version).toBe(program.version) }) + + describe("Bool constants given as plain JS booleans", () => { + const boolProgram = (value: UPLC.ConstantValue) => + new UPLC.Program({ version: "1.0.0", body: UPLC.constantTerm("Bool", value) }) + + const decodedConstant = (bytes: Uint8Array): UPLC.ConstantValue => { + const decoded = UPLC.fromFlatBytes(bytes) + if (decoded.body.type !== "Constant") throw new Error("expected a Constant term") + return decoded.body.value + } + + it("encodes JS false as False, not True", () => { + const falseBytes = UPLC.toFlatBytes(boolProgram(false)) + const trueBytes = UPLC.toFlatBytes(boolProgram(true)) + expect(Bytes.toHex(falseBytes)).not.toBe(Bytes.toHex(trueBytes)) + expect(decodedConstant(falseBytes)).toEqual({ index: 0n, fields: [] }) + }) + + it("encodes JS true as True", () => { + const trueBytes = UPLC.toFlatBytes(boolProgram(true)) + expect(decodedConstant(trueBytes)).toEqual({ index: 1n, fields: [] }) + }) + + it("matches the { index } representation byte for byte", () => { + expect(Bytes.toHex(UPLC.toFlatBytes(boolProgram(false)))).toBe( + Bytes.toHex(UPLC.toFlatBytes(boolProgram({ index: 0n, fields: [] }))) + ) + expect(Bytes.toHex(UPLC.toFlatBytes(boolProgram(true)))).toBe( + Bytes.toHex(UPLC.toFlatBytes(boolProgram({ index: 1n, fields: [] }))) + ) + }) + + it("roundtrips JS booleans nested in a list of Bool", () => { + const program = new UPLC.Program({ + version: "1.0.0", + body: UPLC.constantTerm({ list: "Bool" }, { items: [false, true] }) + }) + expect(decodedConstant(UPLC.toFlatBytes(program))).toEqual({ + items: [ + { index: 0n, fields: [] }, + { index: 1n, fields: [] } + ] + }) + }) + }) }) describe("Schema Transform - FromFlatBytes", () => {