Mastering smart contract deployment with MultiversX JavaScript SDK
- Intro to MultiversX blockchain interactions with JavaScript SDK
- Transfer tokens using MultiversX JavaScript SDK
- Mastering smart contract deployment with MultiversX JavaScript SDK
- Step-by-step guide to MultiversX smart contract interactions with JavaScript SDK
- Creating NFTs with MultiversX Blockchain Using 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();
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");
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
});
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);
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}`
);
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 ...>
}
]
}
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.
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.
Check the video. Please subscribe on X, GitHub, and YouTube. Thanks.