Creating NFTs with MultiversX Blockchain Using JavaScript SDK

github x github

In the fifth article in the series, let's focus on token management—specifically, non-fungible token management. We will issue the collection, set roles, and create an NFT using some scripts and the MultiversX JavaScript SDK. Let's jump in.

The codebase for this one is bigger, so I will only paste some of it here. You can check the full working example in the GitHub repository. Let's discuss three main functions that we will trigger in exactly that order: issueNftCollection - issue a collection token setSpecialRolesForNft - set special roles for the collection createNft - create an actual NFT Each function reuses the same code, so there are also helper functions. Let's start with the first one.

Issuing the collection token

The collection token is an umbrella for all the NFTs you want to create within it. It can have its unique properties and roles. I won't focus on that much because it is all in the docs. Let's see how we can issue such a token.

Here is what our function will look like:

const issueNftCollection = () => {
  const factory = getTransactionFactory();

  const transaction = factory.createTransactionForIssuingNonFungible({
    canAddSpecialRoles: true,
    canChangeOwner: true,
    canFreeze: true,
    canPause: true,
    canTransferNFTCreateRole: true,
    canUpgrade: true,
    canWipe: true,
    sender: new Address(senderAddress),
    tokenName: collectionName,
    tokenTicker: collectionTicker,
 });

  processTransaction(transaction, "issue");
};

Let's go step by step, including the getTransactionFactory and processTransaction helper functions. The first one is responsible for creating the transaction factory, which is the same for all three functions, so it will always be used in the same way. Let's see what it looks like:

import { TransactionsConverter, TokenManagementTransactionsOutcomeParser } from '@multiversx/sdk-core';
(...)
const getTransactionFactory = () => {
  const factoryConfig = new TransactionsFactoryConfig({ chainID: "D" });
  const factory = new TokenManagementTransactionsFactory({
    config: factoryConfig,
 });
  return factory;
};

It's quite straightforward and similar to the previous operations in previous articles. We use TransactionsFactoryConfig and then pass it to the special factory responsible for token management (TokenManagementTransactionsFactory). This is what we will use to build all three of our transactions for issuing, setting roles, and creating the NFT.

With the transaction factory, we can start using different methods responsible for token management. What we need here is createTransactionForIssuingNonFungible. As you can see above, we can use it and provide all required information, like collection properties (read more about them in the docs), token name, and ticker.

When we have the transaction data prepared, we need to do a couple of things, like serialization for signing, signing, and broadcasting the transaction. It is the same for all three transactions. It is why we have another helper function: processTransaction, and we will take a closer look at it now:

import {
  TransactionComputer,
} from "@multiversx/sdk-core";
import {
  syncAndGetAccount,
  getSigner,
  apiNetworkProvider,
} from "./setup.js";

(...)

const processTransaction = async (transaction, type) => {
  const user = await syncAndGetAccount();
  const computer = new TransactionComputer();
  const signer = await getSigner();

  // Increase the nonce
  transaction.nonce = user.getNonceThenIncrement();

  // Serialize the transaction for signing
  const serializedTransaction = computer.computeBytesForSigning(transaction);

  // Sign the transaction with our signer
  transaction.signature = await signer.sign(serializedTransaction);

  // Broadcast the transaction
  const txHash = await apiNetworkProvider.sendTransaction(transaction);

  await parseOutcome(txHash, type);

  console.log(
    "Check in the Explorer: ",
    `https://devnet-explorer.multiversx.com/transactions/${txHash}`
 );
};

These operations are almost the same as in previous articles because they are required for every transaction. So, we use our user, signer, and computer to sign and broadcast the transaction. The user and signer are prepared in the setup.js file and imported here. Check the first article for more information about them. You will also find the file in the repository.

To demonstrate how to parse the outcome from such a transaction, we have another helper function called parseOutcome, and let's jump to its code right away:

import {
  TokenManagementTransactionsOutcomeParser,
  TransactionsConverter,
  TransactionWatcher,
} from "@multiversx/sdk-core";

(...)

const parseOutcome = async (txHash, type) => {
  console.log("Pending...");

  // Get the transaction on network, we need to wait for results here, we use TransactionWatcher for that
  const transactionOnNetwork = await new TransactionWatcher(
    apiNetworkProvider
 ).awaitCompleted(txHash);

  // Parse the outcome
  const converter = new TransactionsConverter();
  const parser = new TokenManagementTransactionsOutcomeParser();
  const transactionOutcome =
    converter.transactionOnNetworkToOutcome(transactionOnNetwork);

  let parsedOutcome;

  if (type === "issue") {
    parsedOutcome = parser.parseIssueNonFungible(transactionOutcome);

    console.log(`Token identifier: ${parsedOutcome?.[0]?.tokenIdentifier}`);
 }
  if (type === "create") {
    parsedOutcome = parser.parseNftCreate(transactionOutcome);

    console.log(
      `Token identifier: ${parsedOutcome?.[0]?.tokenIdentifier} Nonce: ${parsedOutcome?.[0]?.nonce}`
 );
 }
};

It takes the transaction hash and operation type (because they are all used in three different functions, and we want to differentiate the result). Then it uses a specific parser (TokenManagementTransactionsOutcomeParser). It is all a wrapper for the outcome result that needs to be parsed. Otherwise, you would need to do this by hand, which could be more complex. The output of this helper function is to get the token ID and, in the case of NFT creation, also the nonce, which differentiates the NFTs. Here, we only log them to the console.

And that's it. You've issued the NFT collection token.

Set special roles

Next, we need to add special roles. At least an NFTCreate role is required to create NFTs, but there is much more. You can find more information about the roles in the documentation. For now, let's see the second function:

const setSpecialRolesForNft = () => {
  const factory = getTransactionFactory();

  const transaction =
    factory.createTransactionForSettingSpecialRoleOnNonFungibleToken({
      addRoleNFTCreate: true,
      addRoleNFTBurn: true,
      addRoleNFTAddURI: true,
      addRoleNFTUpdateAttributes: true,
      addRoleESDTTransferRole: false,
      sender: new Address(senderAddress),
      tokenIdentifier: collectionId,
      user: new Address(senderAddress), // The owner of the role, in our case, the same as the sender
 });

  processTransaction(transaction);
};

Again, we use the same factory creation helper function. However, the method for building the transaction is different. It is createTransactionForSettingSpecialRoleOnNonFungibleToken (what a long name!). As you can see, you can define all possible roles here. You need to provide the collection ID as the token identifier, and what is important is that you can also provide any address that will get that role. So it can be the address of the collection owner, and usually, it is like that, but it can also be another address/wallet. Processing the transaction is the same as previously.

Creating the NFT

The last function is the actual creation of the NFT, where you need to provide the attributes, URLs for your assets, and other data, such as royalties cut. Here are some important tips, but first, let's see the code.

const createNft = () => {
  const factory = getTransactionFactory();

  const attributes = new TextEncoder().encode(
    "metadata:bafybeihof6n2b4gkahn2nwfhcxe72kvms4o5uevcok5chvg6f2zpfsi6hq/33.json"
 );
  const hash = "";

  const transaction = factory.createTransactionForCreatingNFT({
    // You can read more about attributes in the docs. The data should be in the proper format to be picked up by MultiversX services like Explorer, etc.
    attributes,
    hash, // It can be any hash, for example hash of the attributes, images, etc. (not mandatory)
    initialQuantity: 1n, // It will always be 1 (BigInt) for NFT
    name: "Some NFT name for the token",
    royalties: 500, // number from 0 to 10000 where 10000 is 100%
    sender: new Address(senderAddress),
    tokenIdentifier: collectionId,
    uris: [
      "https://ipfs.io/ipfs/bafybeibimqon4pjm54x27n6we5qohx57gd6n2mnbkxu2r6nejp3nbenk7u/33.png",
      "https://ipfs.io/ipfs/bafybeihof6n2b4gkahn2nwfhcxe72kvms4o5uevcok5chvg6f2zpfsi6hq/33.json",
 ],
 });

  // Very custom gasLimit calculation. Sometimes (like in this case), the built-in gas calculation isn't enough
  // Check docs for more info on how gas limits are calculated
  // Anyway, you can define your own value through transaction.gasLimit
  transaction.gasLimit = BigInt(
    transaction.data.length * 1500 +
 (attributes?.length || 0 + hash?.length || 0) * 50000
 );

  processTransaction(transaction, "create");
};

The MultiversX APIs, Explorer, xSpotlight, and other services will 'prefer' URLs with the default IPFS gateway, as shown in the video and example code. The same goes for the attributes; you can keep there whatever you want, but it is good to keep the metadata information in the form of IPFS CID and file name of the JSON data, like: metadata:bafybeihof6n2b4gkahn2nwfhcxe72kvms4o5uevcok5chvg6f2zpfsi6hq/33.json, plus other data, for example with tags: metadata:bafybeihof6n2b4gkahn2nwfhcxe72kvms4o5uevcok5chvg6f2zpfsi6hq/33.json;tags:tag1. You can also read about it in the documentation.

Anyway, let's get back to our last function. It's the same factory with a different method, which in this case is createTransactionForCreatingNFT. With this, we can provide all the required data, as seen in the example. What is nonstandard here is that I had to overwrite the gas limit to create a transaction. The factory normally handles it, but I needed more than the default calculation for the gas limit. The good part is that you can always overwrite it, like adding a new BigInt value to your transaction object like transaction.gasLimit = <BigUint value>. Read more about the gass calculation in the docs.

Summary

It is not complete code to copy and paste, but we discussed the most crucial parts for creating NFTs with the MultiversX JavaScript SDK. The main steps are issue, roles, and creation. The whole working example is linked below. You'll also find a full walkthrough video with voice-over comments.

As always, feel free to contact me on X or by e-mail. Please check the tools I maintain: the Elven Family and Buildo.dev. With Buildo, you can perform many management operations using a nice web UI. You can issue fungible and non-fungible tokens. You can also perform other operations, like multi-transfers or claiming developer rewards. There is much more.

Check the video. Please subscribe on X, GitHub, and YouTube. Thanks.

Walkthrough video

The demo code