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
5 changes: 4 additions & 1 deletion packages/mesh-transaction/src/mesh-tx-builder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
83 changes: 83 additions & 0 deletions packages/mesh-transaction/test/clone-output.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>([
["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<string, string>;

// 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);
});
});