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.
-
In the
deploy
command, we first callloadWallet
to get a new instance ofWallet
, 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'scode
key callingwallet.deployContract()
function. Here we also pass thecodeMetadata
and make the contractupgradeable
, specify thegasLimit
and anycodeArgs
which will be encoded using thee
utility. -
The
upgrade
is very similar to thedeploy
command. Here we use theenvChain.select()
function to get the correct address of our contract from thedata.json
file depending on the environment (devnet, testnet, mainnet) the command runs on (which comes from theCHAIN
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.