From 2a03cda77c0fe83734b88b4309f68d4ab7768b17 Mon Sep 17 00:00:00 2001 From: oscarozaine Date: Sun, 7 Jun 2026 00:10:16 -0700 Subject: [PATCH] fix(transaction): preserve Map/bigint in cloneOutput via structuredClone cloneOutput used a JSON round-trip, which silently drops Map entries and mangles bigint inside Mesh Data inline datums (e.g. CIP-68 metadata from metadataToCip68). The empty Map then made serializeOutput throw "Cannot convert undefined to a BigInt". Use structuredClone, consistent with how the tx builder already clones txOutput and other queue items in tx-builder-core.ts. Adds regression tests for Map preservation, bigint preservation, deep-copy independence, and plain ADA-only outputs. Fixes #704 Co-Authored-By: Claude Opus 4.8 --- .../src/mesh-tx-builder/index.ts | 5 +- .../test/clone-output.test.ts | 83 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 packages/mesh-transaction/test/clone-output.test.ts diff --git a/packages/mesh-transaction/src/mesh-tx-builder/index.ts b/packages/mesh-transaction/src/mesh-tx-builder/index.ts index e3b248cf1..fe38e3a84 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/index.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/index.ts @@ -1861,7 +1861,10 @@ function tierRefScriptFee( } export const cloneOutput = (output: Output): Output => { - return JSONBig.parse(JSONBig.stringify(output)); + // structuredClone preserves Map, bigint and nested Mesh Data objects, which a + // JSON round-trip silently drops (e.g. a Map in an inline datum becomes `{}`). + // See https://github.com/MeshJS/mesh/issues/704 + return structuredClone(output); }; export const setLoveLace = (output: Output, lovelace: bigint): Output => { diff --git a/packages/mesh-transaction/test/clone-output.test.ts b/packages/mesh-transaction/test/clone-output.test.ts new file mode 100644 index 000000000..d20ba8316 --- /dev/null +++ b/packages/mesh-transaction/test/clone-output.test.ts @@ -0,0 +1,83 @@ +import { Output } from "@meshsdk/common"; + +import { cloneOutput } from "../src"; + +// Regression tests for https://github.com/MeshJS/mesh/issues/704 +// cloneOutput previously used a JSON round-trip, which silently drops Map +// entries and mangles bigint values inside Mesh `Data` inline datums (e.g. +// CIP-68 metadata produced by metadataToCip68), causing +// "Cannot convert undefined to a BigInt" during serializeOutput. +describe("cloneOutput", () => { + it("preserves a Map inside a Mesh Data inline datum", () => { + const metadata = new Map([ + ["name", "Mesh Token"], + ["image", "ipfs://Qm..."], + ]); + + const output: Output = { + address: "addr_test1vqld...", + amount: [{ unit: "lovelace", quantity: "2000000" }], + datum: { + type: "Inline", + data: { + type: "Mesh", + // CIP-68-style constructor: { alternative, fields: [Map, version] } + content: { alternative: 0, fields: [metadata, 1n] }, + }, + }, + }; + + const cloned = cloneOutput(output); + const clonedContent = cloned.datum!.data.content as { + alternative: number; + fields: unknown[]; + }; + const clonedMap = clonedContent.fields[0] as Map; + + // The Map must survive cloning (JSON would have produced an empty object). + // Use the toStringTag rather than `instanceof` so the assertion is robust to + // the cross-realm Map that structuredClone yields inside the jest VM. + expect(Object.prototype.toString.call(clonedMap)).toBe("[object Map]"); + expect(clonedMap.size).toBe(2); + expect(clonedMap.get("name")).toBe("Mesh Token"); + expect(clonedMap.get("image")).toBe("ipfs://Qm..."); + + // bigint must survive (JSON.stringify throws on / coerces bigint). + expect(clonedContent.fields[1]).toBe(1n); + expect(typeof clonedContent.fields[1]).toBe("bigint"); + }); + + it("returns a deep copy that is independent of the original", () => { + const output: Output = { + address: "addr_test1vqld...", + amount: [ + { unit: "lovelace", quantity: "2000000" }, + { unit: "abc123", quantity: "5" }, + ], + datum: { + type: "Inline", + data: { type: "Mesh", content: { alternative: 0, fields: [42n] } }, + }, + }; + + const cloned = cloneOutput(output); + + // Structurally equal... + expect(cloned).toEqual(output); + // ...but not the same references (mutating the clone must not touch source). + expect(cloned).not.toBe(output); + expect(cloned.amount).not.toBe(output.amount); + + cloned.amount[0]!.quantity = "999"; + expect(output.amount[0]!.quantity).toBe("2000000"); + }); + + it("clones a plain ADA-only output", () => { + const output: Output = { + address: "addr_test1vqld...", + amount: [{ unit: "lovelace", quantity: "1500000" }], + }; + + expect(cloneOutput(output)).toEqual(output); + }); +});