Cookbook (v13)
This page will guide you through the process of handling common tasks using sdk-js v13 (latest, stable version).
This cookbook makes use of sdk-js v13
. In order to migrate from sdk-js v12.x
to sdk-js v13
, please also follow the migration guide.
Creating network providers
Creating an API provider:
import { ApiNetworkProvider } from "@multiversx/sdk-core";
const apiNetworkProvider = new ApiNetworkProvider("https://devnet-api.multiversx.com", { clientName: "multiversx-your-client-name" });
Creating a Proxy provider:
import { ProxyNetworkProvider } from "@multiversx/sdk-core";
const proxyNetworkProvider = new ProxyNetworkProvider("https://devnet-gateway.multiversx.com", { clientName: "multiversx-your-client-name" });
Use the classes from @multiversx/sdk-core/out/networkProviders
only as a starting point.
As your dApp matures, make sure you switch to using your own network provider, tailored to your requirements
(whether deriving from the default ones or writing a new one, from scratch) that directly interacts with the MultiversX API (or Gateway).
On this topic, please see extending sdk-js.
Fetching network parameters
const networkConfig = await apiNetworkProvider.getNetworkConfig();
console.log(networkConfig.MinGasPrice);
console.log(networkConfig.ChainID);
Working with accounts
Synchronizing an account object
The following snippet fetches (from the Network) the nonce and the balance of an account, and updates the local representation of the account.
import { Account } from "@multiversx/sdk-core";
const alice = new Account(addressOfAlice);
const aliceOnNetwork = await apiNetworkProvider.getAccount(addressOfAlice);
alice.update(aliceOnNetwork);
console.log("Nonce:", alice.nonce);
console.log("Balance:", alice.balance.toString());
Managing the sender nonce locally
When sending a bunch of transactions, you usually have to first fetch the account nonce from the network (see above), then manage it locally (e.g. increment upon signing & broadcasting a transaction):
alice.incrementNonce();
console.log("Nonce:", alice.nonce);
Since sdk-core v13
, the Transaction
class exhibits its state as public read-write properties. For example, you can access and set the nonce
property, instead of using getNonce
and setNonce
.
If you are using sdk-core v13
or later, use tx.nonce =
to apply the nonce to a transaction.
For sdk-core v12
or earlier, use the legacy tx.setNonce()
to apply the nonce to a transaction.
notYetSignedTx.nonce = alice.getNonceThenIncrement();
For further reference, please see nonce management.
Broadcasting transactions
Preparing a simple transaction
Since sdk-core v13
, the Transaction
class exhibits its state as public read-write properties. For example, you can access and set the nonce
property, instead of using getNonce
and setNonce
.
import { Transaction } from "@multiversx/sdk-core";
const tx = new Transaction({
data: Buffer.from("food for cats"),
gasLimit: 70000n,
sender: addressOfAlice.toBech32(),
receiver: addressOfBob.toBech32(),
value: 1000000000000000000n,
chainID: "D"
});
tx.nonce = 42n;
Signing a transaction
Note that the transactions must be signed before being broadcasted. On the front-end, signing can be achieved using a signing provider. On this purpose, we recommend using sdk-dapp instead of integrating the signing providers on your own.
For the sake of simplicity, in this section we'll use a UserSigner
object to sign the transaction.
In real-world dApps, transactions are signed by end-users using their wallet, through a signing provider.
import { TransactionComputer, UserSigner } from "@multiversx/sdk-core";
import { promises } from "fs";
const fileContent = await promises.readFile("../testwallets/alice.json", { encoding: "utf8" });
const walletObject = JSON.parse(fileContent);
const signer = UserSigner.fromWallet(walletObject, "password");
const computer = new TransactionComputer();
const serializedTx = computer.computeBytesForSigning(tx);
tx.signature = await signer.sign(serializedTx);
Broadcast using a network provider
In order to broadcast a transaction, use a network provider:
const txHash = await apiNetworkProvider.sendTransaction(readyToBroadcastTx);
console.log("TX hash:", txHash);
Wait for transaction completion
import { TransactionWatcher } from "@multiversx/sdk-core";
const watcherUsingApi = new TransactionWatcher(apiNetworkProvider);
const transactionOnNetworkUsingApi = await watcherUsingApi.awaitCompleted(txHash);
If, instead, you use a ProxyNetworkProvider
to instantiate the TransactionWatcher
, you'll need to patch the getTransaction
method,
so that it instructs the network provider to fetch the so-called processing status, as well (required by the watcher to detect transaction completion).
const watcherUsingProxy = new TransactionWatcher({
getTransaction: async (hash) => {
return await proxyNetworkProvider.getTransaction(hash, true);
}
});
const transactionOnNetworkUsingProxy = await watcherUsingProxy.awaitCompleted(txHash);
In order to wait for multiple transactions:
await Promise.all([
watcherUsingApi.awaitCompleted(txHash1),
watcherUsingApi.awaitCompleted(txHash2),
watcherUsingApi.awaitCompleted(txHash3)
]);
In some circumstances, when awaiting for a transaction completion in order to retrieve its logs and events, it's possible that these pieces of information are missing at the very moment the transaction is marked as completed - they may not be immediately available.
If that is an issue, you can configure the TransactionWatcher
to have additional patience
before returning the transaction object. Below, we're adding a patience of 8 seconds:
const watcherWithPatience = new TransactionWatcher(apiNetworkProvider, { patienceMilliseconds: 8000 });
Alternatively, use TransactionWatcher.awaitAnyEvent()
or TransactionWatcher.awaitOnCondition()
to customize the waiting strategy.
For a different awaiting strategy, also see extending sdk-js.
Token transfers
Generally speaking, in order to create transactions that transfer native tokens or ESDT tokens, one should use the TransferTransactionsFactory
class.
In sdk-core v13
, the TransferTransactionsFactory
class was extended with new methods,
to be aligned with the SDKs specs.
The old, legacy methods are still available (see below), thus existing client code isn't affected.
In sdk-core v13
, the TokenTransfer
class has changed, in a non-breaking manner.
Though, from now on, it should only be used for preparing ESDT token transfers, not native EGLD transfers.
A TokenTransfer
object can still be instantiated using the legacy methods, e.g. fungibleFromAmount
, nonFungible
(which are still available),
but we recommend using the new approach instead (which, among others, makes abstraction of the number of decimals a token has).
For formatting or parsing token amounts, see formatting and parsing amounts.
First, let's create a TransferTransactionsFactory
:
import { Token, TokenTransfer, TransactionsFactoryConfig, TransferTransactionsFactory } from "@multiversx/sdk-core";
// The new approach of creating a "TransferTransactionsFactory":
const factoryConfig = new TransactionsFactoryConfig({ chainID: "D" });
const factory = new TransferTransactionsFactory({ config: factoryConfig });
Now, we can use the factory to create transfer transactions.
EGLD transfers (value movements)
const tx1 = factory.createTransactionForNativeTokenTransfer({
sender: addressOfAlice,
receiver: addressOfBob,
// 1 EGLD
nativeAmount: BigInt("1000000000000000000")
});
tx1.nonce = 42n;
Single ESDT transfer
const tx2 = factory.createTransactionForESDTTokenTransfer({
sender: addressOfAlice,
receiver: addressOfBob,
tokenTransfers: [
new TokenTransfer({
token: new Token({ identifier: "TEST-8b028f" }),
amount: 10000n
})
]
});
tx2.nonce = 43n;
Single NFT transfer
const tx3 = factory.createTransactionForESDTTokenTransfer({
sender: addressOfAlice,
receiver: addressOfBob,
tokenTransfers: [
new TokenTransfer({
token: new Token({ identifier: "TEST-38f249", nonce: 1n }),
amount: 1n
})
]
});
tx3.nonce = 44n;
Single SFT transfer
const tx4 = factory.createTransactionForESDTTokenTransfer({
sender: addressOfAlice,
receiver: addressOfBob,
tokenTransfers: [
new TokenTransfer({
token: new Token({ identifier: "SEMI-9efd0f", nonce: 1n }),
amount: 5n
})
]
});
tx4.nonce = 45n;
Multi ESDT / NFT transfer
const tx5 = factory.createTransactionForESDTTokenTransfer({
sender: addressOfAlice,
receiver: addressOfBob,
tokenTransfers: [
new TokenTransfer({
token: new Token({ identifier: "TEST-8b028f" }),
amount: 10000n
}),
new TokenTransfer({
token: new Token({ identifier: "TEST-38f249", nonce: 1n }),
amount: 1n
}),
new TokenTransfer({
token: new Token({ identifier: "SEMI-9efd0f", nonce: 1n }),
amount: 5n
})
]
});
tx5.nonce = 46n;
Formatting and parsing amounts
For formatting or parsing token amounts as numbers (with fixed number of decimals), please do not rely on sdk-core
. Instead, use sdk-dapp
(higher level) or bignumber.js
(lower level).
You can format amounts using formatAmount
from sdk-dapp
:
import { formatAmount } from '@multiversx/sdk-dapp/utils/operations';
console.log("Format using sdk-dapp:", formatAmount({
input: "1500000000000000000",
decimals: 18,
digits: 4
}));
Or directly using bignumber.js
:
import BigNumber from "bignumber.js";
BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_FLOOR });
console.log("Format using bignumber.js:", new BigNumber("1500000000000000000").shiftedBy(-18).toFixed(4));
You can parse amounts using parseAmount
from sdk-dapp
:
import { formatAmount } from '@multiversx/sdk-dapp/utils/operations';
console.log("Parse using sdk-dapp:", parseAmount("1.5", 18));
Or directly using bignumber.js
:
console.log("Parse using bignumber.js:", new BigNumber("1.5").shiftedBy(18).decimalPlaces(0).toFixed(0));
Contract ABIs
A contract's ABI describes the endpoints, data structure and events that a contract exposes. While contract interactions are possible without the ABI, they are easier to implement when the definitions are available.
Load the ABI from a file
import { AbiRegistry } from "@multiversx/sdk-core";
import { promises } from "fs";
let abiJson = await promises.readFile("../contracts/adder.abi.json", { encoding: "utf8" });
let abiObj = JSON.parse(abiJson);
let abi = AbiRegistry.create(abiObj);
Load the ABI from an URL
import axios from "axios";
const response = await axios.get("https://github.com/multiversx/mx-sdk-js-core/raw/main/src/testdata/adder.abi.json");
abi = AbiRegistry.create(response.data);
Manually construct the ABI
If an ABI file isn't directly available, but you do have knowledge of the contract's endpoints and types, you can manually construct the ABI. Let's see a simple example:
abi = AbiRegistry.create({
"endpoints": [{
"name": "add",
"inputs": [],
"outputs": []
}]
});
An endpoint with both inputs and outputs:
abi = AbiRegistry.create({
"endpoints": [
{
"name": "foo",
"inputs": [
{ "type": "BigUint" },
{ "type": "u32" },
{ "type": "Address" }
],
"outputs": [
{ "type": "u32" }
]
},
{
"name": "bar",
"inputs": [
{ "type": "counted-variadic<utf-8 string>" },
{ "type": "variadic<u64>" }
],
"outputs": []
}
]
});
Contract deployments
Load the bytecode from a file
import { Code } from "@multiversx/sdk-core";
const codeBuffer = await promises.readFile("../contracts/adder.wasm");
const code = Code.fromBuffer(codeBuffer);
Perform a contract deployment
In sdk-core v13
, the recommended way to create transactions for deploying
(and, for that matter, upgrading and interacting with)
smart contracts is through a SmartContractTransactionsFactory
.
The older (legacy) approach, using the method SmartContract.deploy()
, is still available, however.
At some point in the future, SmartContract.deploy()
will be deprecated and removed.
Now, let's create a SmartContractTransactionsFactory
:
import { SmartContractTransactionsFactory, TransactionsFactoryConfig } from "@multiversx/sdk-core";
const factoryConfig = new TransactionsFactoryConfig({ chainID: "D" });
let factory = new SmartContractTransactionsFactory({
config: factoryConfig
});
If the contract ABI is available, provide it to the factory:
factory = new SmartContractTransactionsFactory({
config: factoryConfig,
abi: abi
});
Now, prepare the deploy transaction:
import { U32Value } from "@multiversx/sdk-core";
// For deploy arguments, use "TypedValue" objects if you haven't provided an ABI to the factory:
let args = [new U32Value(42)];
// Or use simple, plain JavaScript values and objects if you have provided an ABI to the factory:
args = [42];
const deployTransaction = factory.createTransactionForDeploy({
sender: addressOfAlice,
bytecode: code.valueOf(),
gasLimit: 6000000n,
arguments: args
});
When creating transactions using SmartContractTransactionsFactory
, even if the ABI is available and provided,
you can still use TypedValue
objects as arguments for deployments and interactions.
Even further, you can use a mix of TypedValue
objects and plain JavaScript values and objects. For example:
let args = [new U32Value(42), "hello", { foo: "bar" }, new TokenIdentifierValue("TEST-abcdef")];
Then, as previously seen, set the transaction nonce (the account nonce must be synchronized beforehand).
deployTransaction.nonce = deployer.getNonceThenIncrement();
Now, sign the transaction using a wallet / signing provider of your choice.
For the sake of simplicity, in this section we'll use a UserSigner
object to sign the transaction.
In real-world dApps, transactions are signed by end-users using their wallet, through a signing provider.
const fileContent = await promises.readFile("../testwallets/alice.json", { encoding: "utf8" });
const walletObject = JSON.parse(fileContent);
const signer = UserSigner.fromWallet(walletObject, "password");
const computer = new TransactionComputer();
const serializedTx = computer.computeBytesForSigning(deployTransaction);
deployTransaction.signature = await signer.sign(serializedTx);
Then, broadcast the transaction and await its completion, as seen in the section broadcasting transactions:
const txHash = await apiNetworkProvider.sendTransaction(deployTransaction);
const transactionOnNetwork = await new TransactionWatcher(apiNetworkProvider).awaitCompleted(txHash);
Computing the contract address
Even before broadcasting, at the moment you know the sender address and the nonce for your deployment transaction, you can (deterministically) compute the (upcoming) address of the contract:
import { AddressComputer } from "@multiversx/sdk-core";
const addressComputer = new AddressComputer();
const contractAddress = addressComputer.computeContractAddress(
Address.fromBech32(deployTransaction.sender),
deployTransaction.nonce
);
console.log("Contract address:", contractAddress.bech32());
Parsing transaction outcome
In the end, you can parse the results using a SmartContractTransactionsOutcomeParser
.
However, since the parseDeploy
method requires a TransactionOutcome
object as input,
we need to first convert our TransactionOnNetwork
object to a TransactionOutcome
, by means of a TransactionsConverter
.
import { SmartContractTransactionsOutcomeParser, TransactionsConverter } from "@multiversx/sdk-core";
const converter = new TransactionsConverter();
const parser = new SmartContractTransactionsOutcomeParser();
const transactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork);
const parsedOutcome = parser.parseDeploy({ transactionOutcome });
console.log(parsedOutcome);
Contract interactions
In sdk-core v13
, the recommended way to create transactions for calling
(and, for that matter, deploying and upgrading)
smart contracts is through a SmartContractTransactionsFactory
.
The older (legacy) approaches, using SmartContract.call()
, SmartContract.methods.myFunction()
, SmartContract.methodsExplicit.myFunction()
and
new Interaction(contract, "myFunction", args)
are still available.
However, at some point in the (more distant) future, they will be deprecated and removed.
Now, let's create a SmartContractTransactionsFactory
:
import { SmartContractTransactionsFactory, TransactionsFactoryConfig } from "@multiversx/sdk-core";
const factoryConfig = new TransactionsFactoryConfig({ chainID: "D" });
let factory = new SmartContractTransactionsFactory({
config: factoryConfig
});
If the contract ABI is available, provide it to the factory:
factory = new SmartContractTransactionsFactory({
config: factoryConfig,
abi: abi
});
Regular interactions
Now, let's prepare a contract transaction, to call the add
function of our
previously deployed smart contract:
import { U32Value } from "@multiversx/sdk-core";
// For arguments, use "TypedValue" objects if you haven't provided an ABI to the factory:
let args = [new U32Value(42)];
// Or use simple, plain JavaScript values and objects if you have provided an ABI to the factory:
args = [42];
const transaction = factory.createTransactionForExecute({
sender: addressOfAlice,
contract: Address.fromBech32("erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60"),
function: "add",
gasLimit: 5000000,
arguments: args
});
When creating transactions using SmartContractTransactionsFactory
, even if the ABI is available and provided,
you can still use TypedValue
objects as arguments for deployments and interactions.
Even further, you can use a mix of TypedValue
objects and plain JavaScript values and objects. For example:
let args = [new U32Value(42), "hello", { foo: "bar" }, new TokenIdentifierValue("TEST-abcdef")];
Then, as previously seen, set the transaction nonce (the account nonce must be synchronized beforehand).
transaction.nonce = alice.getNonceThenIncrement();
Now, sign the transaction using a wallet / signing provider of your choice.
For the sake of simplicity, in this section we'll use a UserSigner
object to sign the transaction.
In real-world dApps, transactions are signed by end-users using their wallet, through a signing provider.
const fileContent = await promises.readFile("../testwallets/alice.json", { encoding: "utf8" });
const walletObject = JSON.parse(fileContent);
const signer = UserSigner.fromWallet(walletObject, "password");
const computer = new TransactionComputer();
const serializedTx = computer.computeBytesForSigning(transaction);
transaction.signature = await signer.sign(serializedTx);
Then, broadcast the transaction and await its completion, as seen in the section broadcasting transactions:
const txHash = await apiNetworkProvider.sendTransaction(transaction);
const transactionOnNetwork = await new TransactionWatcher(apiNetworkProvider).awaitCompleted(txHash);
Transfer & execute
At times, you may want to send some tokens (native EGLD or ESDT) along with the contract call.
For transfer & execute with native EGLD, prepare your transaction as follows:
const transactionWithNativeTransfer = factory.createTransactionForExecute({
sender: addressOfAlice,
contract: Address.fromBech32("erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60"),
function: "add",
gasLimit: 5000000,
arguments: args,
nativeTransferAmount: 1000000000000000000n
});
Above, we're sending 1 EGLD along with the contract call.
For transfer & execute with ESDT tokens, prepare your transaction as follows:
const transactionWithTokenTransfer = factory.createTransactionForExecute({
sender: addressOfAlice,
contract: Address.fromBech32("erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60"),
function: "add",
gasLimit: 5000000,
arguments: args,
tokenTransfers: [
new TokenTransfer({
token: new Token({ identifier: "UTK-14d57d" }),
amount: 42000000000000000000n
})
]
});
Or, for transferring multiple tokens (NFTs included):
const transactionWithMultipleTokenTransfers = factory.createTransactionForExecute({
sender: addressOfAlice,
contract: Address.fromBech32("erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60"),
function: "add",
gasLimit: 5000000,
arguments: args,
tokenTransfers: [
new TokenTransfer({
token: new Token({ identifier: "UTK-14d57d" }),
amount: 42000000000000000000n
}),
new TokenTransfer({
token: new Token({ identifier: "EXAMPLE-453bec", nonce: 3n }),
amount: 1n
})
]
});
Above, we've prepared the TokenTransfer
objects as seen in the section token transfers.
Parsing transaction outcome
Once a transaction is completed, you can parse the results using a SmartContractTransactionsOutcomeParser
.
However, since the parseExecute
method requires a TransactionOutcome
object as input,
we need to first convert our TransactionOnNetwork
object to a TransactionOutcome
, by means of a TransactionsConverter
.
import { SmartContractTransactionsOutcomeParser, TransactionsConverter } from "@multiversx/sdk-core";
const converter = new TransactionsConverter();
const parser = new SmartContractTransactionsOutcomeParser({
abi: abi
});
const transactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork);
const parsedOutcome = parser.parseExecute({ transactionOutcome });
console.log(parsedOutcome);
Decode transaction events
Additionally, you might be interested into decoding the events emitted by a contract.
You can do so by means of the TransactionEventsParser
.
Suppose we'd like to decode a startPerformAction
event emitted by the multisig contract.
Let's fetch a previously-processed transaction,
to serve as an example, and convert it to a TransactionOutcome
(see above why):
const transactionOnNetworkMultisig = await apiNetworkProvider.getTransaction("05d445cdd145ecb20374844dcc67f0b1e370b9aa28a47492402bc1a150c2bab4");
const transactionOutcomeMultisig = converter.transactionOnNetworkToOutcome(transactionOnNetworkMultisig);
Now, let's find and parse the event we are interested in:
import { TransactionEventsParser, findEventsByFirstTopic } from "@multiversx/sdk-core";
const abiJsonMultisig = await promises.readFile("../contracts/multisig-full.abi.json", { encoding: "utf8" });
const abiMultisig = AbiRegistry.create(JSON.parse(abiJsonMultisig));
const eventsParser = new TransactionEventsParser({
abi: abiMultisig
});
const [event] = findEventsByFirstTopic(transactionOutcomeMultisig, "startPerformAction");
const parsedEvent = eventsParser.parseEvent({ event });
console.log(parsedEvent);
Contract queries
In order to perform Smart Contract queries, we recommend the use of SmartContractQueriesController
.
The legacy approaches that rely on SmartContract.createQuery()
or Interaction.buildQuery()
are still available, but they will be deprecated in the (distant) future.
You will notice that the SmartContractQueriesController
requires a QueryRunner
object at initialization.
A NetworkProvider
, slightly adapted, is used to satisfy this requirement.
import { QueryRunnerAdapter, SmartContractQueriesController } from "@multiversx/sdk-core";
const queryRunner = new QueryRunnerAdapter({
networkProvider: apiNetworkProvider
});
let controller = new SmartContractQueriesController({
queryRunner: queryRunner
});
If the contract ABI is available, provide it to the controller:
controller = new SmartContractQueriesController({
queryRunner: queryRunner,
abi: abi
});
Let's create a query object:
const query = controller.createQuery({
contract: "erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60",
function: "getSum",
arguments: [],
});
Then, run the query against the network. You will get a SmartContractQueryResponse
object.
const response = await controller.runQuery(query);
The invocation of controller.runQuery()
ultimately calls the VM query endpoints of the MultiversX REST API.
The response object contains the raw output of the query, which can be parsed as follows:
const [sum] = controller.parseQueryResponse(response);
console.log(sum);
Explicit decoding / encoding of values
When needed, you can use the BinaryCodec
to decode and encode values manually,
leveraging contract ABIs:
const abiJsonExample = await promises.readFile("../contracts/example.abi.json", { encoding: "utf8" });
const abiExample = AbiRegistry.create(JSON.parse(abiJsonExample));
const abiJsonMultisig = await promises.readFile("../contracts/multisig-full.abi.json", { encoding: "utf8" });
const abiMultisig = AbiRegistry.create(JSON.parse(abiJsonMultisig));
The ABI files used within this cookbook are available here.
Decoding a custom type
Example of decoding a custom type (a structure) called DepositEvent
from binary data:
import { BinaryCodec } from "@multiversx/sdk-core";
const depositCustomType = abiExample.getCustomType("DepositEvent");
const codec = new BinaryCodec();
let data = Buffer.from("00000000000003db000000", "hex");
let decoded = codec.decodeTopLevel(data, depositCustomType);
let decodedValue = decoded.valueOf();
console.log(JSON.stringify(decodedValue, null, 4));
Example of decoding a custom type (a structure) called Reward
from binary data:
const rewardStructType = abiExample.getStruct("Reward");
data = Buffer.from("010000000445474c440000000201f400000000000003e80000000000000000", "hex");
[decoded] = codec.decodeNested(data, rewardStructType);
decodedValue = decoded.valueOf();
console.log(JSON.stringify(decodedValue, null, 4));
Example of decoding a custom type (an enum) called Action
(of multisig contract) from binary data:
const actionStructType = abiMultisig.getEnum("Action");
data = Buffer.from("0500000000000000000500d006f73c4221216fa679bc559005584c4f1160e569e1000000012a0000000003616464000000010000000107", "hex");
[decoded] = codec.decodeNested(data, actionStructType);
decodedValue = decoded.valueOf();
console.log(JSON.stringify(decodedValue, null, 4));
Encoding a custom type
Example of encoding a custom type (a struct) called EsdtTokenPayment
(of multisig contract) into binary data:
import { BigUIntValue, Field, Struct, TokenIdentifierValue, U64Value } from "@multiversx/sdk-core";
const paymentType = abiMultisig.getStruct("EsdtTokenPayment");
const paymentStruct = new Struct(paymentType, [
new Field(new TokenIdentifierValue("TEST-8b028f"), "token_identifier"),
new Field(new U64Value(0n), "token_nonce"),
new Field(new BigUIntValue(10000n), "amount")
]);
const encoded = codec.encodeNested(paymentStruct);
console.log(encoded.toString("hex"));
Signing objects and verifying signatures
Skip this section if you're building a dApp. This section is destined for developers of wallet-like applications or backend (server-side) components that are concerned with signing transactions and messages.
For dApps, use the available signing providers instead. Note that we recommend using sdk-dapp instead of integrating the signing providers on your own.
You might also be interested into the language-agnostic overview on signing transactions.
Signing objects
Creating a UserSigner
from a JSON wallet:
import { UserSigner } from "@multiversx/sdk-core";
import { promises } from "fs";
const fileContent = await promises.readFile("../testwallets/alice.json", { encoding: "utf8" });
const walletObject = JSON.parse(fileContent);
let signer = UserSigner.fromWallet(walletObject, "password");
Creating a UserSigner
from a PEM file:
const pemText = await promises.readFile("../testwallets/alice.pem", { encoding: "utf8" });
signer = UserSigner.fromPem(pemText);
Signing a transaction, as we've seen before:
import { Transaction, TransactionComputer } from "@multiversx/sdk-core";
const transaction = new Transaction({
nonce: 91,
sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
receiver: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
value: 1000000000000000000n,
gasLimit: 50000n,
chainID: "D"
});
const transactionComputer = new TransactionComputer()
let serializedTransaction = transactionComputer.computeBytesForSigning(transaction);
transaction.signature = await signer.sign(serializedTransaction);
console.log("Signature", Buffer.from(transaction.signature).toString("hex"));
Signing an arbitrary message:
import { Message, MessageComputer } from "@multiversx/sdk-core";
let message = new Message({
data: Buffer.from("hello")
});
const messageComputer = new MessageComputer();
let serializedMessage = messageComputer.computeBytesForSigning(message);
message.signature = await signer.sign(serializedMessage);
console.log("Signature", Buffer.from(message.signature).toString("hex"));
Verifying signatures
Creating a UserVerifier
:
import { UserVerifier } from "@multiversx/sdk-core";
const aliceVerifier = UserVerifier.fromAddress(addressOfAlice);
const bobVerifier = UserVerifier.fromAddress(addressOfBob);
Verifying a signature:
serializedTransaction = transactionComputer.computeBytesForVerifying(transaction);
serializedMessage = messageComputer.computeBytesForVerifying(message);
console.log("Is signature of Alice?", aliceVerifier.verify(serializedTransaction, transaction.signature));
console.log("Is signature of Alice?", aliceVerifier.verify(serializedMessage, message.signature));
console.log("Is signature of Bob?", bobVerifier.verify(serializedTransaction, transaction.signature));
console.log("Is signature of Bob?", bobVerifier.verify(serializedMessage, message.signature));
Handling messages over boundaries
Generally speaking, signed Message
objects are meant to be sent to a remote party (e.g. a service), which can then verify the signature.
In order to prepare a message for transmission, you can use the MessageComputer.packMessage()
utility method:
const packedMessage = messageComputer.packMessage(message);
console.log("Packed message", packedMessage);
Then, on the receiving side, you can use MessageComputer.unpackMessage()
to reconstruct the message, prior verification:
const unpackedMessage = messageComputer.unpackMessage(packedMessage);
const serializedUnpackedMessage = messageComputer.computeBytesForVerifying(unpackedMessage);
console.log("Unpacked message", unpackedMessage);
console.log("Is signature of Alice?", aliceVerifier.verify(serializedUnpackedMessage, message.signature));
Signing hashes of objects
Under the hood, MessageComputer.computeBytesForSigning()
does not compute a plain serialization of the message.
Instead, it first decorates the message (with a special prefix, plus the message length), and computes a keccak256
hash of this decorated variant.
Ultimately, the signature is computed over the hash.
However, for transactions, by default, the Network expects the signature to be computed over the plain serialization of the transaction.
The function TransactionComputer.computeBytesForSigning()
adheres to this default policy.
The behavior can be overridden by setting the sign using hash flag of transaction.options
:
transactionComputer.applyOptionsForHashSigning(transaction);
Then, the transaction should be serialized and signed as follows:
const bytesToSign = transactionComputer.computeHashForSigning(transaction);
transaction.signature = await signer.sign(bytesToSign);
If you'd like to learn more about hash signing, please refer to the overview on signing transactions.