Interact with Contract

Interact with Contract

The 4th and last step is to deploy your contract and interact with it on the real blockchain (devnet / testnet / mainnet). xSuite makes deploying and interacting super simple.

The code for interacting with the real blockchain is nearly the same as the code for testing your contract and interacting with the simulnet. Instead of using a LSWorld object, you will use a World object. That's the only difference.

xSuite also provides an envChain utility to easily interact with the different networks (devent / testnet / mainnet).

Your first interaction

Interactions can be written using any JavaScript CLI library. As we prefer commander (opens in a new tab), we are going to teach using this one.

First, install commander in your contract with npm install commander. You can then add the scripts:

"interact:devnet": "CHAIN=devnet tsx interact/index.ts",
"interact:testnet": "CHAIN=testnet tsx interact/index.ts",
"interact:mainnet": "CHAIN=mainnet tsx interact/index.ts"

in the package.json of your contract. This allows you to interact with devnet / testnet / mainnet with the commands:

npm run interact:devnet <ACTION>
npm run interact:testnet <ACTION>
npm run interact:mainnet <ACTION>

interact/data.json

Create the file interact/data.json that will contain all the data about your contract in the different networks. As a start, you can put:

{
  "code": "file:output/contract.wasm",
  "address": {
    "devnet": "",
    "testnet": "",
    "mainnet": ""
  }
}

The code field would contain the path to the contract WASM code and the address field would contain the address of the contract on devnet, testnet and mainnet. The envChain utility will select the correct address to use depending on the value of the CHAIN env variable.

interact/index.ts

Create the file interact/index.ts that will contains the interactions. As a start, you can put:

import { Command } from "commander";
import { envChain, World } from "xsuite";
import data from "./data.json";
 
const world = World.new({
  chainId: envChain.id(),
});
 
const loadWallet = () => world.newWalletFromFile("wallet.json");
 
const program = new Command();
 
program.command("deploy").action(async () => {
  const wallet = await loadWallet();
  const result = await wallet.deployContract({
    code: data.code,
    codeMetadata: ["upgradeable"],
    gasLimit: 20_000_000,
  });
  console.log("Result:", result);
});
 
program.command("upgrade").action(async () => {
  const wallet = await loadWallet();
  const result = await wallet.upgradeContract({
    callee: envChain.select(data.address),
    code: data.code,
    codeMetadata: ["upgradeable"],
    gasLimit: 20_000_000,
  });
  console.log("Result:", result);
});
 
program.parse(process.argv);

The commands can then be run for devnet like follows:

npm run interact:devnet deploy
npm run interact:devnet upgrade

Let's break the file down and explain it.

Imports

You should have the following imports at the top of the interact/index.ts file:

import { Command } from "commander";
import { envChain, World } from "xsuite";
import data from "./data.json";

We use the Command class from commander to allow us to write nice NodeJs CLI commands.

envChain is the utility we talked about previously. World is for interacting with the real blockchain (devnet / testnet / mainnet), similar to LSWorld from tests for interacting with the simulnet.

We then import the data.json file and will use the e and d utilities from xsuite to encode/decode data respectively to a format that the MultiversX blockchain understands. More info on the Data Encoding page

World, wallet & program

Next, you should have the following variables:

const world = World.new({
  chainId: envChain.id(),
});
 
const loadWallet = () => world.newWalletFromFile("wallet.json");
 
const program = new Command();

Here we define the world object using World.new() which takes the proxyUrl, chainId and gasPrice as arguments. Using the envChain utility we can easily get the appropriate publicProxyUrl (i.e. https://devnet-gateway.multiversx.com/ (opens in a new tab) for Devnet) and chain id depending on the current CHAIN environment variable value that is set in the scripts section of our package.json file.

We then declare the function loadWallet to load a wallet from a .json file using the world.newWalletFromFile() function. The reason we don't call this directly is that we will get a password prompt after running this function, and we only want to trigger that after commander handles our custom command.

Finally, we initialize the program variable with an instance of the Command class.

Basic commands

Finally, we can write our first basic commands:

program.command("deploy").action(async () => {
  const wallet = await loadWallet();
  const result = await wallet.deployContract({
    code: data.code,
    codeMetadata: ["upgradeable"],
    gasLimit: 20_000_000,
    codeArgs: [e.Str("ourString")],
  });
  console.log("Result:", result);
});
 
program.command("upgrade").action(async () => {
  const wallet = await loadWallet();
  const result = await wallet.upgradeContract({
    callee: envChain.select(data.address),
    code: data.code,
    codeMetadata: ["upgradeable"],
    gasLimit: 20_000_000,
    codeArgs: [e.Str("ourString")],
  });
  console.log("Result:", result);
});
 
program.parse(process.argv);

Here we declare two CLI commands, deploy and upgrade to easily manage our contract.

  1. In the deploy command, we first call loadWallet to get a new instance of Wallet, which represents a wallet on the MultiversX blockchain. When calling this function, we will be prompted for the keystore file's password.

    Then we will deploy the contract code that comes from the data.json file's code key calling wallet.deployContract() function. Here we also pass the codeMetadata and make the contract upgradeable, specify the gasLimit and any codeArgs which will be encoded using the e utility.

  2. The upgrade is very similar to the deploy command. Here we use the envChain.select() function to get the correct address of our contract from the data.json file depending on the environment (devnet, testnet, mainnet) the command runs on (which comes from the CHAIN environmental variable).

Advanced interactions

If you want to create more advanced commands, with advanced options or arguments, it is highly recommendeded to check out the commander.js (opens in a new tab) documentation. However, below we will detail some examples on how to write more advanced interactions.

Suppose you want an interaction that takes some arguments, to easily create new resources. You could do something like this:

program
  .command("createOffer")
  .argument("token", "The id of the token")
  .argument("amount", "The amount to send")
  .action(async (token: string, amount: number) => {
    const wallet = await loadWallet();
 
    const result = await wallet.callContract({
      callee: envChain.select(data.address),
      gasLimit: 10_000_000,
      funcName: "createOffer",
      funcArgs: [
        e.Str(token),
        e.U64(BigInt(amount)),
      ],
    });
    console.log(result);
  });

You can call the above command like this for devnet:

npm run interact:devnet createOffer TOKEN-123456 1000

It will execute the transaction with token being TOKEN-123456 and amount being 1000. You can execute this command with different arguments to quickly create multiple resources.

Of course you are also not limited to calling only one callContract function or query function per command, or even deploying only one contract using deployContract. You can call the functions multiple times to setup more advanced contracts, like for example if you need to deploy 2 contract which depend on each other:

program
  .command("deploy")
  .argument("[decimals]", "The number of decimals with a default of 18", 18)
  .action(async (decimals: number) => {
    const wallet = await loadWallet();
 
    const result = await wallet.deployContract({
      code: data.code,
      codeMetadata: ["upgradeable"],
      gasLimit: 100_000_000,
      codeArgs: [
        e.U8(BigInt(decimals))
      ]
    });
 
    // Setup some data on the first contract
    const args: any[] = envChain.select(data.pairs);
    for (let [tokenIn, tokenOut] of args) {
      await wallet.callContract({
        callee: result.address,
        gasLimit: 20_000_000,
        funcName: "addPair",
        funcArgs: [
          e.Str(tokenIn),
          e.Str(tokenOut),
        ],
      });
    }
 
    // Deploy another contract that depends on the first one
    const resultOther = await wallet.deployContract({
      code: data.otherCode,
      codeMetadata: ["upgradeable"],
      gasLimit: 100_000_000,
      funcArgs: [
        e.Addr(result.address),
      ],
    });
 
    console.log("Contract Address:", result.address);
    console.log("Other Contract Address", resultOther.address);
  });

You can call the above command like this for devnet:

npm run interact:devnet deploy

The decimals argument is optional, and will have a default of 18 if it is not specified.

The data.json file for the above example could look something like this:

{
  "code": "file:contract/output/contract.wasm",
  "otherCode": "file:otherContract/output/other-contract.wasm",
  "address": {
    "devnet": "",
    "testnet": "",
    "mainnet": ""
  },
  "otherAddress": {
    "devnet": "",
    "testnet": "",
    "mainnet": ""
  },
  "pairs": {
    "devnet": [
      ["WEGLD-d7c6bb", "USDC-8d4068"],
      ["WEGLD-d7c6bb", "ASH-4ce444"]
    ],
    "testnet": [],
    "mainnet": [
      ["WEGLD-bd4d79", "USDC-c76f1f"],
      ["WEGLD-bd4d79", "ASH-a642d1"]
    ]
  }
}

You can populate the address and otherAddress fields with the correct addresses of the contracts after the deploy command was run for an environment. Then you can use the envChain.select() utility in an upgrade command for example to upgrade both contracts.