DEV Community

Cover image for How to batch transactions with Ternoa SDK πŸ”—
Victor Salomon
Victor Salomon

Posted on

How to batch transactions with Ternoa SDK πŸ”—

Batching some transactions will make you stop loosing time ⏱. If you look to an alternative into creating transactions one by one on the Ternoa blockchain, you are at the right place. Let's imagine a use-case where you want to create a thousand of NFT. Instead of creating them one by one, you should batch them all into only one single call function. Easy, right ? Lets dive into detail right now! 🀿

This article is not meant to be a full coverage of how to use Ternoa SDK. Exemples are quite simple (particularly the batched items) as we want to focus on the flow to handle succeeded or failed batches. We assume you already know the basic concepts of blockchain events, extrinsics (some substrate functions, also called here transaction or tx), how to initialize the Ternoa SDK, and the basic extrinsic lifecycle:

  • πŸ”§ Create a transaction
  • πŸ–Š Sign a transaction
  • πŸš€ Send a transaction

At the end of this article, you will be able to identify which type of batch function might suits to your use-case and how to handle the failed events.

Ternoa SDK version used : v1.3.1-rc0
npm i ternoa-js@1.3.1-rc0
This version of the SDK embed some breaking changes: the submitTxBlocking now provides the block information alongside events in the result. It also provides the new forceBatchand a bunch of fancy useful helpers to check the result of batched transactions. Learn more here.

πŸ‘€ What's the difference between a Batch, a BatchAll and a ForceBatch call ?

The first important step is to understand each of the three batchs respective process. If they all aim to group several transactions in one call, the difference is how they handle one or several failed extrinsic(s) and the corresponding events returned. Here is a summary:

Without failed tx With failed tx(s)
Batch ItemCompleted(n)
BatchCompleted
ExtrinsicSuccess
BatchInterrupted
ExtrinsicSuccess
BatchAll ItemCompleted(n)
BatchCompleted
ExtrinsicSuccess
ExtrinsicFailed
ForceBatch ItemCompleted(n)
BatchCompleted
ExtrinsicSuccess
itemFailed(n)
BatchCompletedWithErrors
ExtrinsicSuccess

All the code snippets below assume you already have initialized the API in your dApp, on the network of your choice (reminder : default is Ternoa Alphanet. Warning with Mainnet.), and have an account filled with test CAPS to use the keyring to sign and submit transactions. In case you don't, claim some test CAPS on the Ternoa Alphanet Faucet.

1 - In case of successful extrinsic :

Batch/BatchAll/ForceBatch work the same. For each of theses functions, when completing a full batch without any issue, the SDK, through the chain, will provide a BatchCompleted event, meaning that everything went well.

In order to retrieve those specifics events, the ternoa SDK provides some features as the findEvent() function or the findEventOrThrow() that can be bundled to your batch transaction when the call is over.


const simpleBatchTxExemple = async () => {
  try {
    ... 
    // we initialized the api:
    const network = "wss://alphanet.ternoa.com"
    await initializeApi(network)
    // we make the keyring accessible in a constant:
    const keyring = await getKeyringFromSeed("your seed")
    // Let's create some transactions to batch :
    // an NFT
    const signableNFTTxHex = await createNftTx("NFT to be batched", 0, undefined, false)
    // and a collection
    const signableCollectionTxHex = await createCollectionTx("Collection to be batched", 1000)
    // here we batchAll the transactions hashs in an array [].
    // we use the batchAll as we want the tx to be reverted in case of fail.
    const signableBatchTx = await batchAllTxHex([signableNFTTxHex, signableCollectionTxHex])
    // we submit the batched extrinsics and sign them with our keyring variable to receive the events details.
    // we can destructure the result to isolate blockInfo and events
    const { blockInfo, events } = await submitTxBlocking(signableBatchTx, WaitUntil.BlockInclusion, keyring)
    // we retrived the required event : here let's say we want the NFTCreatedEvent to access detail of the transaction.
    const eventsDetail = events.findEventOrThrow(NFTCreatedEvent)
    return eventsDetail
  } catch (err) {
    console.log(err)
  }
}
Enter fullscreen mode Exit fullscreen mode

That's it! Now we know how to batch a transaction, lets see how to handle errors, and which batch function you should use.

2 - In case of failed extrinsic within a batch call (among all the transactions submitted):

  • The batch process stops where it is, and keeps the firsts validated transactions.
  • For each successful event the chain provides a specific transaction event (ex: nft.CollectionCreated) + an ItemCompleted event, until it reachs a failed event. The ternoa SDK then throw a BatchInterrupted event with the error detail and the block index of the failed extrinsic.

Where it gets slightly complicated : Since Batch Extrinsic is considered as successful, even when interrupted, it does not mean that all transactions are validated. The chain provides the ExtrinsicSuccess event anyway.

A good practice would be to monitor ExtrinsicSuccess and check if any BatchInterruptedEvent have been thrown within the batch transaction to retrieve the detail of the failed extrinsic.


const batchTxExemple = async () => {
  try {
    ...
    // again, api is already initialized 
    // keyring is also retrieved and available in a variable
    // let's create some transactions to batch:
    // a first collection
    const signableCollection1 = await createCollectionTx("BatchAllCollection1", undefined)
    // as we want to make the batch fail, we create an nft with an error (a non existing collectionId):
    const signableNFT = await createNftTx("create NFT", 0, 20000, false)
    // and a second collection
    const signableCollection2 = await createCollectionTx("BatchAllCollection2", undefined)
    // here we batch the previous transactions hashs in an array []. 
    // we want to retrieve the failed tx and keep the firsts valid extrinsics.
    const signableForceBatchTx = await batchTxHex([
      signableCollection1,
      signableNFT,
      signableCollection2,
    ])
    // we can again destructure the result to isolate blockInfo and events
    const { blockInfo, events } = await 
submitTxBlocking(signableForceBatchTx, WaitUntil.BlockInclusion, keyring)

    // at this point, we know the batch will fail, because of the wrong collectionId in createNftTx.
    // let's check and catch the error : 
    // we try to look at a BatchInterruptedEvent
    const interruptedEvent = events.findEvent(BatchInterruptedEvent)
    // if there is one, we return the interrupted event.
    if (interruptedEvent) return interruptedEvent
    return { blockInfo, events }
  } catch (err) {
    console.log(err)
  }
}

// expected output of the error detail is the following: 'No Collection was found with that NFT id.'

Enter fullscreen mode Exit fullscreen mode

So what happened here:

  • We batched a group of transactions using the batch function.
  • We deliberately made an extrinsic failed (we created an NFT with a non existing collection Id).
  • When we signed and submitted the batch tx, we received the events list describing what happened.
  • As expected, we retrieved an error : a BatchInterruptedEvent meaning that the firsts transactions (here the collection created) have been validated on the chain and the batch has been interrupted/stopped when trying to create the NFT.
  • Because some transactions are valid, the batch extrinsic is considered as successful even if it has been interrupted.
  • The last transaction (the second collection) has not been executed.

3 - In case of a failed extrinsic within a batchAll call (among all the transactions submitted):

  • An ExtrinsicFailed event is provided with the error detail of the first failed extrinsic encountered.
  • The chain reverts each validated extrinsics : Meaning the chain status goes back to where it was before the call. No extrinsic is validated at all.

const batchAllTxExemple = async () => {
  try {
    ...
    // again, api is already initialized 
    // keyring is also retrieved and available in a variable
    // let's create some transactions to batch using batchAll:
    // a first collection
    const signableCollection1 = await createCollectionTx("BatchAllCollection1", undefined)
    // as we want to make the batch fail, we create again an nft with an error (a non existing collectionId):
    const signableNFT = await createNftTx("create NFT", 0, 20000, false)
    // and a second collection
    const signableCollection2 = await createCollectionTx("BatchAllCollection2", undefined)
    // here we batch the previous transactions hashs in an array [].
    // we want all the tx to be reverted incase of a failed tx.
    const signableForceBatchTx = await batchAllTxHex([
      signableCollection1,
      signableNFT,
      signableCollection2,
    ])
    // we can again destructure the result to isolate blockInfo and events
    const { blockInfo, events } = await submitTxBlocking(signableForceBatchTx, WaitUntil.BlockInclusion, keyring)

    // at this point, we know the batch will fail, because of the wrong collectionId in createNftTx.
    // let's check and handle the error : 
    // this time we try to look at an ExtrinsicFailedEvent
    const failedEvent = events.findEvent(ExtrinsicFailedEvent)
    // if there is one, we return the failed event.
    if (failedEvent) return failedEvent
    return { blockInfo, events }
  } catch (err) {
    console.log(err)
  }
}

// expected output of the error detail is the following: 'No Collection was found with that NFT id.'
Enter fullscreen mode Exit fullscreen mode

So what happened here:

  • We batched a group of transactions using the batchAll function.
  • We deliberately made an extrinsic failed (we created an NFT with a non existing collection Id).
  • When we signed and submitted the batchAll tx, we received the events list describing what happened.
  • As expected, we retrieved an error : an extrinsicFailedEvent meaning that all transactions have been reverted and the batchAll extrinsic has not been validated (considered as not successful).
  • The last transaction (the second collection) has not been executed.

4 - In case of a failed extrinsic within a forceBatch call (among all the transactions submitted):

  • The batch process will anyway keep running until the last extrinsic provided in the forceBatch parameters.
  • For each successful events the chain provides a specific transaction event (ex: nft.nftCreated) + an ItemCompleted event, while it provides an ItemFailed event with the error detail for each failed extrinsics.
  • In case of failed extrinsic, the chain will also provide a BatchCompletedWithErrors event.

Like for the classic batch extrinsic, the forceBatch extrinsic is considered as successful even with failedItems. The chain provides the ExtrinsicSuccess event at the end.

Again the good practice would be to monitor ExtrinsicSuccess with the BatchCompletedWithErrorsevent and check if any ItemFailed events have been thrown within the forceBatch.


const forceBatchTxExemple = async () => {
  try {
    ...
    // again, api is already initialized 
    // keyring is also retrieved and available in a variable
    // let's create some transactions to batch using forceBatch:
    // a first collection
    const signableCollection1 = await createCollectionTx("BatchAllCollection1", undefined)
    // as we want to make the batch fail, we create again an nft with an error (a non existing collectionId):
    const signableNFT = await createNftTx("create NFT", 0, 20000, false)
    // and a second collection
    const signableCollection2 = await createCollectionTx("BatchAllCollection2", undefined)
    // here we forceBatch the previous transactions hashs in an array [].
    // we want the process to be ran until the end even in case of failed tx.
    const signableForceBatchTx = await forceBatchAllTxHex([
      signableCollection1,
      signableNFT,
      signableCollection2,
    ])
    // we can again destructure the result to isolate blockInfo and events
    const { blockInfo, events } = await submitTxBlocking(signableForceBatchTx, WaitUntil.BlockInclusion, keyring)

    // at this point, we know the batch will fail, because of the wrong collectionId in createNftTx.
    // let's check and handle the error : 
    // this time we try to look at an BatchCompletedWithErrorsEvent
    const isBatchIncomplete = events.findEvent(BatchCompletedWithErrorsEvent)
    if (isBatchIncomplete) {
    // if the forceBatch is incomplete, we retrieve the list of failed events in an array of event []
    const failedEvents = events.findEvents(ItemFailedEvent)
    return failedEvents
    }
    return { blockInfo, events }
  } catch (err) {
    console.log(err)
  }
}

// expected output is an array of failed events with their respective error details. Here only one event: the NFT created
Enter fullscreen mode Exit fullscreen mode

So what happened here:

  • We batched a group of transactions using the forceBatch function.
  • We deliberately made an extrinsic failed (we created an NFT with a non existing collection Id).
  • When we signed and submitted the forceBatch tx, we received the events list describing what happened.
  • As expected, we retrieved an error : an BatchCompletedWithErrorsEvent meaning that the valid transactions are kept while the failed ones are not validated.
  • for the first time, the last transaction (the second collection) has been executed and is validated (no issue is retrieved).

πŸ’Ž Improve checking batch results

Since the v1.3.1-rc0 version, the Ternoa SDK provides the related utils to check a batch call status and result. According to the type of batch call you need to execute, you will find the corresponding util function :

  • checkBatch for a 'standard' batch call
  • checkForceBatch for a forceBatch call
  • checkBatchAll for a batchAll call

As an exemple, checkForceBatch will provide in case of batch without errors, the list of successful events, and an isBatchCompleteWithoutErrors boolean (true). If any errors occurs it will provide the same isBatchCompleteWithoutErrors boolean (false), the list of successful events and failedItems, and an isTxSuccess boolean (true).


const forceBatchTxExemple = async () => {
  try {
    ...
    // using the exact same code as before
    const { blockInfo, events } = await submitTxBlocking(signableForceBatchTx, WaitUntil.BlockInclusion, keyring)
    // here we use the checkForceBatch to see if any errors happened.
    const isForceBatchSuccedeed = await checkForceBatch(events)
    // do your logic with the results provided by isForceBatchSuccedeed.
   return what you need.
    ...
  } catch (err) {
    console.log(err)
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”₯ Conclusion

The general best practice is to always use the batchAll transaction. It provide more consistency to avoid loosing any transactions on the way. But depending on the use-case, you can go with the batch function that suit you the best, and in case of failed transaction, you now know how to monitor them πŸ’ͺ.

πŸ™Œ Congrats folks, you are now batch friendly !
You know how to make the difference between a batch, a batchAll and a forceBatch call. You know which type of event to monitor in the result of your function, and which one to query to retrieve the good information.
Have fun 🦦 !

Top comments (2)

Collapse
 
ogous profile image
Ogous Chan Ali

Your deeds would never be forgotten among the Fellowship of Batches, Victor !

Collapse
 
peshwar profile image
PE

Great post, Victor!