DEV Community

Cover image for Mastering smart contract deployment with MultiversX JavaScript SDK
Julian.io
Julian.io

Posted on • Originally published at julian.io

Mastering smart contract deployment with MultiversX JavaScript SDK

In the third article and video, I would like to focus on the MultiversX JavaScript/TypeScript SDK in the context of smart contract deployments. As in previous articles, we will go through the whole script step by step, explaining each SDK tool.

First, let's see the whole script and go through each important part:

import { promises } from "node:fs";
import {
 TransactionComputer,
 TransactionsFactoryConfig,
 SmartContractTransactionsFactory,
 Code,
 Address,
 TransactionWatcher,
 SmartContractTransactionsOutcomeParser,
 TransactionsConverter,
} from "@multiversx/sdk-core";
import {
 syncAndGetAccount,
 senderAddress,
 getSigner,
 apiNetworkProvider,
} from "./setup.js";

const deploySmartContract = async () => {
 const user = await syncAndGetAccount();
 const computer = new TransactionComputer();
 const signer = await getSigner();

 // Load smart contract code
 // For source code check: https://github.com/xdevguild/piggy-bank-sc/tree/master
 const codeBuffer = await promises.readFile("./piggybank.wasm");
 const code = Code.fromBuffer(codeBuffer);

 // Load ABI file (not required for now, but will be useful when interacting with the SC)
 // Although it would be helpful if we had initial arguments to pass
 const abiFile = await promises.readFile("./piggybank.abi.json", "UTF-8");

 // Prepare transfer transactions factory
 const factoryConfig = new TransactionsFactoryConfig({ chainID: "D" });
 let scFactory = new SmartContractTransactionsFactory({
 config: factoryConfig,
 abi: abiFile,
 });

 // Prepare deploy transaction
 const deployTransaction = scFactory.createTransactionForDeploy({
 sender: new Address(senderAddress),
 bytecode: code.valueOf(),
 gasLimit: 10000000n,
 arguments: [], // Pass arguments for init function on SC, we don't have any on this smart contract
 // Below ones are optional with default values
 nativeTransferAmount: 0, // Sometimes you need to send EGLD to the init function on SC
 isUpgradeable: true, // You will be able to upgrade the contract
    isReadable: false, // You will be able to read its state through another contract
    isPayable: false, // You will be able to send funds to it
    isPayableBySmartContract: false, // Only smart contract can send funds to it
 });

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

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

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

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

  // You can compute the smart contract address before broadcasting the transaction
  // https://docs.multiversx.com/sdk-and-tools/sdk-js/sdk-js-cookbook-v13#computing-the-contract-address
  // But let's see how to get it from the network after deployment

  console.log("Pending...");

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

  // Now let's parse the results with TransactionsConverter and SmartContractTransactionsOutcomeParser
  const converter = new TransactionsConverter();
  const parser = new SmartContractTransactionsOutcomeParser();
  const transactionOutcome =
    converter.transactionOnNetworkToOutcome(transactionOnNetwork);
  const parsedOutcome = parser.parseDeploy({ transactionOutcome });

  console.log(
    `Smart Contract deployed. Here it is:\nhttps://devnet-explorer.multiversx.com/accounts/${parsedOutcome.contracts[0].address}\n\nCheck the transaction in the Explorer:\nhttps://devnet-explorer.multiversx.com/transactions/${txHash}`
 );
};

deploySmartContract();
Enter fullscreen mode Exit fullscreen mode

As you probably have already noticed, the structure is very similar to that of the previous scripts. We use the same helpers from the setup.js file, so I won't focus on them here. Check the first article for more info about them. The preparation to broadcast is also similar. There is one new thing, but we will get to it.

What is important in this demo is that I need a smart contract. This is why I included the WASM source code and the ABI file in the repository. The smart contract is a simple piggy bank functionality, and you can find the source code in the xDevGuild GitHub repository: Piggy Bank Smart Contract. The functionality is simple but not important in this context. Let's focus on the deployment.

After downloading the source code (in the same place as the script file), we need to read and include it in our script. We can do this with Node file system utilities. We also need to use Code from MultiversX SDK to prepare the proper format.

// Load smart contract code
// For source code check: https://github.com/xdevguild/piggy-bank-sc/tree/master
const codeBuffer = await promises.readFile("./piggybank.wasm");
const code = Code.fromBuffer(codeBuffer);

// Load ABI file (not required for now, but will be useful when interacting with the SC)
// Although it would be helpful if we had initial arguments to pass
const abiFile = await promises.readFile("./piggybank.abi.json", "UTF-8");
Enter fullscreen mode Exit fullscreen mode

Next, we need to prepare the core setup. The configuration uses TransactionsFactoryConfig (similar to previous ones) and the SmartContractTransactionsFactory. The factory is similar to others but specific to smart contract operations. After that, we can use the createTransactionForDeploy from our factory and configure the deployment transaction. Let's stop for a moment, but first, let's see that part of the code to clarify it.

// Prepare transfer transactions factory
const factoryConfig = new TransactionsFactoryConfig({ chainID: "D" });
let scFactory = new SmartContractTransactionsFactory({
  config: factoryConfig,
  abi: abiFile,
});

// Prepare deploy transaction
const deployTransaction = scFactory.createTransactionForDeploy({
  sender: new Address(senderAddress),
  bytecode: code.valueOf(),
  gasLimit: 10000000n,
  arguments: [], // Pass arguments for init function on SC, we don't have any on this smart contract
  // Below ones are optional with default values
  nativeTransferAmount: 0, // Sometimes you need to send EGLD to the init function on SC
  isUpgradeable: true, // You will be able to upgrade the contract
  isReadable: false, // You will be able to read its state through another contract
  isPayable: false, // You will be able to send funds to it
  isPayableBySmartContract: false, // Only smart contract can send funds to it
});
Enter fullscreen mode Exit fullscreen mode

When configuring the deployment transaction, you have a couple of options. Of course, the most important is to provide the binary source code of the smart contract, but you can also do a couple of other things.

Each smart contract has an init function, which is triggered when the contract is deployed. This function could be useful in many ways, mostly for initial storage configuration. Of course, you can pass arguments to it. This is why we have the arguments array when configuring the transaction. In our case, the Piggy Bank doesn't require initial arguments, but you would need that in many cases. You can pass plain data to the array when using ABI. It should be handled properly, but you can also use data helpers from MultiversX SDK. You'll find them, for example, here: mx-sdk-js-core typesystem. So, in short words, you can, for example, import U32Value from MultiversX SDK and then use it like: arguments: [new U32Value(123)]. But don't worry about it when you have the ABI. Then it should also work like arguments: [123]. Of course, the order of arguments is important.

Okay, what next? Sometimes, you need to provide a payment for the init function. For example, your smart contract could have logic that requires locking some EGLD amount on initialization. It is why we have the nativeTransferAmount. You can pass it there.

We also have some 'flags' that will help to configure our smart contract and its future behavior. You can define your contract as upgradable with isUpgradable. You can define if your contract can be payable by anyone by isPayable. You can limit the payable functionality only to allow a smart contract with isPayableBySmartContract. Finally, you can define if your smart contract can be readable by other smart contracts using isReadable.

Okay, let's move on. After we configure our deployment transaction, we need to prepare some standard steps, as with all transactions. So we need to increment the nonce, serialize the transaction, sign it, and broadcast it.

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

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

// Sign the transaction with out signer
deployTransaction.signature = await signer.sign(serializedDeployTransaction);

// Broadcast the transaction
const txHash = await apiNetworkProvider.sendTransaction(deployTransaction);
Enter fullscreen mode Exit fullscreen mode

In this case, we need to get the smart contract address. We can compute it before we send the transaction, but we want to be sure that the deployment transaction went through and that the smart contract was deployed. We will get the address from the transaction outcome. We can do this by using a couple of tools. These operations are more general, not only in the context of smart contracts, so you can use them for any transaction.

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

// Now let's parse the results with TransactionsConverter and SmartContractTransactionsOutcomeParser
const converter = new TransactionsConverter();
const parser = new SmartContractTransactionsOutcomeParser();
const transactionOutcome =
  converter.transactionOnNetworkToOutcome(transactionOnNetwork);
const parsedOutcome = parser.parseDeploy({ transactionOutcome });

console.log(
  `Smart Contract deployed. Here it is:\nhttps://devnet-explorer.multiversx.com/accounts/${parsedOutcome.contracts[0].address}\n\nCheck the transaction in the Explorer:\nhttps://devnet-explorer.multiversx.com/transactions/${txHash}`
);
Enter fullscreen mode Exit fullscreen mode

The TransactionWatcher will wait and get the transaction results on the chain. We must also prepare a converter and parser using tools from the MultiversX SDK. With that, we can pass the transactionOnNetwork and parse the outcome to get the address.

The parsedOutome in this case has such a structure:

{
  returnCode: 'ok',
  returnMessage: 'ok',
  contracts: [
    {
      address: 'erd1qqqqqq...',
      ownerAddress: 'erd1...',
      codeHash: <Buffer ...>
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Summary

That's it. We have a full deployment script. The smart contract has been deployed to the devnet chain and is ready to work with. I'll put together an article and video that show how to interact with such a smart contract.

Follow me on X (@theJulianIo) and YouTube (@julian_io) or GitHub for more MultiversX magic.

Please check the tools I maintain: the Elven Family and Buildo.dev. With Buildo, you can do a lot of management operations using a nice web UI. You can issue fungible tokens, non-fungible tokens. You can also do other operations, like multi-transfers or claiming developer rewards. There is much more.

Walkthrough video

The demo code

Top comments (0)