Building NFT related CLI tool with MultiversX JavaScript SDK

github twitter github

It is the next article in the series about NFTs on the MultiversX blockchain, so the tool we will discuss here is strictly related to the NFTs topic. But bear with me. Here, you will find many examples of how to use JS SDK in a general way, not only for the NFT case.

I decided to dig deeper into the JS SDK SDK to know it better. And as always, the best way of learning is to build something useful.

And why such a tool? I saw that many people who don't have much technical knowledge, or even some developers, found it challenging to mint the NFT on the MultiversX blockchain. I think this is because there is no official option on the mainnet yet. There are a couple of ready services and solutions, but it is sometimes confusing for many, even using them.

Let's start with a quick introduction of the tool, but feel free to skip to the JS SDK part below.

The tool

The tool which will be an example when introducing the JS SDK SDK is elven-tools-cli.

Quickly what the tool is about, and then we will go straight to the code and how it is built. So what it can do?

  • deriving the PEM file from the mnemonic (seed phrase)
  • deploying the minter Smart Contract
  • issuing collection handler - which is required as an anchor for all minted NFTs
  • adding proper roles, for example, the role which allows creating of NFTs
  • setup for couple of different workflows. Check docs for more.

Full walkthrough video (always check if it is up to date with the elven.tools docs):

How it works?

The flow for the tool in its final form should be something similar to the steps below:

  1. User installs the elven-tools-cli from the npm registry
  2. User derives the PEM file. Which will be used to sign all transactions without the need to confirm everything
  3. User deploys the minter contract from the official repository or local file system (after changes in the code). All on the user's behalf without the intervention of any third party.
  4. User becomes the owner of the Smart Contract
  5. User can control the Smart Contract logic through time using elven-tools-cli or any custom way of making the calls to it.

For more documentation check: elven.tools.

What kind of Smart Contract does it deploy?

You'll find the code and description here. Also check out all the endpoints here. The smart contract is deployed from the linked repo, but you can also clone it and upload it from the local file system.

Let's back to JS SDK

So JS SDK SDK is a JavaScript implementation of MultiversX's SDK, which can be used in the Node environment, but also as a browser-based tool. Of course, there are ready-to-use tools that incorporate the JS SDK library, like, for example, sdk-dapp library that helps with auth process and gives a lot of helper tools. Let's focus only on the JS SDK tool for now.

What can the JS SDK tool do?

  1. Allows interacting with the MultiversX blockchain
  2. Allows interacting with the Smart Contracts on the MultiversX blockchain
  3. Provides a lot of cryptographic tools
  4. Provides wallets related tooling
  5. Provides auth tooling
  6. Provides a lot of helpers tools

JS SDK in the elven-tools-cli

Finally, let's go through the code and see how the JS SDK tools were used there.

Quick TL;DR. In such tools, you would always need the way to sign every transaction. It can be done using a Keystore JSON file or a PEM file derived from the mnemonic (seed phrase). When we know how to sign our transaction, we need to decide on which chain we want to operate and which proxy provider we should use. Then we also need to initialize our Account object with our wallet address. Then we need to construct our Smart Contract object with its address and optionally the abi JSON file if it is available. Then you will be able to call the functions on Smart Contract and sign all transactions with a synchronized account.

I will write about all the details in the following chapters.

Working with PEM or Keystore files

If you have your Keystore file, you can sign the transaction. You could restore it from the mnemonic (seed phrase) if you don't have it. For example here, for the devnet. The elven-tools-cli library doesn't use the Keystore for that, but it would be good to describe this path too. So I will use the examples from my other library elven-mint. Below is a simple function that initializes the signer. It will require the Keystore file contents and the password. It uses the UserSigner class from JS SDK.

import { UserSigner } from '@multiversx/sdk-core';

export const prepareUserSigner = (wallet: any, walletPassword: string) => {
  return UserSigner.fromWallet(wallet, walletPassword);
};

You will find an actual example here.

The second option with the same result is to derive the PEM file from the mnemonic (seed phrase). And then create the signer using that file. It is how this was implemented in the elven-tools-cli library. It also has the command for deriving the PEM. It will save it as walletKey.pem. Let's see how it looks:

import { UserSigner } from '@multiversx/sdk-core';

export const prepareUserSigner = (walletPemKey: string) => {
  return UserSigner.fromPem(walletPemKey);
};

You will find an actual example here.

As you can see, we use the same class from the JS SDK library, but we need the PEM file contents here. Previously we had used the fromWallet method. Now it is fromPem.

What is worth mentioning is that there is a simple way to derive the PEM from mnemonic using the mxpy tool. But to be concise with the elven-tools-cli, I also implemented it in the library. Mxpy is awesome, but it is written in Python, so one would need to configure the environment to use it. So here we have a JavaScript implementation. Check the code here.

Important! Do not share your PEM file with anyone anywhere. It includes your private key, so it should be kept secret, especially when working with the mainnet.

Ok, we have our signer. We will use it later to sign all the transactions. Don't worry if you have no idea how it should look like in the actual code, and the examples are unreadable for you. I will go through a simplified example at the end.

Configure the Proxy provider

The next step would be to configure the Proxy provider and synchronize it with the network. We need to decide here on which chain we would like to work. I usually work on the devnet, so this will be my configuration example:

import { ProxyProvider, NetworkConfig } from '@multiversx/sdk-core';

export const getProvider = () => {
  return new ProxyProvider('https://devnet-gateway.multiversx.com', { timeout: 5000 });
};

export const syncProviderConfig = async (provider: IProvider) => {
  return NetworkConfig.getDefault().sync(provider);
};

You will find an actual example here.

As you can see, we use the devnet official gateway. But in real mainnet projects, you would probably want to prepare your own architecture. You can read about it here.

This way, we will have the configuration and we will be in sync with the Network.

Prepare the Account instance

The account instance must properly synchronize the nonces on the network when making the transactions. Don't worry about that for now. I will paste a simple example with all the steps at the end.

Below is an example of when we use the PEM file. We need to parse the user key using the PEM file, and then we would need to get the address from the public key.

import { Account, parseUserKey } from '@multiversx/sdk-core';

export const prepareUserAccount = async (walletPemKey: string) => {
  const userKey = parseUserKey(walletPemKey);
  const address = userKey.generatePublicKey().toAddress();
  return new Account(address);
};

You will find an actual example here.

Of course, you could pass the bech32 address if you have it. In elven-tools-cli, I have only the PEM here, so I used it. The example with the address would be:

import { Account, Address } from '@multiversx/sdk-core';

export const prepareUserAccount = async (address: string) => {
  return new Account(new Address(address));
};

You will find an actual example here.

Prepare the Smart Contract instance

The Smart Contract instance has a lot of valuable methods. In the elven-tools-cli it is used mainly for making transactions and interacting with the Smart Contract.

import { AbiRegistry, SmartContract, Address, SmartContractAbi } from '@multiversx/sdk-core';

export const createSmartContractInstance = (
  abi?: AbiRegistry,
  address?: string
) => {
  const contract = new SmartContract({
    address: address ? new Address(address) : undefined,
    abi:
      abi &&
      new SmartContractAbi(
        abi,
        abi.interfaces.map((iface) => iface.name)
      ),
  });
  return contract;
};

You will find an actual example here.

As you can see, we can pass the address and the abi. If we provide the abi file contents, we will be able to initialize all the custom endpoints of the smart contract and keep them on the Smart Contract instance. So you could use something like:

smartContractInstance.methods.customEndpointOnSmartContract(...);

This method will be an instance of the Interaction.

Anyway, let's back to the Smart Contract instance. What interests us the most is the call and deploy methods. It is how elven-tools-cli deploys the contract:

import {
  Code,
  SmartContract,
  GasLimit,
  CodeMetadata,
  BytesValue,
  U32Value,
  BigUIntValue,
  BooleanValue
} from '@multiversx/sdk-core';

export const getDeployTransaction = (
  code: Code,
  contract: SmartContract,
  gasLimit: number,
  imgBaseCid: string,
  fileExtension: string,
  metadataBaseCid: string,
  numberOfTokens: number,
  tokensLimitPerAddress: number,
  sellingPrice: string,
  royalties?: string,
  tags?: string,
  provenanceHash?: string,
  upgradable = true,
  readable = false,
  payable = false,
  metadataInAssets = false
) => {
  return contract.deploy({
    code,
    codeMetadata: new CodeMetadata(upgradable, readable, payable),
    gasLimit: new GasLimit(gasLimit),
    initArguments: [
      BytesValue.fromUTF8(imgBaseCid.trim()),
      BytesValue.fromUTF8(metadataBaseCid.trim()),
      new U32Value(numberOfTokens),
      new U32Value(tokensLimitPerAddress),
      new BigUIntValue(new BigNumber(Number(royalties) * 100 || 0)),
      new BigUIntValue(Balance.egld(sellingPrice.trim()).valueOf()),
      BytesValue.fromUTF8(fileExtension.trim()),
      BytesValue.fromUTF8(tags?.trim() || ''),
      BytesValue.fromUTF8(provenanceHash?.trim() || ''),
      new BooleanValue(metadataInAssets),
    ],
  });
};

You will find an actual example here.

As you can see, many arguments are correctly prepared using helpers for data types from the JS SDK library.

There is also the codeMetadata field. Which is responsible for setting Smart Contract's allowed actions. Read more about it here.

import {
  ContractFunction,
  SmartContract,
  BytesValue,
  Balance,
  GasLimit
} from '@multiversx/sdk-core';
import { issueTokenFnName } from './config';

export const getIssueTransaction = (
  contract: SmartContract,
  gasLimit: number,
  value: number,
  tokenName: string,
  tokenTicker: string
) => {
  return contract.call({
    func: new ContractFunction(issueTokenFnName),
    args: [BytesValue.fromUTF8(tokenName), BytesValue.fromUTF8(tokenTicker)],
    value: Balance.egld(value),
    gasLimit: new GasLimit(gasLimit),
  });
};

You will find an actual example here.

So we call the Smart Contract's issue function, which is called issueToken. We need to pass the arguments, token name and ticker, gas limit, and value of 0.05 EGLD. It is the cost of issuing the collection handler/token (one-time payment for all NFTs in the collection).

If you are overwhelmed by a collection handler or token and how it is different from an NFT token. Please read about it here and here.

Let's put it all together

We have all pieces explained, but let's see how it looks in an actual code. Excellent and straightforward examples are in the README.md file in the JS SDK repo here. So I won't copy them. Instead, let's look into the elven-tools-cli code. Especially as an example, let's see the issuance of the handler/token for the collection.

The elven-tools-cli library is a standard Node CLI tool. We have a couple of commands which then trigger a programmed function. One of such commands is the elven-tools nft-minter issue-collection-token.

Check the code first: issueCollectionToken function.

Parts of the code are encapsulated into generic functions, for example, the setup function which prepares all that we had described above.

Let's go step by step through the setup function:

  1. It reads the PEM file, the ABI file, and the Smart Contract WASM code.
  2. Then, it creates the Smart Contract instance using the Smart Contract address and Abi contents.
  3. Then, it prepares the Proxy provider and syncs the Network.
  4. It also prepares the Account for the user and syncs it with the provider.
  5. The last step here is to create the signer using the PEM file.

Ok, let's back to the main code.

We can now initialize our transaction with all settings from the setup function. See the getIssueTransaction above. We do something like:

import { getIssueTransa }
const issueCollectionTokenTx = getIssueTransaction(...);

The arguments should be similar to those from the example above.

When we have our transaction instance, we can go and synchronize and increment the nonce with the user account. Then we can sign the transaction using previously instantiated signer in the setup function. It all looks like that:

issueCollectionTokenTx.setNonce(userAccount.nonce);
userAccount.incrementNonce();
signer.sign(issueCollectionTokenTx);

When the transaction is signed, you are free to send it. It is as simple as:

await issueCollectionTokenTx.send(provider);

The provider was also previously created in the setup function. Remember that the setup function comes from elven-tools-cli, not the JS SDK. It is just a wrapper for custom setup stuff.

The last thing would be to wait for the results and react when the transaction is finished. Because we work with the Smart Contract here, we would need to also wait for the Smart Contract results. You can do something like:

await issueCollectionTokenTx.awaitNotarized(provider);
const txHash = issueCollectionTokenTx.getHash();

const scResults = (
  await issueCollectionTokenTx.getAsOnNetwork(provider)
).getSmartContractResults();

That's it. We have the whole flow. The same flow is also in the setLocalRoles function, and probably it will be very similar in most of the tasks which interact with the Smart Contracts.

Summary

It was a quick introduction to elven-tools-cli with main steps when interacting with Smart Contracts on the MultiversX blockchain using JS SDK SDK library.

Hopefully, it will be a little bit helpful, at least for JavaScript developers.

For all non-technical people interested in the tools, I would recommend installing the Node environment and trying to install the elven-tools-cli to play around. All you need to do is to npm install elven-tools -g and then use it according to the documentation in the repository. There are also animated gifs for simplicity and a real example of the configuration file, which is, in fact, optional. Also, check out the elven-nft-minter-sc Smart Contract, which the elven-tools-cli deploys with the deploy command. It isn't as usable as it should be, but this will change soon.

Feel free to contact me on Twitter if you have any questions. You can also subscribe to be up to date with all changes I will make in the coming days. I will also try to keep this article up to date.