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/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 2194b04d8..bd8062549 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-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..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)
+ .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 e12bb7b02..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);
+ tx.metadataValue(721, metadata);
}
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..c56fa1ce9
--- /dev/null
+++ b/packages/mesh-core-csl/src/core/adaptor/metadata.ts
@@ -0,0 +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)),
+ });
+ });
+ 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/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 3c24a61f7..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
@@ -14,6 +14,7 @@ import {
emptyTxBuilderBody,
LanguageVersion,
MeshTxBuilderBody,
+ Metadatum,
MintItem,
Network,
Output,
@@ -35,6 +36,8 @@ import {
Withdrawal,
} from "@meshsdk/common";
+import { metadataObjToMap } from "../utils";
+
export class MeshTxBuilderCore {
txEvaluationMultiplier = 1.1;
private txOutput?: Output;
@@ -1412,13 +1415,20 @@ 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
* @returns The MeshTxBuilder instance
*/
- metadataValue = (tag: string, metadata: any) => {
- const metadataString = JSONBig.stringify(metadata);
- this.meshTxBuilderBody.metadata.push({ tag, metadata: metadataString });
+ metadataValue = (
+ label: number | bigint | string,
+ metadata: Metadatum | object,
+ ) => {
+ label = BigInt(label);
+ if (typeof metadata === "object" && !(metadata instanceof Map)) {
+ this.meshTxBuilderBody.metadata.set(label, metadataObjToMap(metadata));
+ } else {
+ this.meshTxBuilderBody.metadata.set(label, metadata);
+ }
return this;
};
diff --git a/packages/mesh-transaction/src/transaction/index.ts b/packages/mesh-transaction/src/transaction/index.ts
index 5c1977336..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,6 +34,7 @@ import {
} from "@meshsdk/core-cst";
import { MeshTxBuilder, MeshTxBuilderOptions } from "../mesh-tx-builder";
+import { mergeContents, metadataObjToMap } from "../utils";
export interface TransactionOptions extends MeshTxBuilderOptions {
initiator: IInitiator;
@@ -470,9 +472,22 @@ 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 },
- });
+ 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);
}
@@ -585,13 +600,13 @@ 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 {number} label The label to use for the metadata entry.
+ * @param {unknown} metadata The value to use for the metadata entry.
* @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(label: number, metadata: Metadatum | object): Transaction {
+ this.txBuilder.metadataValue(label, metadata);
return this;
}
diff --git a/packages/mesh-transaction/src/transaction/transaction-v2.ts b/packages/mesh-transaction/src/transaction/transaction-v2.ts
index 0fc7151db..4be0494e8 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,10 @@ 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
+ ): 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/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..1323f0ebe
--- /dev/null
+++ b/packages/mesh-transaction/src/utils/metadata.ts
@@ -0,0 +1,150 @@
+import JSONBig from "json-bigint";
+
+import type { Metadatum, MetadatumMap } 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 to MetadatumMap recursively
+ const map: MetadatumMap = new Map();
+ 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");
+ }
+};
+
+/**
+ * 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
+ */
+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;
+ }
+ }
+ // 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`,
+ );
+};
+
+const getMergeDepth = (mergeOption: MetadataMergeLevel): number => {
+ 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";
+};
+
+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;
+ }
+ }
+ return true;
+};
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..b36029197
--- /dev/null
+++ b/packages/mesh-transaction/test/transaction/txMetadata.test.ts
@@ -0,0 +1,276 @@
+import { mergeContents, metadataObjToMap } from "@meshsdk/transaction";
+
+describe("Transaction Metadata Merge", () => {
+ it("should merge two identical number metadata entries", () => {
+ 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 currentMetadata = "Hey!";
+ const newMetadata = "Hey!";
+ const expectedOutput = "Hey!";
+ mergeContents(currentMetadata, newMetadata, 1);
+ expect(currentMetadata).toEqual(expectedOutput);
+ });
+ it("should not merge two different numbers", () => {
+ 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 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 currentMetadata = 42;
+ const newMetadata = "42";
+ expect(() => mergeContents(currentMetadata, newMetadata, 1)).toThrow("cannot merge 42 with \"42\"");
+ });
+ 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 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 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(() => mergeContents(
+ metadataObjToMap(0),
+ metadataObjToMap([]),
+ 1
+ )).toThrow("cannot merge primitive type with array type");
+ expect(() => mergeContents(
+ metadataObjToMap({}),
+ metadataObjToMap(""),
+ 1
+ )).toThrow("cannot merge map type with primitive type");
+ 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 merge depth 1", () => {
+ const currentMetadata = metadataObjToMap({
+ msg: ["A", "B", "C"],
+ msg2: ["X", "Y", "Z"]
+ });
+ const newMetadata = metadataObjToMap({
+ msg: ["D", "E", "F"]
+ });
+ 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 currentMetadata = metadataObjToMap({
+ msg: ["A", "B", "C"],
+ msg2: ["X", "Y", "Z"]
+ });
+ const newMetadata = metadataObjToMap({
+ msg: ["D", "E", "F"]
+ });
+ 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 currentMetadata = metadataObjToMap({
+ "policyId1": {
+ "My NFT 1": {
+ "name": "My NFT 1"
+ }
+ }
+ });
+ const newMetadata = metadataObjToMap({
+ "policyId1": {
+ "My NFT 2": {
+ "name": "My NFT 2",
+ "description": "My second NFT"
+ }
+ }
+ });
+ 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 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",
+ "files": [
+ { name: "NFT 1 P 2", src: "abc", mediaType: "image/png" }
+ ]
+ }
+ }
+ });
+ 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 newMetadata2 = metadataObjToMap({
+ "policyId1": {
+ "My NFT 2": {
+ "name": "My NFT 2",
+ "files": [
+ { name: "NFT 2 Image", src: "pqr", mediaType: "image/jpeg" }
+ ]
+ }
+ }
+ });
+ 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 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 = 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 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 = 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 currentMetadata = new Map([
+ [0n, metadataObjToMap("line 1")],
+ [674n, metadataObjToMap({ msg: "line 2" })],
+ [721n, metadataObjToMap({ policyId1: { NFT1: { name: "line 3" } } })],
+ [1n, metadataObjToMap("line 4")]
+ ]);
+ 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")]
+ ]);
+ mergeContents(currentMetadata, newMetadata, 3);
+ expect(currentMetadata).toEqual(expectedOutput);
+ });
+});