From 2969e4f4cad2dfd407b92846d7d388c6a2cde733 Mon Sep 17 00:00:00 2001 From: Dharmveer Bharti Date: Sat, 30 Nov 2024 19:02:13 +0530 Subject: [PATCH 1/6] [Fix] merge metadata by tag Fixes: - preserve metadata with multiple assets minting Features: - Transaction's setMetadata(), MeshTxBuilder's metadataValue(): 3rd argument to recursively merge new metadata with existing metadata entries under that tag Thank you for contributing to Mesh! We appreciate your effort and dedication to improving this project. To ensure that your contribution is in line with the project's guidelines and can be reviewed efficiently, please fill out the template below. --- packages/mesh-transaction/src/index.ts | 1 + .../src/mesh-tx-builder/tx-builder-core.ts | 9 +- .../mesh-transaction/src/transaction/index.ts | 11 +- packages/mesh-transaction/src/utils/index.ts | 1 + .../mesh-transaction/src/utils/metadata.ts | 133 ++++++ .../test/transaction/txMetadata.test.ts | 417 ++++++++++++++++++ 6 files changed, 567 insertions(+), 5 deletions(-) create mode 100644 packages/mesh-transaction/src/utils/index.ts create mode 100644 packages/mesh-transaction/src/utils/metadata.ts create mode 100644 packages/mesh-transaction/test/transaction/txMetadata.test.ts diff --git a/packages/mesh-transaction/src/index.ts b/packages/mesh-transaction/src/index.ts index 83c938075..ac43b6b15 100644 --- a/packages/mesh-transaction/src/index.ts +++ b/packages/mesh-transaction/src/index.ts @@ -1,3 +1,4 @@ export * from "./mesh-tx-builder"; export * from "./scripts"; export * from "./transaction"; +export * from "./utils"; diff --git a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts index 617304c03..2b4a867ce 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts @@ -35,6 +35,8 @@ import { Withdrawal, } from "@meshsdk/common"; +import { MetadataMergeLevel, mergeAllMetadataByTag } from "../utils"; + export class MeshTxBuilderCore { txEvaluationMultiplier = 1.1; private txOutput?: Output; @@ -1414,11 +1416,16 @@ export class MeshTxBuilderCore { * Add metadata to the transaction * @param tag The tag of the metadata * @param metadata The metadata in any format + * @param mergeExistingMetadataByTag Whether to merge the new metadata + * under a tag with the existing metadata with the same tag * @returns The MeshTxBuilder instance */ - metadataValue = (tag: string, metadata: any) => { + metadataValue = (tag: string, metadata: any, mergeExistingMetadataByTag: MetadataMergeLevel = false) => { const metadataString = JSONBig.stringify(metadata); this.meshTxBuilderBody.metadata.push({ tag, metadata: metadataString }); + if (mergeExistingMetadataByTag) { + this.meshTxBuilderBody.metadata = mergeAllMetadataByTag(this.meshTxBuilderBody.metadata, tag, mergeExistingMetadataByTag); + } return this; }; diff --git a/packages/mesh-transaction/src/transaction/index.ts b/packages/mesh-transaction/src/transaction/index.ts index 5c1977336..fb3eecfef 100644 --- a/packages/mesh-transaction/src/transaction/index.ts +++ b/packages/mesh-transaction/src/transaction/index.ts @@ -33,6 +33,7 @@ import { } from "@meshsdk/core-cst"; import { MeshTxBuilder, MeshTxBuilderOptions } from "../mesh-tx-builder"; +import { MetadataMergeLevel } from "../utils"; export interface TransactionOptions extends MeshTxBuilderOptions { initiator: IInitiator; @@ -472,9 +473,9 @@ export class Transaction { if (mint.label === "721" || mint.label === "20") { this.setMetadata(Number(mint.label), { [policyId]: { [mint.assetName]: mint.metadata }, - }); + }, mint.label === "721" ? 2 : true); } else { - this.setMetadata(Number(mint.label), mint.metadata); + this.setMetadata(Number(mint.label), mint.metadata, true); } } @@ -587,11 +588,13 @@ export class Transaction { * * @param {number} key The key to use for the metadata entry. * @param {unknown} value The value to use for the metadata entry. + * @param {MetadataMergeLevel} mergeExistingMetadataByTag Whether to merge the new metadata + * under a tag (key) with the existing metadata with the same tag * @returns {Transaction} The Transaction object. * @see {@link https://meshjs.dev/apis/transaction#setMetadata} */ - setMetadata(key: number, value: unknown): Transaction { - this.txBuilder.metadataValue(key.toString(), value as object); + setMetadata(key: number, value: unknown, mergeExistingMetadataByTag: MetadataMergeLevel = false): Transaction { + this.txBuilder.metadataValue(key.toString(), value as object, mergeExistingMetadataByTag); return this; } diff --git a/packages/mesh-transaction/src/utils/index.ts b/packages/mesh-transaction/src/utils/index.ts new file mode 100644 index 000000000..e6c55b6c9 --- /dev/null +++ b/packages/mesh-transaction/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./metadata"; diff --git a/packages/mesh-transaction/src/utils/metadata.ts b/packages/mesh-transaction/src/utils/metadata.ts new file mode 100644 index 000000000..e345ce0b2 --- /dev/null +++ b/packages/mesh-transaction/src/utils/metadata.ts @@ -0,0 +1,133 @@ +import JSONBig from "json-bigint"; +import { Metadata } from "@meshsdk/common"; + +type ValueType = "array" | "object" | "primitive" | "nullish"; + +export type MetadataMergeLevel = boolean | number; + +const getMergeDepth = (mergeOption: MetadataMergeLevel): number => { + return typeof mergeOption === "number" + ? mergeOption + : mergeOption === true + ? 1 + : 0; +} + +const getType = (value: unknown): ValueType => { + if (Array.isArray(value)) + return "array"; + if (value !== null && typeof value === "object") + return "object"; + if (value === null || value === undefined) + return "nullish"; + return "primitive"; +} + +/** + * Recursively merge two items. Returns the 2nd item if the maximum allowed + * merge depth has passed. + * + * Merging objects ({ key: value }): + * Two objects are merged by recursively including the (key, value) pairs from both the objects. + * When further merge isn't allowed (by currentDepth), the 2nd item is preferred, + * replacing the 1st item. + * + * Merging arrays: + * Two arrays are merged by concatenating them. + * When merge isn't allowed (by currentDepth), the 2nd array is returned. + * + * Merging primitive types (number, string, etc.): + * Primitive types are not merged in the sense of concatenating. In case they are the same, + * either of them can be considered as the "merged value". 2nd item is returned here. + * When merge isn't allowed (by currentDepth), the 2nd item is returned. + * + * @param a first item + * @param b second item + * @param currentDepth the current merge depth; decreases in a recursive call + * @returns merged item or a preferred item, chosen according to currentDepth + */ +const mergeContents = (a: any, b: any, currentDepth: number): any => { + const type_a = getType(a); + const type_b = getType(b); + + if (currentDepth <= 0 || type_a === "nullish" || type_b === "nullish") { + // Tend to return the 2nd item, which is supposed to be the updated one + // If both are nullish, 2nd item is returned + return b ?? a ?? b; + } + + if (type_a === "primitive" && type_b === "primitive") { + if (a === b) { + return b; + } else { + throw new Error(`Tx metadata merge error: cannot merge ${a} with ${b}`); + } + } + + else if (type_a === "array" && type_b === "array") { + return [...a, ...b]; + } + + else if (type_a === "object" && type_b === "object") { + for (const key in b) { + Object.assign(a, { [key]: mergeContents(a[key], b[key], currentDepth - 1) }); + } + return a; + } + + throw new Error(`Tx metadata merge error: cannot merge ${type_a} type with ${type_b} type`); +} + +const mergeChosenMetadata = (metadataArr: string[], tag: string, mergeDepth: number): Metadata[] => { + if (!metadataArr.length) + return []; + + // Assuming all the elements are JSON stringified, parse them first + for (let i = 0; i < metadataArr.length; i++) { + try { + metadataArr[i] = JSONBig.parse(metadataArr[i]!); + } catch (e) { + throw new Error(`Tx metadata merge error: cannot parse metadata value: ${e}`); + } + } + + let mergedSoFar = metadataArr[0]; + + for (let i = 1; i < metadataArr.length; i++) { + mergedSoFar = mergeContents(mergedSoFar, metadataArr[i], mergeDepth); + } + + return [{ tag, metadata: JSONBig.stringify(mergedSoFar) }]; +}; + +/** + * Merges multiple metadata entries in an array of metadata that belong to the same tag. + * Can merge objects upto a defined depth. + * + * @param metadataList metadata array of meshTxBuilderBody + * @param tag tag value to group all the metadata belonging to that same tag + * @param mergeOption the effective depth till which the merge will happen, + * beyond this depth, the newer element would replace the older one. + * If false or 0, the latest metadata entry with the specified tag is preserved + * and the earlier ones are discarded + * @returns metadata array for meshTxBuilderBody; with all the metadata entries not belonging + * to the specified tag are preserved + */ +export const mergeAllMetadataByTag = (metadataList: Metadata[], tag: string, mergeOption: MetadataMergeLevel): Metadata[] => { + const mergeDepth = getMergeDepth(mergeOption); + + const chosenElementsMetadata = []; + const restElements = []; + + for (const metadata of metadataList) { + if (metadata.tag == tag) { // Number can also match here + chosenElementsMetadata.push(metadata.metadata); + } else { + restElements.push(metadata); + } + } + + const mergedItem = mergeChosenMetadata(chosenElementsMetadata, tag, mergeDepth); + + return [...restElements, ...mergedItem]; +} diff --git a/packages/mesh-transaction/test/transaction/txMetadata.test.ts b/packages/mesh-transaction/test/transaction/txMetadata.test.ts new file mode 100644 index 000000000..a0cbd8765 --- /dev/null +++ b/packages/mesh-transaction/test/transaction/txMetadata.test.ts @@ -0,0 +1,417 @@ +import JSONBig from "json-bigint"; +import { Metadata } from "@meshsdk/common"; +import { mergeAllMetadataByTag } from "@meshsdk/transaction"; + +describe("Transaction Metadata Merge", () => { + + const createMetadataArray = (arr: { tag: string, metadata: any }[]): Metadata[] => { + for (const elem of arr) { + elem.metadata = JSONBig.stringify(elem.metadata); + } + return arr; + }; + + it("should merge two identical number metadata entries", () => { + const input = createMetadataArray([ + { tag: "0", metadata: 42 }, + { tag: "0", metadata: 42 }, + ]); + const expectedOutput = createMetadataArray([ + { tag: "0", metadata: 42 }, + ]); + const output = mergeAllMetadataByTag(input, "0", true); + expect(output).toEqual(expectedOutput); + }); + it("should merge two identical string metadata entries", () => { + const input = createMetadataArray([ + { tag: "1", metadata: "42" }, + { tag: "1", metadata: "42" }, + ]); + const expectedOutput = createMetadataArray([ + { tag: "1", metadata: "42" }, + ]); + const output = mergeAllMetadataByTag(input, "1", true); + expect(output).toEqual(expectedOutput); + }); + it("should not merge two different numbers", () => { + const input = createMetadataArray([ + { tag: "1", metadata: 42 }, + { tag: "1", metadata: 43 }, + ]); + expect(() => mergeAllMetadataByTag(input, "1", true)).toThrow("cannot merge 42 with 43"); + }); + it("should not merge two different strings", () => { + const input = createMetadataArray([ + { tag: "0", metadata: "Alice" }, + { tag: "0", metadata: "Bob" }, + ]); + expect(() => mergeAllMetadataByTag(input, "0", true)).toThrow("cannot merge Alice with Bob"); + }); + it("should not merge two same values of different types", () => { + const input = createMetadataArray([ + { tag: "0", metadata: 42 }, + { tag: "0", metadata: "42" }, + ]); + expect(() => mergeAllMetadataByTag(input, "0", true)).toThrow("cannot merge 42 with 42"); + }); + it("should replace with the latest item if there is no merge", () => { + const input = createMetadataArray([ + { tag: "1", metadata: 42 }, + { tag: "1", metadata: 43 }, + ]); + const expectedOutput = createMetadataArray([ + { tag: "1", metadata: 43 }, + ]); + expect(mergeAllMetadataByTag(input, "1", false)).toEqual(expectedOutput); + }); + + it("should not merge two different values of the same object key", () => { + const input = createMetadataArray([ + { tag: "721", metadata: { version: 1 } }, + { tag: "721", metadata: { version: 2 } }, + ]); + expect(() => mergeAllMetadataByTag(input, "721", 2)).toThrow("cannot merge 1 with 2"); + }); + it("should replace with the latest value of the same object key if values are not merged", () => { + const input = createMetadataArray([ + { tag: "721", metadata: { version: 1 } }, + { tag: "721", metadata: { version: 2 } }, + ]); + const expectedOutput = createMetadataArray([ + { tag: "721", metadata: { version: 2 } }, + ]); + expect(mergeAllMetadataByTag(input, "721", 1)).toEqual(expectedOutput); + expect(mergeAllMetadataByTag(input, "721", true)).toEqual(expectedOutput); + }); + it("should not merge different types", () => { + expect(() => mergeAllMetadataByTag( + createMetadataArray([ + { tag: "0", metadata: 0 }, + { tag: "0", metadata: [] } + ]), + "0", + true + )).toThrow("cannot merge primitive type with array type"); + expect(() => mergeAllMetadataByTag( + createMetadataArray([ + { tag: "0", metadata: {} }, + { tag: "0", metadata: "" } + ]), + "0", + true + )).toThrow("cannot merge object type with primitive type"); + expect(() => mergeAllMetadataByTag( + createMetadataArray([ + { tag: "0", metadata: {} }, + { tag: "0", metadata: [] } + ]), + "0", + true + )).toThrow("cannot merge object type with array type"); + }); + it("should preserve object key and value if merged with a nullish value", () => { + expect(mergeAllMetadataByTag( + createMetadataArray([ + { tag: "0", metadata: { value: 1 } }, + { tag: "0", metadata: { value: null } } + ]), + "0", + 2 + )).toEqual(createMetadataArray([ + { tag: "0", metadata: { value: 1 } } + ])); + expect(mergeAllMetadataByTag( + createMetadataArray([ + { tag: "0", metadata: { value: 1 } }, + { tag: "0", metadata: { value: undefined } } + ]), + "0", + true + )).toEqual(createMetadataArray([ + { tag: "0", metadata: { value: 1 } } + ])); + }); + + it("should replace 674 standard msg array for default merge depth", () => { + const input = createMetadataArray([ + { tag: "674", metadata: { msg: ["A", "B", "C"] } }, + { tag: "674", metadata: { msg: ["D", "E", "F"] } }, + { tag: "674", metadata: { msg2: ["X", "Y", "Z"] } }, + ]); + + const expectedOutput = createMetadataArray([ + { + tag: "674", metadata: { + msg: ["D", "E", "F"], + msg2: ["X", "Y", "Z"] + } + } + ]); + + expect(mergeAllMetadataByTag(input, "674", 1)).toEqual(expectedOutput); + expect(mergeAllMetadataByTag(input, "674", true)).toEqual(expectedOutput); + }); + + it("should concatenate 674 standard msg arrays for merge depth 2", () => { + const input = createMetadataArray([ + { tag: "674", metadata: { msg: ["A", "B", "C"] } }, + { tag: "674", metadata: { msg: ["D", "E", "F"] } }, + { tag: "674", metadata: { msg2: ["X", "Y", "Z"] } }, + ]); + + const expectedOutput = createMetadataArray([ + { + tag: "674", metadata: { + msg: ["A", "B", "C", "D", "E", "F"], + msg2: ["X", "Y", "Z"] + } + } + ]); + + expect(mergeAllMetadataByTag(input, "674", 2)).toEqual(expectedOutput); + }); + + it("should merge multiple CIP-25 NFTs metadata under the same policy id", () => { + const input: Metadata[] = createMetadataArray([ + { + tag: "721", + metadata: { + "policyId1": { + "My NFT 1": { + "name": "My NFT 1" + } + } + } + }, + { + tag: "721", + metadata: { + "policyId1": { + "My NFT 2": { + "name": "My NFT 2", + "description": "My second NFT" + } + } + } + } + ]); + + const expectedOutput: Metadata[] = createMetadataArray([ + { + tag: "721", + metadata: { + "policyId1": { + "My NFT 1": { + "name": "My NFT 1" + }, + "My NFT 2": { + "name": "My NFT 2", + "description": "My second NFT" + } + } + } + } + ]); + + expect(mergeAllMetadataByTag(input, "721", 2)).toEqual(expectedOutput); + }); + + it("should merge multiple CIP-25 NFTs metadata under different policy ids", () => { + const input = createMetadataArray([ + { + tag: "721", + metadata: { + "policyId1": { + "My NFT 1": { + "name": "My NFT 1", + "files": [ + { name: "NFT 1 Image", src: "xyz", mediaType: "image/jpeg" } + ] + } + } + } + }, + { + tag: "721", + metadata: { + "policyId2": { + "My NFT 1": { + "name": "My NFT 1 Policy 2", + "files": [ + { name: "NFT 1 P 2", src: "abc", mediaType: "image/png" } + ] + } + } + } + }, + { + tag: "721", + metadata: { + "policyId1": { + "My NFT 2": { + "name": "My NFT 2", + "files": [ + { name: "NFT 2 Image", src: "pqr", mediaType: "image/jpeg" } + ] + } + } + } + } + ]); + + const expectedOutput = createMetadataArray([ + { + tag: "721", + metadata: { + "policyId1": { + "My NFT 1": { + "name": "My NFT 1", + "files": [ + { name: "NFT 1 Image", src: "xyz", mediaType: "image/jpeg" } + ] + }, + "My NFT 2": { + "name": "My NFT 2", + "files": [ + { name: "NFT 2 Image", src: "pqr", mediaType: "image/jpeg" } + ] + } + }, + "policyId2": { + "My NFT 1": { + "name": "My NFT 1 Policy 2", + "files": [ + { name: "NFT 1 P 2", src: "abc", mediaType: "image/png" } + ] + } + } + } + } + ]); + + expect(mergeAllMetadataByTag(input, "721", 2)).toEqual(expectedOutput); + }); + + it("should replace with the latest CIP-25 NFT metadata of the same policy and asset id", () => { + const input = createMetadataArray([ + { + tag: "721", + metadata: { + "policyId1": { + "My NFT 1": { name: "NFT 1 Name", files: [{ name: "NFT Image" }] } // old metadata here + } + } + }, + { + tag: "721", + metadata: { + "policyId1": { + "My NFT 2": { name: "NFT 2 Name" } + } + } + }, + { + tag: "721", + metadata: { + "policyId1": { + "My NFT 1": { name: "Latest NFT 1", image: "xyz", description: "Latest NFT here" } + } + } + } + ]); + + const expectedOutput = createMetadataArray([ + { + tag: "721", + metadata: { + "policyId1": { + "My NFT 1": { name: "Latest NFT 1", image: "xyz", description: "Latest NFT here" }, + "My NFT 2": { name: "NFT 2 Name" } + } + } + } + ]); + + expect(mergeAllMetadataByTag(input, "721", 2)).toEqual(expectedOutput); + }); + + it("should attach version to CIP-25 metadata", () => { + const input = createMetadataArray([ + { + tag: "721", + metadata: { + "policyId1": { "My NFT 1": { name: "My NFT 1" } } + } + }, + { + tag: "721", + metadata: { + "policyId1": { "My NFT 2": { name: "My NFT 2" } } + } + }, + { + tag: "721", + metadata: { + version: "1.0" + } + }, + { + tag: "721", + metadata: { + "policyId2": { "My NFT 1": { name: "My NFT 1 Policy 2" } } + } + } + ]); + + const expectedOutput = createMetadataArray([ + { + tag: "721", + metadata: { + "policyId1": { + "My NFT 1": { name: "My NFT 1" }, + "My NFT 2": { name: "My NFT 2" } + }, + "version": "1.0", // version inserted in an ordered manner + "policyId2": { + "My NFT 1": { name: "My NFT 1 Policy 2" } + } + } + } + ]); + + expect(mergeAllMetadataByTag(input, "721", 2)).toEqual(expectedOutput); + }); + + it("should preserve metadata entries with other tags in the original order", () => { + const input = createMetadataArray([ + { tag: "0", metadata: "line 1" }, + { tag: "0", metadata: "line 2" }, + { tag: "674", metadata: { msg: ["line 3"] } }, + { tag: "721", metadata: { policyId: { NFT: { name: "NFT" } } } }, + { tag: "674", metadata: { msg: ["line 5"] } }, + { tag: "721", metadata: { policyId: { NFT2: { name: "NFT 2" } } } }, + { tag: "1", metadata: "line 7" }, + ]); + + const expectedOutput1 = createMetadataArray([ + { tag: "0", metadata: "line 1" }, + { tag: "0", metadata: "line 2" }, + { tag: "674", metadata: { msg: ["line 3"] } }, + { tag: "674", metadata: { msg: ["line 5"] } }, + { tag: "1", metadata: "line 7" }, + { tag: "721", metadata: { policyId: { NFT: { name: "NFT" }, NFT2: { name: "NFT 2" } } } }, + ]); + + expect(mergeAllMetadataByTag(input, "721", 2)).toEqual(expectedOutput1); + + const expectedOutput2 = createMetadataArray([ + { tag: "0", metadata: "line 1" }, + { tag: "0", metadata: "line 2" }, + { tag: "1", metadata: "line 7" }, + { tag: "721", metadata: { policyId: { NFT: { name: "NFT" }, NFT2: { name: "NFT 2" } } } }, + { tag: "674", metadata: { msg: ["line 5"] } }, + ]); + + expect(mergeAllMetadataByTag(expectedOutput1, "674", true)).toEqual(expectedOutput2); + }); +}); From dc77004e92f7f60113b931a3c1df20b1078f0095 Mon Sep 17 00:00:00 2001 From: Dharmveer Bharti Date: Fri, 6 Dec 2024 01:02:41 +0530 Subject: [PATCH 2/6] tx metadata as Map --- .../src/pages/api/donate-mint-mesh.ts | 2 +- .../pages/apis/transaction/basics/cip20.tsx | 2 +- .../src/pages/apis/txbuilder/basics/cip20.tsx | 12 +- .../pages/apis/txbuilder/basics/multisig.tsx | 6 +- .../apis/txbuilder/basics/set-metadata.tsx | 10 +- .../minting/minting-native-script.tsx | 4 +- .../minting/minting-one-signature.tsx | 4 +- .../minting/minting-plutus-script.tsx | 6 +- .../minting/minting-royalty-token.tsx | 4 +- .../txbuilder/minting/multiple-assets.tsx | 6 +- .../src/types/transaction-builder/index.ts | 11 +- .../content-ownership/offchain/offchain.ts | 2 +- .../mesh-contract/src/plutus-nft/offchain.ts | 2 +- .../mesh-core-csl/src/core/adaptor/index.ts | 3 +- .../src/core/adaptor/metadata.ts | 36 ++ .../mesh-core-csl/test/core/builder.test.ts | 2 +- .../src/mesh-tx-builder/tx-builder-core.ts | 18 +- .../mesh-transaction/src/transaction/index.ts | 12 +- .../src/transaction/transaction-v2.ts | 8 +- .../mesh-transaction/src/utils/metadata.ts | 193 ++++--- .../test/transaction/txMetadata.test.ts | 515 ++++++++---------- 21 files changed, 441 insertions(+), 417 deletions(-) create mode 100644 packages/mesh-core-csl/src/core/adaptor/metadata.ts diff --git a/apps/playground/src/pages/api/donate-mint-mesh.ts b/apps/playground/src/pages/api/donate-mint-mesh.ts index 2c3409340..76f2d4800 100644 --- a/apps/playground/src/pages/api/donate-mint-mesh.ts +++ b/apps/playground/src/pages/api/donate-mint-mesh.ts @@ -94,7 +94,7 @@ export default async function handler( .selectUtxosFrom(utxos) .mint("1", policyId, stringToHex(assetName)) .mintingScript(forgingScript) - .metadataValue("721", fullAssetMetadata) + .metadataValue(721, fullAssetMetadata) .txOut(donateAddress, [ { unit: "lovelace", quantity: costLovelace.toString() }, ]) diff --git a/apps/playground/src/pages/apis/transaction/basics/cip20.tsx b/apps/playground/src/pages/apis/transaction/basics/cip20.tsx index 2fc46c5e2..11ec8f601 100644 --- a/apps/playground/src/pages/apis/transaction/basics/cip20.tsx +++ b/apps/playground/src/pages/apis/transaction/basics/cip20.tsx @@ -34,7 +34,7 @@ function Left() { The specification for the individual strings follow the general design specification for JSON metadata, which is already implemented and in operation on the cardano blockchain. The used metadatum label is{" "} - 674:, this number was choosen because it is the T9 encoding + 674:, this number was chosen because it is the T9 encoding of the string msg. The message content has the key msg: and consists of an array of individual message-strings. The number of theses diff --git a/apps/playground/src/pages/apis/txbuilder/basics/cip20.tsx b/apps/playground/src/pages/apis/txbuilder/basics/cip20.tsx index 3d20ced5a..8d0903519 100644 --- a/apps/playground/src/pages/apis/txbuilder/basics/cip20.tsx +++ b/apps/playground/src/pages/apis/txbuilder/basics/cip20.tsx @@ -22,7 +22,7 @@ export default function TxbuilderCip20() { function Left() { let code = `txBuilder\n`; - code += ` .metadataValue(tag, metadata)\n`; + code += ` .metadataValue(label, metadata)\n`; return ( <> @@ -36,7 +36,7 @@ function Left() { The specification for the individual strings follow the general design specification for JSON metadata, which is already implemented and in operation on the cardano blockchain. The used metadatum label is{" "} - 674: this number was choosen because it is the T9 encoding + 674: this number was chosen because it is the T9 encoding of the string msg. The message content has the key msg: and consists of an array of individual message-strings. The number of theses @@ -60,14 +60,14 @@ function Right() { const changeAddress = await wallet.getChangeAddress(); const txBuilder = getTxBuilder(); - const tag = "674"; + const label = 674; const metadata = { msg: message.split("\n"), }; const unsignedTx = await txBuilder .changeAddress(changeAddress) - .metadataValue(tag.toString(), metadata) + .metadataValue(label, metadata) .selectUtxosFrom(utxos) .complete(); @@ -82,7 +82,7 @@ function Right() { codeSnippet += `const changeAddress = await wallet.getChangeAddress();\n`; codeSnippet += `const txBuilder = getTxBuilder();\n`; codeSnippet += `\n`; - codeSnippet += `const tag = "674";\n`; + codeSnippet += `const label = 674;\n`; codeSnippet += `const metadata = {\n`; codeSnippet += ` msg: [\n`; for (let line of message.split("\n")) { @@ -92,7 +92,7 @@ function Right() { codeSnippet += `});\n\n`; codeSnippet += `const unsignedTx = await txBuilder\n`; codeSnippet += ` .changeAddress(changeAddress)\n`; - codeSnippet += ` .metadataValue(tag, metadata)\n`; + codeSnippet += ` .metadataValue(label, metadata)\n`; codeSnippet += ` .selectUtxosFrom(utxos)\n`; codeSnippet += ` .complete();\n`; codeSnippet += `\n`; diff --git a/apps/playground/src/pages/apis/txbuilder/basics/multisig.tsx b/apps/playground/src/pages/apis/txbuilder/basics/multisig.tsx index f8297619a..9e209ef01 100644 --- a/apps/playground/src/pages/apis/txbuilder/basics/multisig.tsx +++ b/apps/playground/src/pages/apis/txbuilder/basics/multisig.tsx @@ -29,7 +29,7 @@ function Left() { codeTx += `const unsignedTx = await txBuilder\n`; codeTx += ` .mint("1", policyId, stringToHex("MeshToken"))\n`; codeTx += ` .mintingScript(forgingScript)\n`; - codeTx += ` .metadataValue("721", { [policyId]: { [assetName]: demoAssetMetadata } })\n`; + codeTx += ` .metadataValue(721, { [policyId]: { [assetName]: demoAssetMetadata } })\n`; codeTx += ` .changeAddress(address)\n`; codeTx += ` .selectUtxosFrom(utxos)\n`; codeTx += ` .complete();\n`; @@ -88,7 +88,7 @@ function Right() { const unsignedTx = await txBuilder .mint("1", policyId, stringToHex("MeshToken")) .mintingScript(forgingScript) - .metadataValue("721", { [policyId]: { [assetName]: demoAssetMetadata } }) + .metadataValue(721, { [policyId]: { [assetName]: demoAssetMetadata } }) .changeAddress(address) .selectUtxosFrom(utxos) .complete(); @@ -124,7 +124,7 @@ function Right() { codeSnippet += `const unsignedTx = await txBuilder\n`; codeSnippet += ` .mint("1", policyId, stringToHex("MeshToken"))\n`; codeSnippet += ` .mintingScript(forgingScript)\n`; - codeSnippet += ` .metadataValue("721", { [policyId]: { [assetName]: demoAssetMetadata } })\n`; + codeSnippet += ` .metadataValue(721, { [policyId]: { [assetName]: demoAssetMetadata } })\n`; codeSnippet += ` .changeAddress(address)\n`; codeSnippet += ` .selectUtxosFrom(utxos)\n`; codeSnippet += ` .complete();\n`; diff --git a/apps/playground/src/pages/apis/txbuilder/basics/set-metadata.tsx b/apps/playground/src/pages/apis/txbuilder/basics/set-metadata.tsx index ae9f7bda8..9dda03c70 100644 --- a/apps/playground/src/pages/apis/txbuilder/basics/set-metadata.tsx +++ b/apps/playground/src/pages/apis/txbuilder/basics/set-metadata.tsx @@ -22,7 +22,7 @@ export default function TxbuilderSetMetadata() { function Left() { let code = `txBuilder\n`; - code += ` .metadataValue(tag, metadata)\n`; + code += ` .metadataValue(label, metadata)\n`; return ( <> @@ -47,12 +47,12 @@ function Right() { const changeAddress = await wallet.getChangeAddress(); const txBuilder = getTxBuilder(); - const tag = "0"; + const label = 0; const metadata = "This is a message from the Mesh SDK"; const unsignedTx = await txBuilder .changeAddress(changeAddress) - .metadataValue(tag.toString(), metadata) + .metadataValue(label, metadata) .selectUtxosFrom(utxos) .complete(); @@ -66,11 +66,11 @@ function Right() { codeSnippet += `const address = await wallet.getChangeAddress();\n`; codeSnippet += `const txBuilder = getTxBuilder();\n`; codeSnippet += `\n`; - codeSnippet += `const tag = "0";\n`; + codeSnippet += `const label = 0;\n`; codeSnippet += `const metadata = "This is a message from the Mesh SDK";\n\n`; codeSnippet += `const unsignedTx = await txBuilder\n`; codeSnippet += ` .changeAddress(address)\n`; - codeSnippet += ` .metadataValue(tag, metadata)\n`; + codeSnippet += ` .metadataValue(label, metadata)\n`; codeSnippet += ` .selectUtxosFrom(utxos)\n`; codeSnippet += ` .complete();\n`; codeSnippet += `\n`; diff --git a/apps/playground/src/pages/apis/txbuilder/minting/minting-native-script.tsx b/apps/playground/src/pages/apis/txbuilder/minting/minting-native-script.tsx index f8526b1a6..a369af6c7 100644 --- a/apps/playground/src/pages/apis/txbuilder/minting/minting-native-script.tsx +++ b/apps/playground/src/pages/apis/txbuilder/minting/minting-native-script.tsx @@ -84,7 +84,7 @@ function Right() { const unsignedTx = await txBuilder .mint("1", policyId, tokenNameHex) .mintingScript(forgingScript) - .metadataValue("721", metadata) + .metadataValue(721, metadata) .changeAddress(changeAddress) .invalidHereafter(99999999) .selectUtxosFrom(utxos) @@ -125,7 +125,7 @@ const txBuilder = getTxBuilder(); const unsignedTx = await txBuilder .mint("1", policyId, tokenNameHex) .mintingScript(forgingScript) - .metadataValue("721", metadata) + .metadataValue(721, metadata) .changeAddress(changeAddress) .invalidHereafter(99999999) .selectUtxosFrom(utxos) diff --git a/apps/playground/src/pages/apis/txbuilder/minting/minting-one-signature.tsx b/apps/playground/src/pages/apis/txbuilder/minting/minting-one-signature.tsx index 956184cd8..4c2bbfe24 100644 --- a/apps/playground/src/pages/apis/txbuilder/minting/minting-one-signature.tsx +++ b/apps/playground/src/pages/apis/txbuilder/minting/minting-one-signature.tsx @@ -91,7 +91,7 @@ function Right() { const unsignedTx = await txBuilder .mint("1", policyId, tokenNameHex) .mintingScript(forgingScript) - .metadataValue("721", metadata) + .metadataValue(721, metadata) .changeAddress(changeAddress) .selectUtxosFrom(utxos) .complete(); @@ -124,7 +124,7 @@ function Right() { codeSnippet += `const unsignedTx = await txBuilder\n`; codeSnippet += ` .mint("1", policyId, tokenNameHex)\n`; codeSnippet += ` .mintingScript(forgingScript)\n`; - codeSnippet += ` .metadataValue("721", metadata)\n`; + codeSnippet += ` .metadataValue(721, metadata)\n`; codeSnippet += ` .changeAddress(changeAddress)\n`; codeSnippet += ` .selectUtxosFrom(utxos)\n`; codeSnippet += ` .complete();\n`; diff --git a/apps/playground/src/pages/apis/txbuilder/minting/minting-plutus-script.tsx b/apps/playground/src/pages/apis/txbuilder/minting/minting-plutus-script.tsx index b2acc3287..7f18f0441 100644 --- a/apps/playground/src/pages/apis/txbuilder/minting/minting-plutus-script.tsx +++ b/apps/playground/src/pages/apis/txbuilder/minting/minting-plutus-script.tsx @@ -42,7 +42,7 @@ function Left() { codeSnippet3 += ` .mint("1", policyId, tokenNameHex)\n`; codeSnippet3 += ` .mintingScript(demoPlutusMintingScript)\n`; codeSnippet3 += ` .mintRedeemerValue(mConStr0([userInput]))\n`; - codeSnippet3 += ` .metadataValue("721", metadata)\n`; + codeSnippet3 += ` .metadataValue(721, metadata)\n`; codeSnippet3 += ` .changeAddress(changeAddress)\n`; codeSnippet3 += ` .selectUtxosFrom(utxos)\n`; codeSnippet3 += ` .txInCollateral(\n`; @@ -125,7 +125,7 @@ function Right() { .mint("1", policyId, tokenNameHex) .mintingScript(demoPlutusMintingScript) .mintRedeemerValue(mConStr0([userInput])) - .metadataValue("721", metadata) + .metadataValue(721, metadata) .changeAddress(changeAddress) .selectUtxosFrom(utxos) .txInCollateral( @@ -159,7 +159,7 @@ function Right() { code += ` .mint("1", policyId, tokenNameHex)\n`; code += ` .mintingScript(demoPlutusMintingScript)\n`; code += ` .mintRedeemerValue(mConStr0(['${userInput}']))\n`; - code += ` .metadataValue("721", metadata)\n`; + code += ` .metadataValue(721, metadata)\n`; code += ` .changeAddress(changeAddress)\n`; code += ` .selectUtxosFrom(utxos)\n`; code += ` .txInCollateral(\n`; diff --git a/apps/playground/src/pages/apis/txbuilder/minting/minting-royalty-token.tsx b/apps/playground/src/pages/apis/txbuilder/minting/minting-royalty-token.tsx index c8d591a4c..d768e46d1 100644 --- a/apps/playground/src/pages/apis/txbuilder/minting/minting-royalty-token.tsx +++ b/apps/playground/src/pages/apis/txbuilder/minting/minting-royalty-token.tsx @@ -81,7 +81,7 @@ function Right() { const unsignedTx = await txBuilder .mint("1", policyId, "") .mintingScript(forgingScript) - .metadataValue("777", assetMetadata) + .metadataValue(777, assetMetadata) .changeAddress(address) .selectUtxosFrom(utxos) .complete(); @@ -114,7 +114,7 @@ function Right() { code += `const unsignedTx = await txBuilder\n`; code += ` .mint("1", policyId, "")\n`; code += ` .mintingScript(forgingScript)\n`; - code += ` .metadataValue("777", assetMetadata)\n`; + code += ` .metadataValue(777, assetMetadata)\n`; code += ` .changeAddress(address)\n`; code += ` .selectUtxosFrom(utxos)\n`; code += ` .complete();\n`; diff --git a/apps/playground/src/pages/apis/txbuilder/minting/multiple-assets.tsx b/apps/playground/src/pages/apis/txbuilder/minting/multiple-assets.tsx index 6a872b5a1..75835d2fd 100644 --- a/apps/playground/src/pages/apis/txbuilder/minting/multiple-assets.tsx +++ b/apps/playground/src/pages/apis/txbuilder/minting/multiple-assets.tsx @@ -35,7 +35,7 @@ function Left() { let codeSnippet2 = ``; codeSnippet2 += `txBuilder\n`; - codeSnippet2 += ` .metadataValue("721", metadata)\n`; + codeSnippet2 += ` .metadataValue(721, metadata)\n`; codeSnippet2 += ` .changeAddress(changeAddress)\n`; codeSnippet2 += ` .selectUtxosFrom(utxos);\n`; codeSnippet2 += `\n`; @@ -88,7 +88,7 @@ function Right() { } txBuilder - .metadataValue("721", metadata) + .metadataValue(721, metadata) .changeAddress(changeAddress) .selectUtxosFrom(utxos); @@ -125,7 +125,7 @@ function Right() { codeSnippet += `}\n`; codeSnippet += `\n`; codeSnippet += `txBuilder\n`; - codeSnippet += ` .metadataValue("721", metadata)\n`; + codeSnippet += ` .metadataValue(721, metadata)\n`; codeSnippet += ` .changeAddress(changeAddress)\n`; codeSnippet += ` .selectUtxosFrom(utxos);\n`; codeSnippet += `\n`; diff --git a/packages/mesh-common/src/types/transaction-builder/index.ts b/packages/mesh-common/src/types/transaction-builder/index.ts index f3a8e37a6..495e43fb5 100644 --- a/packages/mesh-common/src/types/transaction-builder/index.ts +++ b/packages/mesh-common/src/types/transaction-builder/index.ts @@ -25,7 +25,7 @@ export type MeshTxBuilderBody = { referenceInputs: RefTxIn[]; mints: MintItem[]; changeAddress: string; - metadata: Metadata[]; + metadata: TxMetadata; validityRange: ValidityRange; certificates: Certificate[]; withdrawals: Withdrawal[]; @@ -50,7 +50,7 @@ export const emptyTxBuilderBody = (): MeshTxBuilderBody => ({ referenceInputs: [], mints: [], changeAddress: "", - metadata: [], + metadata: new Map(), validityRange: {}, certificates: [], withdrawals: [], @@ -73,6 +73,13 @@ export type ValidityRange = { // Mint Types +// Transaction Metadata + +export type MetadatumMap = Map; +export type Metadatum = bigint | number | string | Uint8Array | MetadatumMap | Metadatum[]; +export type TxMetadata = Map; + +// to be used for serialization export type Metadata = { tag: string; metadata: string; diff --git a/packages/mesh-contract/src/content-ownership/offchain/offchain.ts b/packages/mesh-contract/src/content-ownership/offchain/offchain.ts index ec14ac921..e821502c4 100644 --- a/packages/mesh-contract/src/content-ownership/offchain/offchain.ts +++ b/packages/mesh-contract/src/content-ownership/offchain/offchain.ts @@ -525,7 +525,7 @@ export class MeshContentOwnershipContract extends MeshTxInitiator { const txHex = await this.mesh .mint("1", policyId, tokenNameHex) .mintingScript(forgingScript) - .metadataValue("721", metadata) + .metadataValue(721, metadata, 2) .changeAddress(walletAddress) .selectUtxosFrom(utxos) .complete(); diff --git a/packages/mesh-contract/src/plutus-nft/offchain.ts b/packages/mesh-contract/src/plutus-nft/offchain.ts index e12bb7b02..6aa6c007e 100644 --- a/packages/mesh-contract/src/plutus-nft/offchain.ts +++ b/packages/mesh-contract/src/plutus-nft/offchain.ts @@ -200,7 +200,7 @@ export class MeshPlutusNFTContract extends MeshTxInitiator { if (assetMetadata) { const metadata = { [policyId]: { [tokenName]: { ...assetMetadata } } }; - tx.metadataValue("721", metadata); + tx.metadataValue(721, metadata, 2); } tx.mintRedeemerValue(mConStr0([])) diff --git a/packages/mesh-core-csl/src/core/adaptor/index.ts b/packages/mesh-core-csl/src/core/adaptor/index.ts index 1169f94a3..9fec4a2f3 100644 --- a/packages/mesh-core-csl/src/core/adaptor/index.ts +++ b/packages/mesh-core-csl/src/core/adaptor/index.ts @@ -4,6 +4,7 @@ import { certificateToObj } from "./certificate"; import { mintItemToObj } from "./mint"; import { networkToObj } from "./network"; import { outputToObj } from "./output"; +import { txMetadataToObj } from "./metadata"; import { collateralTxInToObj, txInToObj } from "./txIn"; import { voteToObj } from "./vote"; import { withdrawalToObj } from "./withdrawal"; @@ -33,7 +34,7 @@ export const meshTxBuilderBodyToObj = ({ referenceInputs: referenceInputs, mints: mints.map((mint) => mintItemToObj(mint)), changeAddress, - metadata: metadata, + metadata: txMetadataToObj(metadata), validityRange: validityRangeToObj(validityRange), certificates: certificates.map(certificateToObj), signingKey: signingKey, diff --git a/packages/mesh-core-csl/src/core/adaptor/metadata.ts b/packages/mesh-core-csl/src/core/adaptor/metadata.ts new file mode 100644 index 000000000..1f7ecd4b3 --- /dev/null +++ b/packages/mesh-core-csl/src/core/adaptor/metadata.ts @@ -0,0 +1,36 @@ +import JSONbig from "json-bigint"; +import type { Metadata, Metadatum, TxMetadata } from "@meshsdk/common"; + +export const txMetadataToObj = (metadata: TxMetadata): Metadata[] => { + const result: Metadata[] = []; + metadata.forEach((value: Metadatum, key: bigint) => { + result.push({ tag: key.toString(), metadata: JSONbig.stringify(metadatumToObj(value)) }); + }); + return result; +}; + +const metadatumToObj = (metadatum: Metadatum): any => { + if (typeof metadatum === "number" || typeof metadatum === "string") { + return metadatum; + } else if (typeof metadatum === "bigint") { + return metadatum.toString(); + } else if (metadatum instanceof Uint8Array) { + return uint8ArrayToHex(metadatum); + } else if (metadatum instanceof Map) { + const result: Record = {}; + metadatum.forEach((value, key) => { + result[metadatumToObj(key)] = metadatumToObj(value); + }); + return result; + } else if (Array.isArray(metadatum)) { + return metadatum.map(metadatumToObj); + } else { + throw new Error("metadatumToObj: Unsupported Metadatum type"); + } +}; + +const uint8ArrayToHex = (bytes: Uint8Array): string => { + return Array.from(bytes) + .map(byte => byte.toString(16).padStart(2, "0")) + .join(""); +}; diff --git a/packages/mesh-core-csl/test/core/builder.test.ts b/packages/mesh-core-csl/test/core/builder.test.ts index bc690b184..64cb0c499 100644 --- a/packages/mesh-core-csl/test/core/builder.test.ts +++ b/packages/mesh-core-csl/test/core/builder.test.ts @@ -85,7 +85,7 @@ describe("Builder", () => { mints: [], changeAddress: "addr_test1qq0yavv5uve45rwvfaw96qynrqt8ckpmkwcg08vlwxxdncxk82f5wz75mzaesmqzl79xqsmedwgucwtuav5str6untqqmykcpn", - metadata: [], + metadata: new Map(), validityRange: {}, certificates: [], withdrawals: [], diff --git a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts index 8c02608e8..23a51c10d 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts @@ -35,7 +35,7 @@ import { Withdrawal, } from "@meshsdk/common"; -import { MetadataMergeLevel, mergeAllMetadataByTag } from "../utils"; +import { MetadataMergeLevel, metadataObjToMap, setAndMergeTxMetadata } from "../utils"; export class MeshTxBuilderCore { txEvaluationMultiplier = 1.1; @@ -1414,18 +1414,16 @@ export class MeshTxBuilderCore { /** * Add metadata to the transaction - * @param tag The tag of the metadata + * @param label The label of the metadata, preferably number * @param metadata The metadata in any format - * @param mergeExistingMetadataByTag Whether to merge the new metadata - * under a tag with the existing metadata with the same tag + * @param mergeExistingMetadataByLabel Whether to merge the new metadata + * with any existing metadata under the same label, and upto what level * @returns The MeshTxBuilder instance */ - metadataValue = (tag: string, metadata: any, mergeExistingMetadataByTag: MetadataMergeLevel = false) => { - const metadataString = JSONBig.stringify(metadata); - this.meshTxBuilderBody.metadata.push({ tag, metadata: metadataString }); - if (mergeExistingMetadataByTag) { - this.meshTxBuilderBody.metadata = mergeAllMetadataByTag(this.meshTxBuilderBody.metadata, tag, mergeExistingMetadataByTag); - } + metadataValue = (label: number | bigint | string, metadata: any, mergeExistingMetadataByLabel: MetadataMergeLevel = false) => { + label = BigInt(label); + metadata = metadataObjToMap(metadata); + setAndMergeTxMetadata(this.meshTxBuilderBody.metadata, label, metadata, mergeExistingMetadataByLabel); return this; }; diff --git a/packages/mesh-transaction/src/transaction/index.ts b/packages/mesh-transaction/src/transaction/index.ts index fb3eecfef..7838301e6 100644 --- a/packages/mesh-transaction/src/transaction/index.ts +++ b/packages/mesh-transaction/src/transaction/index.ts @@ -586,15 +586,15 @@ export class Transaction { /** * Add a JSON metadata entry to the transaction. * - * @param {number} key The key to use for the metadata entry. - * @param {unknown} value The value to use for the metadata entry. - * @param {MetadataMergeLevel} mergeExistingMetadataByTag Whether to merge the new metadata - * under a tag (key) with the existing metadata with the same tag + * @param {number} label The label to use for the metadata entry. + * @param {unknown} metadata The value to use for the metadata entry. + * @param {MetadataMergeLevel} mergeExistingMetadataByLabel Whether to merge the new metadata + * with any existing metadata under the same label, and upto what level * @returns {Transaction} The Transaction object. * @see {@link https://meshjs.dev/apis/transaction#setMetadata} */ - setMetadata(key: number, value: unknown, mergeExistingMetadataByTag: MetadataMergeLevel = false): Transaction { - this.txBuilder.metadataValue(key.toString(), value as object, mergeExistingMetadataByTag); + setMetadata(label: number, metadata: unknown, mergeExistingMetadataByLabel: MetadataMergeLevel = false): Transaction { + this.txBuilder.metadataValue(label, metadata as object, mergeExistingMetadataByLabel); return this; } diff --git a/packages/mesh-transaction/src/transaction/transaction-v2.ts b/packages/mesh-transaction/src/transaction/transaction-v2.ts index 0fc7151db..184f3a819 100644 --- a/packages/mesh-transaction/src/transaction/transaction-v2.ts +++ b/packages/mesh-transaction/src/transaction/transaction-v2.ts @@ -12,6 +12,8 @@ import { UTxO, } from "@meshsdk/common"; +import type { MetadataMergeLevel } from "../utils/metadata"; + export interface TransactionV2 { sendAssets( receiver: string, @@ -47,7 +49,11 @@ export interface TransactionV2 { setRequiredSigners(addresses: string[]): this; setTimeToExpire(slot: string): this; setTimeToStart(slot: string): this; - setMetadata(key: number, value: unknown): this; + setMetadata( + label: number, + metadata: unknown, + mergeExistingMetadataByLabel: MetadataMergeLevel + ): this; withdrawRewards(rewardAddress: string, lovelace: string): this; delegateStake(rewardAddress: string, poolId: string): this; deregisterStake(rewardAddress: string): this; diff --git a/packages/mesh-transaction/src/utils/metadata.ts b/packages/mesh-transaction/src/utils/metadata.ts index e345ce0b2..e2d1f150b 100644 --- a/packages/mesh-transaction/src/utils/metadata.ts +++ b/packages/mesh-transaction/src/utils/metadata.ts @@ -1,34 +1,64 @@ import JSONBig from "json-bigint"; -import { Metadata } from "@meshsdk/common"; - -type ValueType = "array" | "object" | "primitive" | "nullish"; +import type { TxMetadata, Metadatum, MetadatumMap } from "@meshsdk/common"; export type MetadataMergeLevel = boolean | number; -const getMergeDepth = (mergeOption: MetadataMergeLevel): number => { - return typeof mergeOption === "number" - ? mergeOption - : mergeOption === true - ? 1 - : 0; +export const metadataObjToMap = (metadata: any): Metadatum => { + if (typeof metadata === "bigint") { + return metadata; + } else if (typeof metadata === "string") { + return metadata; + } else if (typeof metadata === "number") { + return metadata; + } else if (metadata instanceof Uint8Array) { + return metadata; + } else if (Array.isArray(metadata)) { + // Recursively process each element in the array + return metadata.map(metadataObjToMap); + } else if (metadata && typeof metadata === "object") { + // Convert object to MetadatumMap + const map: MetadatumMap = new Map(); + Object.entries(metadata).forEach(([key, value]) => { + map.set(metadataObjToMap(key), metadataObjToMap(value)); + }); + return map; + } else { + throw new Error("Metadata map conversion: Unsupported metadata type"); + } } -const getType = (value: unknown): ValueType => { - if (Array.isArray(value)) - return "array"; - if (value !== null && typeof value === "object") - return "object"; - if (value === null || value === undefined) - return "nullish"; - return "primitive"; +/** + * Insert new metadata under a label into the transaction's metadata section + * and optionally merge with the existing metadata under the same label recursively + * upto a specified depth. + * + * @param txMetadata metadata map of meshTxBuilderBody, updated implicitly + * @param label label that the new metadata corresponds to + * @param metadata the new metadata + * @param mergeOption the effective depth till which the merge should happen, + * beyond this depth, the newer element would replace the older one. + * If false or 0, the new metadata overwrites any existing metadata + * under the same label + * @returns updated metadata map for meshTxBuilderBody + */ +export const setAndMergeTxMetadata = (txMetadata: TxMetadata, label: bigint, metadata: Metadatum, mergeOption: MetadataMergeLevel): TxMetadata => { + const mergeDepth = getMergeDepth(mergeOption); + + if (txMetadata.has(label)) { + txMetadata.set(label, mergeContents(txMetadata.get(label) as Metadatum, metadata, mergeDepth)); + } else { + txMetadata.set(label, metadata); + } + + return txMetadata; } /** - * Recursively merge two items. Returns the 2nd item if the maximum allowed + * Recursively merge two metadata. Returns the 2nd item if the maximum allowed * merge depth has passed. * - * Merging objects ({ key: value }): - * Two objects are merged by recursively including the (key, value) pairs from both the objects. + * Merging maps ({ key: value }): + * Two maps are merged by recursively including the (key, value) pairs from both the maps. * When further merge isn't allowed (by currentDepth), the 2nd item is preferred, * replacing the 1st item. * @@ -46,88 +76,71 @@ const getType = (value: unknown): ValueType => { * @param currentDepth the current merge depth; decreases in a recursive call * @returns merged item or a preferred item, chosen according to currentDepth */ -const mergeContents = (a: any, b: any, currentDepth: number): any => { - const type_a = getType(a); - const type_b = getType(b); - - if (currentDepth <= 0 || type_a === "nullish" || type_b === "nullish") { - // Tend to return the 2nd item, which is supposed to be the updated one - // If both are nullish, 2nd item is returned - return b ?? a ?? b; +const mergeContents = (a: Metadatum, b: Metadatum, currentDepth: number): Metadatum => { + // Handle no merge + if (currentDepth <= 0) { + return b; } - - if (type_a === "primitive" && type_b === "primitive") { - if (a === b) { - return b; - } else { - throw new Error(`Tx metadata merge error: cannot merge ${a} with ${b}`); - } + // Handle merging of maps + if (a instanceof Map && b instanceof Map) { + b.forEach((value: Metadatum, key: Metadatum) => { + if (a.has(key)) { + a.set(key, mergeContents(a.get(key) as Metadatum, value, currentDepth - 1)); + } else { + a.set(key, value); + } + }); + return a; } - - else if (type_a === "array" && type_b === "array") { + // Handle merging of arrays + else if (Array.isArray(a) && Array.isArray(b)) { return [...a, ...b]; } - - else if (type_a === "object" && type_b === "object") { - for (const key in b) { - Object.assign(a, { [key]: mergeContents(a[key], b[key], currentDepth - 1) }); + // Handle merging of primitive types + if ( + (typeof a === "number" || typeof a === "bigint" || typeof a === "string" || a instanceof Uint8Array) && + (typeof b === "number" || typeof b === "bigint" || typeof b === "string" || b instanceof Uint8Array) + ) { + if (typeof a === typeof b) { + if (a === b) { + // Equal primitive types (string, number or bigint) + return b; + } + if (a instanceof Uint8Array && b instanceof Uint8Array && areUint8ArraysEqual(a, b)) { + // Equal Uint8Array values + return b; + } } - return a; + // If values are not equal or types are mismatched + throw new Error(`Tx metadata merge error: cannot merge ${JSONBig.stringify(a)} with ${JSONBig.stringify(b)}`); } - throw new Error(`Tx metadata merge error: cannot merge ${type_a} type with ${type_b} type`); + // Unsupported or mismatched types + throw new Error(`Tx metadata merge error: cannot merge ${getMetadatumType(a)} type with ${getMetadatumType(b)} type`); } -const mergeChosenMetadata = (metadataArr: string[], tag: string, mergeDepth: number): Metadata[] => { - if (!metadataArr.length) - return []; - - // Assuming all the elements are JSON stringified, parse them first - for (let i = 0; i < metadataArr.length; i++) { - try { - metadataArr[i] = JSONBig.parse(metadataArr[i]!); - } catch (e) { - throw new Error(`Tx metadata merge error: cannot parse metadata value: ${e}`); - } - } +const getMergeDepth = (mergeOption: MetadataMergeLevel): number => { + return typeof mergeOption === "number" + ? mergeOption + : mergeOption === true + ? 1 + : 0; +} - let mergedSoFar = metadataArr[0]; +const getMetadatumType = (a: Metadatum): string => { + if (a instanceof Map) return "map"; + if (Array.isArray(a)) return "array"; + return "primitive"; +} - for (let i = 1; i < metadataArr.length; i++) { - mergedSoFar = mergeContents(mergedSoFar, metadataArr[i], mergeDepth); +const areUint8ArraysEqual = (a: Uint8Array, b: Uint8Array): boolean => { + if (a.length !== b.length) { + return false; } - - return [{ tag, metadata: JSONBig.stringify(mergedSoFar) }]; -}; - -/** - * Merges multiple metadata entries in an array of metadata that belong to the same tag. - * Can merge objects upto a defined depth. - * - * @param metadataList metadata array of meshTxBuilderBody - * @param tag tag value to group all the metadata belonging to that same tag - * @param mergeOption the effective depth till which the merge will happen, - * beyond this depth, the newer element would replace the older one. - * If false or 0, the latest metadata entry with the specified tag is preserved - * and the earlier ones are discarded - * @returns metadata array for meshTxBuilderBody; with all the metadata entries not belonging - * to the specified tag are preserved - */ -export const mergeAllMetadataByTag = (metadataList: Metadata[], tag: string, mergeOption: MetadataMergeLevel): Metadata[] => { - const mergeDepth = getMergeDepth(mergeOption); - - const chosenElementsMetadata = []; - const restElements = []; - - for (const metadata of metadataList) { - if (metadata.tag == tag) { // Number can also match here - chosenElementsMetadata.push(metadata.metadata); - } else { - restElements.push(metadata); + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; } } - - const mergedItem = mergeChosenMetadata(chosenElementsMetadata, tag, mergeDepth); - - return [...restElements, ...mergedItem]; + return true; } diff --git a/packages/mesh-transaction/test/transaction/txMetadata.test.ts b/packages/mesh-transaction/test/transaction/txMetadata.test.ts index a0cbd8765..a945d3f83 100644 --- a/packages/mesh-transaction/test/transaction/txMetadata.test.ts +++ b/packages/mesh-transaction/test/transaction/txMetadata.test.ts @@ -1,205 +1,191 @@ -import JSONBig from "json-bigint"; -import { Metadata } from "@meshsdk/common"; -import { mergeAllMetadataByTag } from "@meshsdk/transaction"; +import { metadataObjToMap, setAndMergeTxMetadata } from "@meshsdk/transaction"; describe("Transaction Metadata Merge", () => { - - const createMetadataArray = (arr: { tag: string, metadata: any }[]): Metadata[] => { - for (const elem of arr) { - elem.metadata = JSONBig.stringify(elem.metadata); - } - return arr; - }; - it("should merge two identical number metadata entries", () => { - const input = createMetadataArray([ - { tag: "0", metadata: 42 }, - { tag: "0", metadata: 42 }, + const txMetadata = new Map([ + [0n, 42] ]); - const expectedOutput = createMetadataArray([ - { tag: "0", metadata: 42 }, + const label = 0n; + const metadata = 42; + const expectedOutput = new Map([ + [0n, 42] ]); - const output = mergeAllMetadataByTag(input, "0", true); - expect(output).toEqual(expectedOutput); + setAndMergeTxMetadata(txMetadata, label, metadata, true); + expect(txMetadata).toEqual(expectedOutput); }); it("should merge two identical string metadata entries", () => { - const input = createMetadataArray([ - { tag: "1", metadata: "42" }, - { tag: "1", metadata: "42" }, + const txMetadata = new Map([ + [1n, "Hey!"] ]); - const expectedOutput = createMetadataArray([ - { tag: "1", metadata: "42" }, + const label = 1n; + const metadata = "Hey!"; + const expectedOutput = new Map([ + [1n, "Hey!"] ]); - const output = mergeAllMetadataByTag(input, "1", true); - expect(output).toEqual(expectedOutput); + setAndMergeTxMetadata(txMetadata, label, metadata, true); + expect(txMetadata).toEqual(expectedOutput); }); it("should not merge two different numbers", () => { - const input = createMetadataArray([ - { tag: "1", metadata: 42 }, - { tag: "1", metadata: 43 }, + const txMetadata = new Map([ + [1n, 42] ]); - expect(() => mergeAllMetadataByTag(input, "1", true)).toThrow("cannot merge 42 with 43"); + const label = 1n; + const metadata = 43; + expect(() => setAndMergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge 42 with 43"); }); it("should not merge two different strings", () => { - const input = createMetadataArray([ - { tag: "0", metadata: "Alice" }, - { tag: "0", metadata: "Bob" }, + const txMetadata = new Map([ + [0n, "Alice"] ]); - expect(() => mergeAllMetadataByTag(input, "0", true)).toThrow("cannot merge Alice with Bob"); + const label = 0n; + const metadata = "Bob"; + expect(() => setAndMergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge \"Alice\" with \"Bob\""); }); it("should not merge two same values of different types", () => { - const input = createMetadataArray([ - { tag: "0", metadata: 42 }, - { tag: "0", metadata: "42" }, + const txMetadata = new Map([ + [0n, 42] ]); - expect(() => mergeAllMetadataByTag(input, "0", true)).toThrow("cannot merge 42 with 42"); + const label = 0n; + const metadata = "42"; + expect(() => setAndMergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge 42 with \"42\""); }); it("should replace with the latest item if there is no merge", () => { - const input = createMetadataArray([ - { tag: "1", metadata: 42 }, - { tag: "1", metadata: 43 }, + const txMetadata = new Map([ + [1n, 42] ]); - const expectedOutput = createMetadataArray([ - { tag: "1", metadata: 43 }, + const label = 1n; + const metadata = 43; + const expectedOutput = new Map([ + [1n, 43] ]); - expect(mergeAllMetadataByTag(input, "1", false)).toEqual(expectedOutput); + setAndMergeTxMetadata(txMetadata, label, metadata, false); + expect(txMetadata).toEqual(expectedOutput); }); it("should not merge two different values of the same object key", () => { - const input = createMetadataArray([ - { tag: "721", metadata: { version: 1 } }, - { tag: "721", metadata: { version: 2 } }, + const txMetadata = new Map([ + [721n, metadataObjToMap({ version: 1 })] ]); - expect(() => mergeAllMetadataByTag(input, "721", 2)).toThrow("cannot merge 1 with 2"); + const label = 721n; + const metadata = metadataObjToMap({ version: 2 }); + expect(() => setAndMergeTxMetadata(txMetadata, label, metadata, 2)).toThrow("cannot merge 1 with 2"); }); it("should replace with the latest value of the same object key if values are not merged", () => { - const input = createMetadataArray([ - { tag: "721", metadata: { version: 1 } }, - { tag: "721", metadata: { version: 2 } }, + const txMetadata = new Map([ + [721n, metadataObjToMap({ version: 1 })] ]); - const expectedOutput = createMetadataArray([ - { tag: "721", metadata: { version: 2 } }, + const label = 721n; + const metadata = metadataObjToMap({ version: 2 }); + const expectedOutput = new Map([ + [721n, metadataObjToMap({ version: 2 })] ]); - expect(mergeAllMetadataByTag(input, "721", 1)).toEqual(expectedOutput); - expect(mergeAllMetadataByTag(input, "721", true)).toEqual(expectedOutput); + setAndMergeTxMetadata(txMetadata, label, metadata, 1); + expect(txMetadata).toEqual(expectedOutput); }); it("should not merge different types", () => { - expect(() => mergeAllMetadataByTag( - createMetadataArray([ - { tag: "0", metadata: 0 }, - { tag: "0", metadata: [] } - ]), - "0", + expect(() => setAndMergeTxMetadata( + new Map([[0n, 0]]), + 0n, // label + [], // metadata true )).toThrow("cannot merge primitive type with array type"); - expect(() => mergeAllMetadataByTag( - createMetadataArray([ - { tag: "0", metadata: {} }, - { tag: "0", metadata: "" } - ]), - "0", + expect(() => setAndMergeTxMetadata( + new Map([[0n, metadataObjToMap({})]]), + 0n, // label + "", // metadata true - )).toThrow("cannot merge object type with primitive type"); - expect(() => mergeAllMetadataByTag( - createMetadataArray([ - { tag: "0", metadata: {} }, - { tag: "0", metadata: [] } - ]), - "0", + )).toThrow("cannot merge map type with primitive type"); + expect(() => setAndMergeTxMetadata( + new Map([[0n, metadataObjToMap({})]]), + 0n, // label + [], // metadata true - )).toThrow("cannot merge object type with array type"); + )).toThrow("cannot merge map type with array type"); }); - it("should preserve object key and value if merged with a nullish value", () => { - expect(mergeAllMetadataByTag( - createMetadataArray([ - { tag: "0", metadata: { value: 1 } }, - { tag: "0", metadata: { value: null } } - ]), - "0", - 2 - )).toEqual(createMetadataArray([ - { tag: "0", metadata: { value: 1 } } - ])); - expect(mergeAllMetadataByTag( - createMetadataArray([ - { tag: "0", metadata: { value: 1 } }, - { tag: "0", metadata: { value: undefined } } - ]), - "0", - true - )).toEqual(createMetadataArray([ - { tag: "0", metadata: { value: 1 } } - ])); + it("plain object to map conversion should not allow nullish values", () => { + expect(() => metadataObjToMap({ "value": null })).toThrow("Unsupported metadata type"); }); it("should replace 674 standard msg array for default merge depth", () => { - const input = createMetadataArray([ - { tag: "674", metadata: { msg: ["A", "B", "C"] } }, - { tag: "674", metadata: { msg: ["D", "E", "F"] } }, - { tag: "674", metadata: { msg2: ["X", "Y", "Z"] } }, + const txMetadata = new Map([ + [ + 674n, + metadataObjToMap({ + msg: ["A", "B", "C"], + msg2: ["X", "Y", "Z"] + }) + ] ]); - - const expectedOutput = createMetadataArray([ - { - tag: "674", metadata: { + const label = 674n; + const metadata = metadataObjToMap({ + msg: ["D", "E", "F"] + }); + const expectedOutput = new Map([ + [ + 674n, + metadataObjToMap({ msg: ["D", "E", "F"], msg2: ["X", "Y", "Z"] - } - } + }) + ] ]); - - expect(mergeAllMetadataByTag(input, "674", 1)).toEqual(expectedOutput); - expect(mergeAllMetadataByTag(input, "674", true)).toEqual(expectedOutput); + setAndMergeTxMetadata(txMetadata, label, metadata, true); + expect(txMetadata).toEqual(expectedOutput); }); it("should concatenate 674 standard msg arrays for merge depth 2", () => { - const input = createMetadataArray([ - { tag: "674", metadata: { msg: ["A", "B", "C"] } }, - { tag: "674", metadata: { msg: ["D", "E", "F"] } }, - { tag: "674", metadata: { msg2: ["X", "Y", "Z"] } }, + const txMetadata = new Map([ + [ + 674n, + metadataObjToMap({ + msg: ["A", "B", "C"], + msg2: ["X", "Y", "Z"] + }) + ] ]); - - const expectedOutput = createMetadataArray([ - { - tag: "674", metadata: { + const label = 674n; + const metadata = metadataObjToMap({ + msg: ["D", "E", "F"] + }); + const expectedOutput = new Map([ + [ + 674n, + metadataObjToMap({ msg: ["A", "B", "C", "D", "E", "F"], msg2: ["X", "Y", "Z"] - } - } + }) + ] ]); - - expect(mergeAllMetadataByTag(input, "674", 2)).toEqual(expectedOutput); + setAndMergeTxMetadata(txMetadata, label, metadata, 2); + expect(txMetadata).toEqual(expectedOutput); }); it("should merge multiple CIP-25 NFTs metadata under the same policy id", () => { - const input: Metadata[] = createMetadataArray([ - { - tag: "721", - metadata: { + const txMetadata = new Map([ + [ + 721n, + metadataObjToMap({ "policyId1": { "My NFT 1": { "name": "My NFT 1" } } - } - }, - { - tag: "721", - metadata: { - "policyId1": { - "My NFT 2": { - "name": "My NFT 2", - "description": "My second NFT" - } - } + }) + ] + ]); + const label = 721n; + const metadata = metadataObjToMap({ + "policyId1": { + "My NFT 2": { + "name": "My NFT 2", + "description": "My second NFT" } } - ]); - - const expectedOutput: Metadata[] = createMetadataArray([ - { - tag: "721", - metadata: { + }); + const expectedOutput = new Map([ + [ + 721n, + metadataObjToMap({ "policyId1": { "My NFT 1": { "name": "My NFT 1" @@ -209,18 +195,18 @@ describe("Transaction Metadata Merge", () => { "description": "My second NFT" } } - } - } + }) + ] ]); - - expect(mergeAllMetadataByTag(input, "721", 2)).toEqual(expectedOutput); + setAndMergeTxMetadata(txMetadata, label, metadata, 2); + expect(txMetadata).toEqual(expectedOutput); }); it("should merge multiple CIP-25 NFTs metadata under different policy ids", () => { - const input = createMetadataArray([ - { - tag: "721", - metadata: { + const txMetadata = new Map([ + [ + 721n, + metadataObjToMap({ "policyId1": { "My NFT 1": { "name": "My NFT 1", @@ -229,40 +215,60 @@ describe("Transaction Metadata Merge", () => { ] } } + }) + ] + ]); + const label = 721n; + const metadata1 = metadataObjToMap({ + "policyId2": { + "My NFT 1": { + "name": "My NFT 1 Policy 2", + "files": [ + { name: "NFT 1 P 2", src: "abc", mediaType: "image/png" } + ] } - }, - { - tag: "721", - metadata: { - "policyId2": { + } + }); + const expectedOutput1 = new Map([ + [ + 721n, + metadataObjToMap({ + "policyId1": { "My NFT 1": { - "name": "My NFT 1 Policy 2", + "name": "My NFT 1", "files": [ - { name: "NFT 1 P 2", src: "abc", mediaType: "image/png" } + { name: "NFT 1 Image", src: "xyz", mediaType: "image/jpeg" } ] } - } - } - }, - { - tag: "721", - metadata: { - "policyId1": { - "My NFT 2": { - "name": "My NFT 2", + }, + "policyId2": { + "My NFT 1": { + "name": "My NFT 1 Policy 2", "files": [ - { name: "NFT 2 Image", src: "pqr", mediaType: "image/jpeg" } + { name: "NFT 1 P 2", src: "abc", mediaType: "image/png" } ] } } + }) + ] + ]); + setAndMergeTxMetadata(txMetadata, label, metadata1, 2); + expect(txMetadata).toEqual(expectedOutput1); + // Merge more NFT metadata + const metadata2 = metadataObjToMap({ + "policyId1": { + "My NFT 2": { + "name": "My NFT 2", + "files": [ + { name: "NFT 2 Image", src: "pqr", mediaType: "image/jpeg" } + ] } } - ]); - - const expectedOutput = createMetadataArray([ - { - tag: "721", - metadata: { + }); + const expectedOutput2 = new Map([ + [ + 721n, + metadataObjToMap({ "policyId1": { "My NFT 1": { "name": "My NFT 1", @@ -285,133 +291,90 @@ describe("Transaction Metadata Merge", () => { ] } } - } - } + }) + ] ]); - - expect(mergeAllMetadataByTag(input, "721", 2)).toEqual(expectedOutput); + setAndMergeTxMetadata(txMetadata, label, metadata2, 2); + expect(txMetadata).toEqual(expectedOutput2); }); it("should replace with the latest CIP-25 NFT metadata of the same policy and asset id", () => { - const input = createMetadataArray([ - { - tag: "721", - metadata: { - "policyId1": { - "My NFT 1": { name: "NFT 1 Name", files: [{ name: "NFT Image" }] } // old metadata here - } - } - }, - { - tag: "721", - metadata: { + const txMetadata = new Map([ + [ + 721n, + metadataObjToMap({ "policyId1": { + "My NFT 1": { name: "NFT 1 Name", files: [{ name: "NFT Image" }] }, // old metadata here "My NFT 2": { name: "NFT 2 Name" } } - } - }, - { - tag: "721", - metadata: { - "policyId1": { - "My NFT 1": { name: "Latest NFT 1", image: "xyz", description: "Latest NFT here" } - } - } - } + }) + ] ]); - - const expectedOutput = createMetadataArray([ - { - tag: "721", - metadata: { + const label = 721n; + const metadata = metadataObjToMap({ + "policyId1": { + "My NFT 1": { name: "Latest NFT 1", image: "xyz", description: "Latest NFT here" } + } + }); + const expectedOutput = new Map([ + [ + 721n, + metadataObjToMap({ "policyId1": { "My NFT 1": { name: "Latest NFT 1", image: "xyz", description: "Latest NFT here" }, "My NFT 2": { name: "NFT 2 Name" } } - } - } + }) + ] ]); - - expect(mergeAllMetadataByTag(input, "721", 2)).toEqual(expectedOutput); + setAndMergeTxMetadata(txMetadata, label, metadata, 2); + expect(txMetadata).toEqual(expectedOutput); }); it("should attach version to CIP-25 metadata", () => { - const input = createMetadataArray([ - { - tag: "721", - metadata: { - "policyId1": { "My NFT 1": { name: "My NFT 1" } } - } - }, - { - tag: "721", - metadata: { - "policyId1": { "My NFT 2": { name: "My NFT 2" } } - } - }, - { - tag: "721", - metadata: { - version: "1.0" - } - }, - { - tag: "721", - metadata: { + const txMetadata = new Map([ + [ + 721n, + metadataObjToMap({ + "policyId1": { "My NFT 1": { name: "My NFT 1" } }, "policyId2": { "My NFT 1": { name: "My NFT 1 Policy 2" } } - } - } + }) + ] ]); - - const expectedOutput = createMetadataArray([ - { - tag: "721", - metadata: { - "policyId1": { - "My NFT 1": { name: "My NFT 1" }, - "My NFT 2": { name: "My NFT 2" } - }, - "version": "1.0", // version inserted in an ordered manner - "policyId2": { - "My NFT 1": { name: "My NFT 1 Policy 2" } - } - } - } + const label = 721n; + const metadata = metadataObjToMap({ + version: 1 + }); + const expectedOutput = new Map([ + [ + 721n, + metadataObjToMap({ + "policyId1": { "My NFT 1": { name: "My NFT 1" } }, + "policyId2": { "My NFT 1": { name: "My NFT 1 Policy 2" } }, + "version": 1 + }) + ] ]); - - expect(mergeAllMetadataByTag(input, "721", 2)).toEqual(expectedOutput); + setAndMergeTxMetadata(txMetadata, label, metadata, 2); + expect(txMetadata).toEqual(expectedOutput); }); it("should preserve metadata entries with other tags in the original order", () => { - const input = createMetadataArray([ - { tag: "0", metadata: "line 1" }, - { tag: "0", metadata: "line 2" }, - { tag: "674", metadata: { msg: ["line 3"] } }, - { tag: "721", metadata: { policyId: { NFT: { name: "NFT" } } } }, - { tag: "674", metadata: { msg: ["line 5"] } }, - { tag: "721", metadata: { policyId: { NFT2: { name: "NFT 2" } } } }, - { tag: "1", metadata: "line 7" }, + const txMetadata = new Map([ + [0n, metadataObjToMap("line 1")], + [674n, metadataObjToMap({ msg: "line 2" })], + [721n, metadataObjToMap({ policyId1: { NFT1: { name: "line 3" } } })], + [1n, metadataObjToMap("line 4")] ]); - - const expectedOutput1 = createMetadataArray([ - { tag: "0", metadata: "line 1" }, - { tag: "0", metadata: "line 2" }, - { tag: "674", metadata: { msg: ["line 3"] } }, - { tag: "674", metadata: { msg: ["line 5"] } }, - { tag: "1", metadata: "line 7" }, - { tag: "721", metadata: { policyId: { NFT: { name: "NFT" }, NFT2: { name: "NFT 2" } } } }, + const label = 721n; + const metadata = metadataObjToMap({ policyId1: { NFT2: { name: "line 5" } } }); + const expectedOutput = new Map([ + [0n, metadataObjToMap("line 1")], + [674n, metadataObjToMap({ msg: "line 2" })], + [721n, metadataObjToMap({ policyId1: { NFT1: { name: "line 3" }, NFT2: { name: "line 5" } } })], + [1n, metadataObjToMap("line 4")] ]); - - expect(mergeAllMetadataByTag(input, "721", 2)).toEqual(expectedOutput1); - - const expectedOutput2 = createMetadataArray([ - { tag: "0", metadata: "line 1" }, - { tag: "0", metadata: "line 2" }, - { tag: "1", metadata: "line 7" }, - { tag: "721", metadata: { policyId: { NFT: { name: "NFT" }, NFT2: { name: "NFT 2" } } } }, - { tag: "674", metadata: { msg: ["line 5"] } }, - ]); - - expect(mergeAllMetadataByTag(expectedOutput1, "674", true)).toEqual(expectedOutput2); + setAndMergeTxMetadata(txMetadata, label, metadata, 2); + expect(txMetadata).toEqual(expectedOutput); }); }); From 17c1d99f3aa01f0d7400860a48c97267142ba46a Mon Sep 17 00:00:00 2001 From: twwu123 Date: Fri, 13 Dec 2024 17:46:00 +0800 Subject: [PATCH 3/6] general fixes to metadata --- .../get-supported-extensions.tsx | 2 +- .../providers/hydra-endpoints/on-message.tsx | 2 +- .../content-ownership/offchain/offchain.ts | 2 +- .../mesh-contract/src/plutus-nft/offchain.ts | 2 +- .../src/core/adaptor/metadata.ts | 52 +++-- .../src/mesh-tx-builder/tx-builder-core.ts | 20 +- .../mesh-transaction/src/transaction/index.ts | 30 ++- .../mesh-transaction/src/utils/metadata.ts | 218 +++++++++--------- .../test/transaction/txMetadata.test.ts | 40 ++-- 9 files changed, 194 insertions(+), 174 deletions(-) diff --git a/apps/playground/src/pages/apis/wallets/browserwallet/get-supported-extensions.tsx b/apps/playground/src/pages/apis/wallets/browserwallet/get-supported-extensions.tsx index 6db1a1bb3..e80235dcb 100644 --- a/apps/playground/src/pages/apis/wallets/browserwallet/get-supported-extensions.tsx +++ b/apps/playground/src/pages/apis/wallets/browserwallet/get-supported-extensions.tsx @@ -1,4 +1,4 @@ -import { use, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { BrowserWallet } from "@meshsdk/core"; import { useWalletList } from "@meshsdk/react"; diff --git a/apps/playground/src/pages/providers/hydra-endpoints/on-message.tsx b/apps/playground/src/pages/providers/hydra-endpoints/on-message.tsx index 8edd5c845..52120098b 100644 --- a/apps/playground/src/pages/providers/hydra-endpoints/on-message.tsx +++ b/apps/playground/src/pages/providers/hydra-endpoints/on-message.tsx @@ -1,4 +1,4 @@ -import { use, useEffect } from "react"; +import { useEffect } from "react"; import { HydraProvider } from "@meshsdk/core"; diff --git a/packages/mesh-contract/src/content-ownership/offchain/offchain.ts b/packages/mesh-contract/src/content-ownership/offchain/offchain.ts index e821502c4..faa3acace 100644 --- a/packages/mesh-contract/src/content-ownership/offchain/offchain.ts +++ b/packages/mesh-contract/src/content-ownership/offchain/offchain.ts @@ -525,7 +525,7 @@ export class MeshContentOwnershipContract extends MeshTxInitiator { const txHex = await this.mesh .mint("1", policyId, tokenNameHex) .mintingScript(forgingScript) - .metadataValue(721, metadata, 2) + .metadataValue(721, metadata) .changeAddress(walletAddress) .selectUtxosFrom(utxos) .complete(); diff --git a/packages/mesh-contract/src/plutus-nft/offchain.ts b/packages/mesh-contract/src/plutus-nft/offchain.ts index 6aa6c007e..56820090c 100644 --- a/packages/mesh-contract/src/plutus-nft/offchain.ts +++ b/packages/mesh-contract/src/plutus-nft/offchain.ts @@ -200,7 +200,7 @@ export class MeshPlutusNFTContract extends MeshTxInitiator { if (assetMetadata) { const metadata = { [policyId]: { [tokenName]: { ...assetMetadata } } }; - tx.metadataValue(721, metadata, 2); + tx.metadataValue(721, metadata); } tx.mintRedeemerValue(mConStr0([])) diff --git a/packages/mesh-core-csl/src/core/adaptor/metadata.ts b/packages/mesh-core-csl/src/core/adaptor/metadata.ts index 1f7ecd4b3..c56fa1ce9 100644 --- a/packages/mesh-core-csl/src/core/adaptor/metadata.ts +++ b/packages/mesh-core-csl/src/core/adaptor/metadata.ts @@ -1,36 +1,40 @@ import JSONbig from "json-bigint"; + import type { Metadata, Metadatum, TxMetadata } from "@meshsdk/common"; export const txMetadataToObj = (metadata: TxMetadata): Metadata[] => { - const result: Metadata[] = []; - metadata.forEach((value: Metadatum, key: bigint) => { - result.push({ tag: key.toString(), metadata: JSONbig.stringify(metadatumToObj(value)) }); + const result: Metadata[] = []; + metadata.forEach((value: Metadatum, key: bigint) => { + result.push({ + tag: key.toString(), + metadata: JSONbig.stringify(metadatumToObj(value)), }); - return result; + }); + return result; }; const metadatumToObj = (metadatum: Metadatum): any => { - if (typeof metadatum === "number" || typeof metadatum === "string") { - return metadatum; - } else if (typeof metadatum === "bigint") { - return metadatum.toString(); - } else if (metadatum instanceof Uint8Array) { - return uint8ArrayToHex(metadatum); - } else if (metadatum instanceof Map) { - const result: Record = {}; - metadatum.forEach((value, key) => { - result[metadatumToObj(key)] = metadatumToObj(value); - }); - return result; - } else if (Array.isArray(metadatum)) { - return metadatum.map(metadatumToObj); - } else { - throw new Error("metadatumToObj: Unsupported Metadatum type"); - } + if (typeof metadatum === "number" || typeof metadatum === "string") { + return metadatum; + } else if (typeof metadatum === "bigint") { + return metadatum.toString(); + } else if (metadatum instanceof Uint8Array) { + return uint8ArrayToHex(metadatum); + } else if (metadatum instanceof Map) { + const result: Record = {}; + metadatum.forEach((value, key) => { + result[metadatumToObj(key)] = metadatumToObj(value); + }); + return result; + } else if (Array.isArray(metadatum)) { + return metadatum.map(metadatumToObj); + } else { + throw new Error("metadatumToObj: Unsupported Metadatum type"); + } }; const uint8ArrayToHex = (bytes: Uint8Array): string => { - return Array.from(bytes) - .map(byte => byte.toString(16).padStart(2, "0")) - .join(""); + return Array.from(bytes) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); }; diff --git a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts index 23a51c10d..72b4aae07 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts @@ -14,6 +14,7 @@ import { emptyTxBuilderBody, LanguageVersion, MeshTxBuilderBody, + Metadatum, MintItem, Network, Output, @@ -25,6 +26,7 @@ import { RefTxIn, TxIn, TxInParameter, + TxMetadata, Unit, UTxO, UtxoSelection, @@ -35,7 +37,7 @@ import { Withdrawal, } from "@meshsdk/common"; -import { MetadataMergeLevel, metadataObjToMap, setAndMergeTxMetadata } from "../utils"; +import { metadataObjToMap } from "../utils"; export class MeshTxBuilderCore { txEvaluationMultiplier = 1.1; @@ -1420,10 +1422,16 @@ export class MeshTxBuilderCore { * with any existing metadata under the same label, and upto what level * @returns The MeshTxBuilder instance */ - metadataValue = (label: number | bigint | string, metadata: any, mergeExistingMetadataByLabel: MetadataMergeLevel = false) => { + metadataValue = ( + label: number | bigint | string, + metadata: Metadatum | object, + ) => { label = BigInt(label); - metadata = metadataObjToMap(metadata); - setAndMergeTxMetadata(this.meshTxBuilderBody.metadata, label, metadata, mergeExistingMetadataByLabel); + if (typeof metadata === "object" && !(metadata instanceof Map)) { + this.meshTxBuilderBody.metadata.set(label, metadataObjToMap(metadata)); + } else { + this.meshTxBuilderBody.metadata.set(label, metadata); + } return this; }; @@ -1474,7 +1482,7 @@ export class MeshTxBuilderCore { return this; }; - /** + /** * Sets a specific fee for the transaction to use * @param fee The specified fee * @returns The MeshTxBuilder instance @@ -1482,7 +1490,7 @@ export class MeshTxBuilderCore { setFee = (fee: string) => { this.meshTxBuilderBody.fee = fee; return this; - } + }; /** * Sets the network to use, this is mainly to know the cost models to be used to calculate script integrity hash diff --git a/packages/mesh-transaction/src/transaction/index.ts b/packages/mesh-transaction/src/transaction/index.ts index 7838301e6..ed7abf3bf 100644 --- a/packages/mesh-transaction/src/transaction/index.ts +++ b/packages/mesh-transaction/src/transaction/index.ts @@ -9,6 +9,7 @@ import { hexToString, IInitiator, metadataToCip68, + Metadatum, Mint, NativeScript, Network, @@ -33,7 +34,7 @@ import { } from "@meshsdk/core-cst"; import { MeshTxBuilder, MeshTxBuilderOptions } from "../mesh-tx-builder"; -import { MetadataMergeLevel } from "../utils"; +import { mergeContents, metadataObjToMap } from "../utils"; export interface TransactionOptions extends MeshTxBuilderOptions { initiator: IInitiator; @@ -471,11 +472,24 @@ export class Transaction { } if (!mint.cip68ScriptAddress && mint.metadata && mint.label) { if (mint.label === "721" || mint.label === "20") { - this.setMetadata(Number(mint.label), { - [policyId]: { [mint.assetName]: mint.metadata }, - }, mint.label === "721" ? 2 : true); + let currentMetadata = this.txBuilder.meshTxBuilderBody.metadata; + if (currentMetadata.size === 0) { + this.setMetadata(Number(mint.label), { + [policyId]: { [mint.assetName]: mint.metadata }, + }); + } else { + let metadataMap = metadataObjToMap({ + [policyId]: { [mint.assetName]: mint.metadata }, + } as object); + let newMetadata = mergeContents( + currentMetadata.get(BigInt(mint.label)) as Metadatum, + metadataMap, + mint.label === "721" ? 2 : 0, + ); + this.setMetadata(Number(mint.label), newMetadata); + } } else { - this.setMetadata(Number(mint.label), mint.metadata, true); + this.setMetadata(Number(mint.label), mint.metadata); } } @@ -588,13 +602,11 @@ export class Transaction { * * @param {number} label The label to use for the metadata entry. * @param {unknown} metadata The value to use for the metadata entry. - * @param {MetadataMergeLevel} mergeExistingMetadataByLabel Whether to merge the new metadata - * with any existing metadata under the same label, and upto what level * @returns {Transaction} The Transaction object. * @see {@link https://meshjs.dev/apis/transaction#setMetadata} */ - setMetadata(label: number, metadata: unknown, mergeExistingMetadataByLabel: MetadataMergeLevel = false): Transaction { - this.txBuilder.metadataValue(label, metadata as object, mergeExistingMetadataByLabel); + setMetadata(label: number, metadata: Metadatum | object): Transaction { + this.txBuilder.metadataValue(label, metadata); return this; } diff --git a/packages/mesh-transaction/src/utils/metadata.ts b/packages/mesh-transaction/src/utils/metadata.ts index e2d1f150b..47ca41d1d 100644 --- a/packages/mesh-transaction/src/utils/metadata.ts +++ b/packages/mesh-transaction/src/utils/metadata.ts @@ -1,146 +1,142 @@ import JSONBig from "json-bigint"; -import type { TxMetadata, Metadatum, MetadatumMap } from "@meshsdk/common"; + +import type { Metadatum, MetadatumMap, TxMetadata } from "@meshsdk/common"; export type MetadataMergeLevel = boolean | number; export const metadataObjToMap = (metadata: any): Metadatum => { - if (typeof metadata === "bigint") { - return metadata; - } else if (typeof metadata === "string") { - return metadata; - } else if (typeof metadata === "number") { - return metadata; - } else if (metadata instanceof Uint8Array) { - return metadata; - } else if (Array.isArray(metadata)) { - // Recursively process each element in the array - return metadata.map(metadataObjToMap); - } else if (metadata && typeof metadata === "object") { - // Convert object to MetadatumMap - const map: MetadatumMap = new Map(); - Object.entries(metadata).forEach(([key, value]) => { - map.set(metadataObjToMap(key), metadataObjToMap(value)); - }); - return map; - } else { - throw new Error("Metadata map conversion: Unsupported metadata type"); - } -} - -/** - * Insert new metadata under a label into the transaction's metadata section - * and optionally merge with the existing metadata under the same label recursively - * upto a specified depth. - * - * @param txMetadata metadata map of meshTxBuilderBody, updated implicitly - * @param label label that the new metadata corresponds to - * @param metadata the new metadata - * @param mergeOption the effective depth till which the merge should happen, - * beyond this depth, the newer element would replace the older one. - * If false or 0, the new metadata overwrites any existing metadata - * under the same label - * @returns updated metadata map for meshTxBuilderBody - */ -export const setAndMergeTxMetadata = (txMetadata: TxMetadata, label: bigint, metadata: Metadatum, mergeOption: MetadataMergeLevel): TxMetadata => { - const mergeDepth = getMergeDepth(mergeOption); - - if (txMetadata.has(label)) { - txMetadata.set(label, mergeContents(txMetadata.get(label) as Metadatum, metadata, mergeDepth)); - } else { - txMetadata.set(label, metadata); - } - - return txMetadata; -} + if (typeof metadata === "bigint") { + return metadata; + } else if (typeof metadata === "string") { + return metadata; + } else if (typeof metadata === "number") { + return metadata; + } else if (metadata instanceof Uint8Array) { + return metadata; + } else if (Array.isArray(metadata)) { + // Recursively process each element in the array + return metadata.map(metadataObjToMap); + } else if (metadata && typeof metadata === "object") { + // Convert object to MetadatumMap + const map: MetadatumMap = new Map(); + Object.entries(metadata).forEach(([key, value]) => { + map.set(metadataObjToMap(key), metadataObjToMap(value)); + }); + return map; + } else { + throw new Error("Metadata map conversion: Unsupported metadata type"); + } +}; /** * Recursively merge two metadata. Returns the 2nd item if the maximum allowed * merge depth has passed. - * + * * Merging maps ({ key: value }): * Two maps are merged by recursively including the (key, value) pairs from both the maps. * When further merge isn't allowed (by currentDepth), the 2nd item is preferred, * replacing the 1st item. - * + * * Merging arrays: * Two arrays are merged by concatenating them. * When merge isn't allowed (by currentDepth), the 2nd array is returned. - * + * * Merging primitive types (number, string, etc.): * Primitive types are not merged in the sense of concatenating. In case they are the same, * either of them can be considered as the "merged value". 2nd item is returned here. * When merge isn't allowed (by currentDepth), the 2nd item is returned. - * + * * @param a first item * @param b second item * @param currentDepth the current merge depth; decreases in a recursive call * @returns merged item or a preferred item, chosen according to currentDepth */ -const mergeContents = (a: Metadatum, b: Metadatum, currentDepth: number): Metadatum => { - // Handle no merge - if (currentDepth <= 0) { +export const mergeContents = ( + a: Metadatum, + b: Metadatum, + currentDepth: number, +): Metadatum => { + // Handle no merge + if (currentDepth <= 0) { + return b; + } + // Handle merging of maps + if (a instanceof Map && b instanceof Map) { + b.forEach((value: Metadatum, key: Metadatum) => { + if (a.has(key)) { + a.set( + key, + mergeContents(a.get(key) as Metadatum, value, currentDepth - 1), + ); + } else { + a.set(key, value); + } + }); + return a; + } + // Handle merging of arrays + else if (Array.isArray(a) && Array.isArray(b)) { + return [...a, ...b]; + } + // Handle merging of primitive types + if ( + (typeof a === "number" || + typeof a === "bigint" || + typeof a === "string" || + a instanceof Uint8Array) && + (typeof b === "number" || + typeof b === "bigint" || + typeof b === "string" || + b instanceof Uint8Array) + ) { + if (typeof a === typeof b) { + if (a === b) { + // Equal primitive types (string, number or bigint) return b; + } + if ( + a instanceof Uint8Array && + b instanceof Uint8Array && + areUint8ArraysEqual(a, b) + ) { + // Equal Uint8Array values + return b; + } } - // Handle merging of maps - if (a instanceof Map && b instanceof Map) { - b.forEach((value: Metadatum, key: Metadatum) => { - if (a.has(key)) { - a.set(key, mergeContents(a.get(key) as Metadatum, value, currentDepth - 1)); - } else { - a.set(key, value); - } - }); - return a; - } - // Handle merging of arrays - else if (Array.isArray(a) && Array.isArray(b)) { - return [...a, ...b]; - } - // Handle merging of primitive types - if ( - (typeof a === "number" || typeof a === "bigint" || typeof a === "string" || a instanceof Uint8Array) && - (typeof b === "number" || typeof b === "bigint" || typeof b === "string" || b instanceof Uint8Array) - ) { - if (typeof a === typeof b) { - if (a === b) { - // Equal primitive types (string, number or bigint) - return b; - } - if (a instanceof Uint8Array && b instanceof Uint8Array && areUint8ArraysEqual(a, b)) { - // Equal Uint8Array values - return b; - } - } - // If values are not equal or types are mismatched - throw new Error(`Tx metadata merge error: cannot merge ${JSONBig.stringify(a)} with ${JSONBig.stringify(b)}`); - } + // If values are not equal or types are mismatched + throw new Error( + `Tx metadata merge error: cannot merge ${JSONBig.stringify(a)} with ${JSONBig.stringify(b)}`, + ); + } - // Unsupported or mismatched types - throw new Error(`Tx metadata merge error: cannot merge ${getMetadatumType(a)} type with ${getMetadatumType(b)} type`); -} + // Unsupported or mismatched types + throw new Error( + `Tx metadata merge error: cannot merge ${getMetadatumType(a)} type with ${getMetadatumType(b)} type`, + ); +}; const getMergeDepth = (mergeOption: MetadataMergeLevel): number => { - return typeof mergeOption === "number" - ? mergeOption - : mergeOption === true - ? 1 - : 0; -} + return typeof mergeOption === "number" + ? mergeOption + : mergeOption === true + ? 1 + : 0; +}; const getMetadatumType = (a: Metadatum): string => { - if (a instanceof Map) return "map"; - if (Array.isArray(a)) return "array"; - return "primitive"; -} + if (a instanceof Map) return "map"; + if (Array.isArray(a)) return "array"; + return "primitive"; +}; const areUint8ArraysEqual = (a: Uint8Array, b: Uint8Array): boolean => { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return false; - } + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; } - return true; -} + } + return true; +}; diff --git a/packages/mesh-transaction/test/transaction/txMetadata.test.ts b/packages/mesh-transaction/test/transaction/txMetadata.test.ts index a945d3f83..39a00ac58 100644 --- a/packages/mesh-transaction/test/transaction/txMetadata.test.ts +++ b/packages/mesh-transaction/test/transaction/txMetadata.test.ts @@ -1,4 +1,4 @@ -import { metadataObjToMap, setAndMergeTxMetadata } from "@meshsdk/transaction"; +import { metadataObjToMap, mergeTxMetadata } from "@meshsdk/transaction"; describe("Transaction Metadata Merge", () => { it("should merge two identical number metadata entries", () => { @@ -10,7 +10,7 @@ describe("Transaction Metadata Merge", () => { const expectedOutput = new Map([ [0n, 42] ]); - setAndMergeTxMetadata(txMetadata, label, metadata, true); + mergeTxMetadata(txMetadata, label, metadata, true); expect(txMetadata).toEqual(expectedOutput); }); it("should merge two identical string metadata entries", () => { @@ -22,7 +22,7 @@ describe("Transaction Metadata Merge", () => { const expectedOutput = new Map([ [1n, "Hey!"] ]); - setAndMergeTxMetadata(txMetadata, label, metadata, true); + mergeTxMetadata(txMetadata, label, metadata, true); expect(txMetadata).toEqual(expectedOutput); }); it("should not merge two different numbers", () => { @@ -31,7 +31,7 @@ describe("Transaction Metadata Merge", () => { ]); const label = 1n; const metadata = 43; - expect(() => setAndMergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge 42 with 43"); + expect(() => mergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge 42 with 43"); }); it("should not merge two different strings", () => { const txMetadata = new Map([ @@ -39,7 +39,7 @@ describe("Transaction Metadata Merge", () => { ]); const label = 0n; const metadata = "Bob"; - expect(() => setAndMergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge \"Alice\" with \"Bob\""); + expect(() => mergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge \"Alice\" with \"Bob\""); }); it("should not merge two same values of different types", () => { const txMetadata = new Map([ @@ -47,7 +47,7 @@ describe("Transaction Metadata Merge", () => { ]); const label = 0n; const metadata = "42"; - expect(() => setAndMergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge 42 with \"42\""); + expect(() => mergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge 42 with \"42\""); }); it("should replace with the latest item if there is no merge", () => { const txMetadata = new Map([ @@ -58,7 +58,7 @@ describe("Transaction Metadata Merge", () => { const expectedOutput = new Map([ [1n, 43] ]); - setAndMergeTxMetadata(txMetadata, label, metadata, false); + mergeTxMetadata(txMetadata, label, metadata, false); expect(txMetadata).toEqual(expectedOutput); }); @@ -68,7 +68,7 @@ describe("Transaction Metadata Merge", () => { ]); const label = 721n; const metadata = metadataObjToMap({ version: 2 }); - expect(() => setAndMergeTxMetadata(txMetadata, label, metadata, 2)).toThrow("cannot merge 1 with 2"); + expect(() => mergeTxMetadata(txMetadata, label, metadata, 2)).toThrow("cannot merge 1 with 2"); }); it("should replace with the latest value of the same object key if values are not merged", () => { const txMetadata = new Map([ @@ -79,23 +79,23 @@ describe("Transaction Metadata Merge", () => { const expectedOutput = new Map([ [721n, metadataObjToMap({ version: 2 })] ]); - setAndMergeTxMetadata(txMetadata, label, metadata, 1); + mergeTxMetadata(txMetadata, label, metadata, 1); expect(txMetadata).toEqual(expectedOutput); }); it("should not merge different types", () => { - expect(() => setAndMergeTxMetadata( + expect(() => mergeTxMetadata( new Map([[0n, 0]]), 0n, // label [], // metadata true )).toThrow("cannot merge primitive type with array type"); - expect(() => setAndMergeTxMetadata( + expect(() => mergeTxMetadata( new Map([[0n, metadataObjToMap({})]]), 0n, // label "", // metadata true )).toThrow("cannot merge map type with primitive type"); - expect(() => setAndMergeTxMetadata( + expect(() => mergeTxMetadata( new Map([[0n, metadataObjToMap({})]]), 0n, // label [], // metadata @@ -129,7 +129,7 @@ describe("Transaction Metadata Merge", () => { }) ] ]); - setAndMergeTxMetadata(txMetadata, label, metadata, true); + mergeTxMetadata(txMetadata, label, metadata, true); expect(txMetadata).toEqual(expectedOutput); }); @@ -156,7 +156,7 @@ describe("Transaction Metadata Merge", () => { }) ] ]); - setAndMergeTxMetadata(txMetadata, label, metadata, 2); + mergeTxMetadata(txMetadata, label, metadata, 2); expect(txMetadata).toEqual(expectedOutput); }); @@ -198,7 +198,7 @@ describe("Transaction Metadata Merge", () => { }) ] ]); - setAndMergeTxMetadata(txMetadata, label, metadata, 2); + mergeTxMetadata(txMetadata, label, metadata, 2); expect(txMetadata).toEqual(expectedOutput); }); @@ -252,7 +252,7 @@ describe("Transaction Metadata Merge", () => { }) ] ]); - setAndMergeTxMetadata(txMetadata, label, metadata1, 2); + mergeTxMetadata(txMetadata, label, metadata1, 2); expect(txMetadata).toEqual(expectedOutput1); // Merge more NFT metadata const metadata2 = metadataObjToMap({ @@ -294,7 +294,7 @@ describe("Transaction Metadata Merge", () => { }) ] ]); - setAndMergeTxMetadata(txMetadata, label, metadata2, 2); + mergeTxMetadata(txMetadata, label, metadata2, 2); expect(txMetadata).toEqual(expectedOutput2); }); @@ -327,7 +327,7 @@ describe("Transaction Metadata Merge", () => { }) ] ]); - setAndMergeTxMetadata(txMetadata, label, metadata, 2); + mergeTxMetadata(txMetadata, label, metadata, 2); expect(txMetadata).toEqual(expectedOutput); }); @@ -355,7 +355,7 @@ describe("Transaction Metadata Merge", () => { }) ] ]); - setAndMergeTxMetadata(txMetadata, label, metadata, 2); + mergeTxMetadata(txMetadata, label, metadata, 2); expect(txMetadata).toEqual(expectedOutput); }); @@ -374,7 +374,7 @@ describe("Transaction Metadata Merge", () => { [721n, metadataObjToMap({ policyId1: { NFT1: { name: "line 3" }, NFT2: { name: "line 5" } } })], [1n, metadataObjToMap("line 4")] ]); - setAndMergeTxMetadata(txMetadata, label, metadata, 2); + mergeTxMetadata(txMetadata, label, metadata, 2); expect(txMetadata).toEqual(expectedOutput); }); }); From 964ef452520b2ad9f0bf4fa4dab63872fa5f5e34 Mon Sep 17 00:00:00 2001 From: Dharmveer Bharti Date: Fri, 13 Dec 2024 17:52:10 +0530 Subject: [PATCH 4/6] Fix metadataObjToMap(): case for Map --- packages/mesh-transaction/src/utils/metadata.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/mesh-transaction/src/utils/metadata.ts b/packages/mesh-transaction/src/utils/metadata.ts index 47ca41d1d..ba3e8dff8 100644 --- a/packages/mesh-transaction/src/utils/metadata.ts +++ b/packages/mesh-transaction/src/utils/metadata.ts @@ -17,11 +17,19 @@ export const metadataObjToMap = (metadata: any): Metadatum => { // Recursively process each element in the array return metadata.map(metadataObjToMap); } else if (metadata && typeof metadata === "object") { - // Convert object to MetadatumMap + // Convert to MetadatumMap recursively const map: MetadatumMap = new Map(); - Object.entries(metadata).forEach(([key, value]) => { - map.set(metadataObjToMap(key), metadataObjToMap(value)); - }); + if (metadata instanceof Map) { + // for Map + metadata.forEach((value, key) => { + map.set(metadataObjToMap(key), metadataObjToMap(value)); + }); + } else { + // for Object + Object.entries(metadata).forEach(([key, value]) => { + map.set(metadataObjToMap(key), metadataObjToMap(value)); + }); + } return map; } else { throw new Error("Metadata map conversion: Unsupported metadata type"); From db86991fdf5da480a18c94e54b8c45cc19f3ea61 Mon Sep 17 00:00:00 2001 From: Dharmveer Bharti Date: Fri, 13 Dec 2024 18:50:38 +0530 Subject: [PATCH 5/6] Some cleaning --- .../mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts | 3 --- packages/mesh-transaction/src/transaction/transaction-v2.ts | 3 +-- packages/mesh-transaction/src/utils/metadata.ts | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts index 72b4aae07..a023ba492 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts @@ -26,7 +26,6 @@ import { RefTxIn, TxIn, TxInParameter, - TxMetadata, Unit, UTxO, UtxoSelection, @@ -1418,8 +1417,6 @@ export class MeshTxBuilderCore { * Add metadata to the transaction * @param label The label of the metadata, preferably number * @param metadata The metadata in any format - * @param mergeExistingMetadataByLabel Whether to merge the new metadata - * with any existing metadata under the same label, and upto what level * @returns The MeshTxBuilder instance */ metadataValue = ( diff --git a/packages/mesh-transaction/src/transaction/transaction-v2.ts b/packages/mesh-transaction/src/transaction/transaction-v2.ts index 184f3a819..4be0494e8 100644 --- a/packages/mesh-transaction/src/transaction/transaction-v2.ts +++ b/packages/mesh-transaction/src/transaction/transaction-v2.ts @@ -51,8 +51,7 @@ export interface TransactionV2 { setTimeToStart(slot: string): this; setMetadata( label: number, - metadata: unknown, - mergeExistingMetadataByLabel: MetadataMergeLevel + metadata: unknown ): this; withdrawRewards(rewardAddress: string, lovelace: string): this; delegateStake(rewardAddress: string, poolId: string): this; diff --git a/packages/mesh-transaction/src/utils/metadata.ts b/packages/mesh-transaction/src/utils/metadata.ts index ba3e8dff8..1323f0ebe 100644 --- a/packages/mesh-transaction/src/utils/metadata.ts +++ b/packages/mesh-transaction/src/utils/metadata.ts @@ -1,6 +1,6 @@ import JSONBig from "json-bigint"; -import type { Metadatum, MetadatumMap, TxMetadata } from "@meshsdk/common"; +import type { Metadatum, MetadatumMap } from "@meshsdk/common"; export type MetadataMergeLevel = boolean | number; From 7c74d9f4b6a272f5598655c7ff4f782a2f05c32c Mon Sep 17 00:00:00 2001 From: Dharmveer Bharti Date: Fri, 13 Dec 2024 18:52:06 +0530 Subject: [PATCH 6/6] Update test file for txMetadata: mergeContents --- .../test/transaction/txMetadata.test.ts | 468 +++++++----------- 1 file changed, 182 insertions(+), 286 deletions(-) diff --git a/packages/mesh-transaction/test/transaction/txMetadata.test.ts b/packages/mesh-transaction/test/transaction/txMetadata.test.ts index 39a00ac58..b36029197 100644 --- a/packages/mesh-transaction/test/transaction/txMetadata.test.ts +++ b/packages/mesh-transaction/test/transaction/txMetadata.test.ts @@ -1,180 +1,118 @@ -import { metadataObjToMap, mergeTxMetadata } from "@meshsdk/transaction"; +import { mergeContents, metadataObjToMap } from "@meshsdk/transaction"; describe("Transaction Metadata Merge", () => { it("should merge two identical number metadata entries", () => { - const txMetadata = new Map([ - [0n, 42] - ]); - const label = 0n; - const metadata = 42; - const expectedOutput = new Map([ - [0n, 42] - ]); - mergeTxMetadata(txMetadata, label, metadata, true); - expect(txMetadata).toEqual(expectedOutput); + const currentMetadata = 42; + const newMetadata = 42; + const expectedOutput = 42; + mergeContents(currentMetadata, newMetadata, 1); + expect(currentMetadata).toEqual(expectedOutput); }); it("should merge two identical string metadata entries", () => { - const txMetadata = new Map([ - [1n, "Hey!"] - ]); - const label = 1n; - const metadata = "Hey!"; - const expectedOutput = new Map([ - [1n, "Hey!"] - ]); - mergeTxMetadata(txMetadata, label, metadata, true); - expect(txMetadata).toEqual(expectedOutput); + const currentMetadata = "Hey!"; + const newMetadata = "Hey!"; + const expectedOutput = "Hey!"; + mergeContents(currentMetadata, newMetadata, 1); + expect(currentMetadata).toEqual(expectedOutput); }); it("should not merge two different numbers", () => { - const txMetadata = new Map([ - [1n, 42] - ]); - const label = 1n; - const metadata = 43; - expect(() => mergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge 42 with 43"); + const currentMetadata = 42; + const newMetadata = 43; + expect(() => mergeContents(currentMetadata, newMetadata, 1)).toThrow("cannot merge 42 with 43"); }); it("should not merge two different strings", () => { - const txMetadata = new Map([ - [0n, "Alice"] - ]); - const label = 0n; - const metadata = "Bob"; - expect(() => mergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge \"Alice\" with \"Bob\""); + const currentMetadata = "Alice"; + const newMetadata = "Bob"; + expect(() => mergeContents(currentMetadata, newMetadata, 1)).toThrow("cannot merge \"Alice\" with \"Bob\""); }); it("should not merge two same values of different types", () => { - const txMetadata = new Map([ - [0n, 42] - ]); - const label = 0n; - const metadata = "42"; - expect(() => mergeTxMetadata(txMetadata, label, metadata, true)).toThrow("cannot merge 42 with \"42\""); + const currentMetadata = 42; + const newMetadata = "42"; + expect(() => mergeContents(currentMetadata, newMetadata, 1)).toThrow("cannot merge 42 with \"42\""); }); - it("should replace with the latest item if there is no merge", () => { - const txMetadata = new Map([ - [1n, 42] - ]); - const label = 1n; - const metadata = 43; - const expectedOutput = new Map([ - [1n, 43] - ]); - mergeTxMetadata(txMetadata, label, metadata, false); - expect(txMetadata).toEqual(expectedOutput); + it("should return the latest item if there is no merge", () => { + const currentMetadata = 42; + const newMetadata = 43; + const expectedOutput = 43; + // `currentMetadata` remains unchanged here + expect(mergeContents(currentMetadata, newMetadata, 0)).toEqual(expectedOutput); }); it("should not merge two different values of the same object key", () => { - const txMetadata = new Map([ - [721n, metadataObjToMap({ version: 1 })] - ]); - const label = 721n; - const metadata = metadataObjToMap({ version: 2 }); - expect(() => mergeTxMetadata(txMetadata, label, metadata, 2)).toThrow("cannot merge 1 with 2"); + const currentMetadata = metadataObjToMap({ version: 1 }); + const newMetadata = metadataObjToMap({ version: 2 }); + expect(() => mergeContents(currentMetadata, newMetadata, 2)).toThrow("cannot merge 1 with 2"); }); it("should replace with the latest value of the same object key if values are not merged", () => { - const txMetadata = new Map([ - [721n, metadataObjToMap({ version: 1 })] - ]); - const label = 721n; - const metadata = metadataObjToMap({ version: 2 }); - const expectedOutput = new Map([ - [721n, metadataObjToMap({ version: 2 })] - ]); - mergeTxMetadata(txMetadata, label, metadata, 1); - expect(txMetadata).toEqual(expectedOutput); + const currentMetadata = metadataObjToMap({ version: 1 }); + const newMetadata = metadataObjToMap({ version: 2 }); + const expectedOutput = metadataObjToMap({ version: 2 }); + mergeContents(currentMetadata, newMetadata, 1); + expect(currentMetadata).toEqual(expectedOutput); }); it("should not merge different types", () => { - expect(() => mergeTxMetadata( - new Map([[0n, 0]]), - 0n, // label - [], // metadata - true + expect(() => mergeContents( + metadataObjToMap(0), + metadataObjToMap([]), + 1 )).toThrow("cannot merge primitive type with array type"); - expect(() => mergeTxMetadata( - new Map([[0n, metadataObjToMap({})]]), - 0n, // label - "", // metadata - true + expect(() => mergeContents( + metadataObjToMap({}), + metadataObjToMap(""), + 1 )).toThrow("cannot merge map type with primitive type"); - expect(() => mergeTxMetadata( - new Map([[0n, metadataObjToMap({})]]), - 0n, // label - [], // metadata - true + expect(() => mergeContents( + metadataObjToMap({}), + metadataObjToMap([]), + 1 )).toThrow("cannot merge map type with array type"); }); it("plain object to map conversion should not allow nullish values", () => { + expect(() => metadataObjToMap(null)).toThrow("Unsupported metadata type"); expect(() => metadataObjToMap({ "value": null })).toThrow("Unsupported metadata type"); }); - it("should replace 674 standard msg array for default merge depth", () => { - const txMetadata = new Map([ - [ - 674n, - metadataObjToMap({ - msg: ["A", "B", "C"], - msg2: ["X", "Y", "Z"] - }) - ] - ]); - const label = 674n; - const metadata = metadataObjToMap({ + it("should replace 674 standard msg array for merge depth 1", () => { + const currentMetadata = metadataObjToMap({ + msg: ["A", "B", "C"], + msg2: ["X", "Y", "Z"] + }); + const newMetadata = metadataObjToMap({ msg: ["D", "E", "F"] }); - const expectedOutput = new Map([ - [ - 674n, - metadataObjToMap({ - msg: ["D", "E", "F"], - msg2: ["X", "Y", "Z"] - }) - ] - ]); - mergeTxMetadata(txMetadata, label, metadata, true); - expect(txMetadata).toEqual(expectedOutput); + const expectedOutput = metadataObjToMap({ + msg: ["D", "E", "F"], + msg2: ["X", "Y", "Z"] + }); + mergeContents(currentMetadata, newMetadata, 1); + expect(currentMetadata).toEqual(expectedOutput); }); it("should concatenate 674 standard msg arrays for merge depth 2", () => { - const txMetadata = new Map([ - [ - 674n, - metadataObjToMap({ - msg: ["A", "B", "C"], - msg2: ["X", "Y", "Z"] - }) - ] - ]); - const label = 674n; - const metadata = metadataObjToMap({ + const currentMetadata = metadataObjToMap({ + msg: ["A", "B", "C"], + msg2: ["X", "Y", "Z"] + }); + const newMetadata = metadataObjToMap({ msg: ["D", "E", "F"] }); - const expectedOutput = new Map([ - [ - 674n, - metadataObjToMap({ - msg: ["A", "B", "C", "D", "E", "F"], - msg2: ["X", "Y", "Z"] - }) - ] - ]); - mergeTxMetadata(txMetadata, label, metadata, 2); - expect(txMetadata).toEqual(expectedOutput); + const expectedOutput = metadataObjToMap({ + msg: ["A", "B", "C", "D", "E", "F"], + msg2: ["X", "Y", "Z"] + }); + mergeContents(currentMetadata, newMetadata, 2); + expect(currentMetadata).toEqual(expectedOutput); }); it("should merge multiple CIP-25 NFTs metadata under the same policy id", () => { - const txMetadata = new Map([ - [ - 721n, - metadataObjToMap({ - "policyId1": { - "My NFT 1": { - "name": "My NFT 1" - } - } - }) - ] - ]); - const label = 721n; - const metadata = metadataObjToMap({ + const currentMetadata = metadataObjToMap({ + "policyId1": { + "My NFT 1": { + "name": "My NFT 1" + } + } + }); + const newMetadata = metadataObjToMap({ "policyId1": { "My NFT 2": { "name": "My NFT 2", @@ -182,44 +120,33 @@ describe("Transaction Metadata Merge", () => { } } }); - const expectedOutput = new Map([ - [ - 721n, - metadataObjToMap({ - "policyId1": { - "My NFT 1": { - "name": "My NFT 1" - }, - "My NFT 2": { - "name": "My NFT 2", - "description": "My second NFT" - } - } - }) - ] - ]); - mergeTxMetadata(txMetadata, label, metadata, 2); - expect(txMetadata).toEqual(expectedOutput); + const expectedOutput = metadataObjToMap({ + "policyId1": { + "My NFT 1": { + "name": "My NFT 1" + }, + "My NFT 2": { + "name": "My NFT 2", + "description": "My second NFT" + } + } + }); + mergeContents(currentMetadata, newMetadata, 2); + expect(currentMetadata).toEqual(expectedOutput); }); it("should merge multiple CIP-25 NFTs metadata under different policy ids", () => { - const txMetadata = new Map([ - [ - 721n, - metadataObjToMap({ - "policyId1": { - "My NFT 1": { - "name": "My NFT 1", - "files": [ - { name: "NFT 1 Image", src: "xyz", mediaType: "image/jpeg" } - ] - } - } - }) - ] - ]); - const label = 721n; - const metadata1 = metadataObjToMap({ + const currentMetadata = metadataObjToMap({ + "policyId1": { + "My NFT 1": { + "name": "My NFT 1", + "files": [ + { name: "NFT 1 Image", src: "xyz", mediaType: "image/jpeg" } + ] + } + } + }); + const newMetadata1 = metadataObjToMap({ "policyId2": { "My NFT 1": { "name": "My NFT 1 Policy 2", @@ -229,33 +156,28 @@ describe("Transaction Metadata Merge", () => { } } }); - const expectedOutput1 = new Map([ - [ - 721n, - metadataObjToMap({ - "policyId1": { - "My NFT 1": { - "name": "My NFT 1", - "files": [ - { name: "NFT 1 Image", src: "xyz", mediaType: "image/jpeg" } - ] - } - }, - "policyId2": { - "My NFT 1": { - "name": "My NFT 1 Policy 2", - "files": [ - { name: "NFT 1 P 2", src: "abc", mediaType: "image/png" } - ] - } - } - }) - ] - ]); - mergeTxMetadata(txMetadata, label, metadata1, 2); - expect(txMetadata).toEqual(expectedOutput1); + const expectedOutput1 = metadataObjToMap({ + "policyId1": { + "My NFT 1": { + "name": "My NFT 1", + "files": [ + { name: "NFT 1 Image", src: "xyz", mediaType: "image/jpeg" } + ] + } + }, + "policyId2": { + "My NFT 1": { + "name": "My NFT 1 Policy 2", + "files": [ + { name: "NFT 1 P 2", src: "abc", mediaType: "image/png" } + ] + } + } + }); + mergeContents(currentMetadata, newMetadata1, 2); + expect(currentMetadata).toEqual(expectedOutput1); // Merge more NFT metadata - const metadata2 = metadataObjToMap({ + const newMetadata2 = metadataObjToMap({ "policyId1": { "My NFT 2": { "name": "My NFT 2", @@ -265,116 +187,90 @@ describe("Transaction Metadata Merge", () => { } } }); - const expectedOutput2 = new Map([ - [ - 721n, - metadataObjToMap({ - "policyId1": { - "My NFT 1": { - "name": "My NFT 1", - "files": [ - { name: "NFT 1 Image", src: "xyz", mediaType: "image/jpeg" } - ] - }, - "My NFT 2": { - "name": "My NFT 2", - "files": [ - { name: "NFT 2 Image", src: "pqr", mediaType: "image/jpeg" } - ] - } - }, - "policyId2": { - "My NFT 1": { - "name": "My NFT 1 Policy 2", - "files": [ - { name: "NFT 1 P 2", src: "abc", mediaType: "image/png" } - ] - } - } - }) - ] - ]); - mergeTxMetadata(txMetadata, label, metadata2, 2); - expect(txMetadata).toEqual(expectedOutput2); + const expectedOutput2 = metadataObjToMap({ + "policyId1": { + "My NFT 1": { + "name": "My NFT 1", + "files": [ + { name: "NFT 1 Image", src: "xyz", mediaType: "image/jpeg" } + ] + }, + "My NFT 2": { + "name": "My NFT 2", + "files": [ + { name: "NFT 2 Image", src: "pqr", mediaType: "image/jpeg" } + ] + } + }, + "policyId2": { + "My NFT 1": { + "name": "My NFT 1 Policy 2", + "files": [ + { name: "NFT 1 P 2", src: "abc", mediaType: "image/png" } + ] + } + } + }); + mergeContents(currentMetadata, newMetadata2, 2); + expect(currentMetadata).toEqual(expectedOutput2); }); it("should replace with the latest CIP-25 NFT metadata of the same policy and asset id", () => { - const txMetadata = new Map([ - [ - 721n, - metadataObjToMap({ - "policyId1": { - "My NFT 1": { name: "NFT 1 Name", files: [{ name: "NFT Image" }] }, // old metadata here - "My NFT 2": { name: "NFT 2 Name" } - } - }) - ] - ]); - const label = 721n; - const metadata = metadataObjToMap({ + const currentMetadata = metadataObjToMap({ + "policyId1": { + "My NFT 1": { name: "NFT 1 Name", files: [{ name: "NFT Image" }] }, // old metadata here + "My NFT 2": { name: "NFT 2 Name" } + } + }); + const newMetadata = metadataObjToMap({ "policyId1": { "My NFT 1": { name: "Latest NFT 1", image: "xyz", description: "Latest NFT here" } } }); - const expectedOutput = new Map([ - [ - 721n, - metadataObjToMap({ - "policyId1": { - "My NFT 1": { name: "Latest NFT 1", image: "xyz", description: "Latest NFT here" }, - "My NFT 2": { name: "NFT 2 Name" } - } - }) - ] - ]); - mergeTxMetadata(txMetadata, label, metadata, 2); - expect(txMetadata).toEqual(expectedOutput); + const expectedOutput = metadataObjToMap({ + "policyId1": { + "My NFT 1": { name: "Latest NFT 1", image: "xyz", description: "Latest NFT here" }, + "My NFT 2": { name: "NFT 2 Name" } + } + }); + mergeContents(currentMetadata, newMetadata, 2); + expect(currentMetadata).toEqual(expectedOutput); }); it("should attach version to CIP-25 metadata", () => { - const txMetadata = new Map([ - [ - 721n, - metadataObjToMap({ - "policyId1": { "My NFT 1": { name: "My NFT 1" } }, - "policyId2": { "My NFT 1": { name: "My NFT 1 Policy 2" } } - }) - ] - ]); - const label = 721n; - const metadata = metadataObjToMap({ + const currentMetadata = metadataObjToMap({ + "policyId1": { "My NFT 1": { name: "My NFT 1" } }, + "policyId2": { "My NFT 1": { name: "My NFT 1 Policy 2" } } + }); + const newMetadata = metadataObjToMap({ version: 1 }); - const expectedOutput = new Map([ - [ - 721n, - metadataObjToMap({ - "policyId1": { "My NFT 1": { name: "My NFT 1" } }, - "policyId2": { "My NFT 1": { name: "My NFT 1 Policy 2" } }, - "version": 1 - }) - ] - ]); - mergeTxMetadata(txMetadata, label, metadata, 2); - expect(txMetadata).toEqual(expectedOutput); + const expectedOutput = metadataObjToMap({ + "policyId1": { "My NFT 1": { name: "My NFT 1" } }, + "policyId2": { "My NFT 1": { name: "My NFT 1 Policy 2" } }, + "version": 1 + }); + mergeContents(currentMetadata, newMetadata, 2); + expect(currentMetadata).toEqual(expectedOutput); }); it("should preserve metadata entries with other tags in the original order", () => { - const txMetadata = new Map([ + const currentMetadata = new Map([ [0n, metadataObjToMap("line 1")], [674n, metadataObjToMap({ msg: "line 2" })], [721n, metadataObjToMap({ policyId1: { NFT1: { name: "line 3" } } })], [1n, metadataObjToMap("line 4")] ]); - const label = 721n; - const metadata = metadataObjToMap({ policyId1: { NFT2: { name: "line 5" } } }); + const newMetadata = new Map([ + [721n, metadataObjToMap({ policyId1: { NFT2: { name: "line 5" } } })] + ]); const expectedOutput = new Map([ [0n, metadataObjToMap("line 1")], [674n, metadataObjToMap({ msg: "line 2" })], [721n, metadataObjToMap({ policyId1: { NFT1: { name: "line 3" }, NFT2: { name: "line 5" } } })], [1n, metadataObjToMap("line 4")] ]); - mergeTxMetadata(txMetadata, label, metadata, 2); - expect(txMetadata).toEqual(expectedOutput); + mergeContents(currentMetadata, newMetadata, 3); + expect(currentMetadata).toEqual(expectedOutput); }); });