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.
Instead of using a SWorld
object like in tests, we will use a "real" World
object, that interacts with the real blockchain.
The package xsuite
also provides an envChain
utility, to easily and quickly deploy contracts for the different networks.
Your first interaction
Interactions are CLI commands written using the commander.js (opens in a new tab) library. You should already have the interact/data.json
and interact/index.ts
files created after following the getting started guide. Below we will detail the contents of the files.
interact/data.json
The data.json
file contains information regarding your contracts and can be used with the envChain
utility. Take the following data.json
file:
{
"code": "file:output/contract.wasm",
"address": {
"devnet": "",
"testnet": "",
"mainnet": ""
}
}
In Typescript code we can import the file and use the data under the fields directly. The code
field contains the path to the contract wasm code, but the address
is an object with fields for devnet
, testnet
and mainnet
. These keys will be used by the envChain
utility depending on the CHAIN
env variable (which is set in the package.json
file in the appropriate interact scripts for each environment) to select the address of the contract on that chain.
interact/index.ts
The interact/index.ts
file contains the commands of our CLI:
import { Command } from "commander";
import { envChain, World } from "xsuite";
import data from "./data.json";
const world = World.new({
proxyUrl: envChain.publicProxyUrl(),
chainId: envChain.id(),
gasPrice: 1000000000,
});
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 SWorld
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({
proxyUrl: envChain.publicProxyUrl(),
chainId: envChain.id(),
gasPrice: 1000000000,
});
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 contractupgradable
, 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.