Answering All of Your Questions About Custom Transactions

Two weeks ago, we introduced our Lisk Bills proof of concept that came with a webinar and guiding article to explain the code behind this blockchain application built with the Lisk SDK. Lisk Bills was our first opportunity to introduce creating custom transactions in a fun, practical way. We investigated the use of different methods like applyAsset, undoAsset, validateAsset, prepare, TYPE, and FEE.

By Michiel Mulders

26 Sep 2019

Deep Dive Into Custom Transactions_Blog_0.png

Lisk Bills was our first opportunity to introduce creating custom transactions in a fun, practical way. We investigated the use of different methods like applyAsset, undoAsset, validateAsset, prepare, TYPE and FEE. However, there is much more to be found.

This article will answer the following questions:

  • What are the limitations of validateAsset?
  • How to use the StateStore and what filters can be applied?
  • Should we extend from BaseTransaction or TransferTransaction?
  • What happens with a transaction when validation fails?
  • When to call the parent method through super and what does it do?

Before we can answer these questions, let’s quickly refresh our knowledge about custom transactions. If you are already up to date with our proof of concept, you can skip this section.

Recap: Creating Custom Blockchain Transactions

Lisk Bills is the first prototype we have built with our Alpha SDK. To display the full potential of our Alpha SDK, we decided to create two custom transactions that interact with each other.

The story goes as follows…

We have a freelancer called Alice who is delivering a design service for our client Bob. Bob is a big fan of blockchain technology and thinks that a blockchain-based invoice might be a good use case for the Lisk Alpha SDK.

It often happens parties disagree about the agreed price, product, or even shipping terms. Bob, therefore, believes blockchain can help with recording all this information right from the beginning, so no disputes can occur and human error can be eliminated. The blockchain should act as proof for the invoice.

Therefore, he develops a first custom transaction (invoiceTransaction) that allows freelancers to submit invoices on his custom blockchain application. The invoice accepts basic properties like a client address, requested amount of tokens, and a description of the delivered service.

Next, he continues work on developing a paymentTransaction that serves as a way to pay for the issued invoice. The paymentTransaction will look up the invoiceTransaction nd validate if the sent amount is at least equal to the requested amount. If you haven’t noticed yet, this is the cool part where both transactions come together for verification purposes.

You can find more technical information about the initial implementation of both custom transactions in our previous post about custom transactions.

FAQ — Tell Me More About Custom Transactions

Let’s answer common questions we receive on our Discord channel.

Should I extend from BaseTransaction or TransferTransaction?

BaseTransaction is the base class we recommend for extending to create your custom transaction. The BaseTransaction class is defined as an abstract class. Most of the defined abstract methods have a base implementation, however, the validateAsset doesn’t have one. It’s your responsibility to add validation code.

This means we do not explicitly expect you to write the logic for each type of function, we have that covered for you. However, you have to add custom logic to the following functions:

  • validateAsset
  • applyAsset
  • undoAsset
  • verifyAgainstTransactions (not needed at the moment)

Besides that, we do not recommend extending from theTransferTransaction if you are unsure of what you are doing. Although this is what we did for our Lisk Bills proof of concept for the PaymentTransaction. As this is a proof of concept, the goal is to teach the principles of custom transactions, so we decided to keep the example as simple as possible.

In short, if we do decide to change the implementation of TransferTransaction, as it’s part of the protocol, your custom transaction might not work anymore. Therefore, it’s safer to copy this TransferTransaction logic into your custom transaction.

Resources:

Do I have to call the parent method in BaseTransaction?

You don’t have to call the parent method in BaseTransaction. However, you can definitely do this if the abstract parent method has an implementation.

Currently, only the prepare method allows you to call its parent method. Assume you want to cache the sender account in the store for later use in applyAsset. As the prepare method already has the same logic defined, we can simply call the parent method in the BaseTransaction class.

File name
1
2Although we encourage you to not call super methods, we have in the interests of simility occasionally done so. However, as we are still in an Alpha SDK phase, the implementation of the prepare method in the BaseTransaction is subject to change.
3
4To prevent you from having to change the implementation, we suggest caching the data yourself instead of calling the parent method through super.prepare(store) (we explain this in question 5).
5
6Also, other functions like applyAsset, validateAsset, or undoAsset do not have parent implementations as they are abstract functions.
7
8In case you do want to extend from the **TransferTransaction** (again not recommended), we suggest calling the super.prepare(store), super.applyAsset(store), and super.undoAsset. This is because these functions have an important implementation inside the TransferTransaction and return errors in case something goes wrong. This makes sure the right data gets cached and the validation errors are collected in the errors array. In case of an error, the transaction gets discarded.
html async prepare(store) { await super.prepare(store); await store.transaction.cache(\[ { id: this.asset.data, }, \]); } applyAsset(store) { const errors = super.applyAsset(store); const transaction = store.transaction.find( transaction =

transaction.id === this.asset.data ); // Find related invoice in transactions for invoiceID ... } ```

What is the responsibility of validateAsset?

The validateAsset function is only used for performing static checks like verifying if the property exists and if it has the right type. More rigorous checks that require data to be cached in the store need be performed in the applyAsset step as you don’t have access to the store inside of the validateAsset function.

How can I use the StateStore?

The store, which is referred to as a StateStore, allows a developer to cache data he requires for performing validation or implementing custom business logic inside the applyAsset, orundoAsset functions.

Under the hood, the cache< method retrieves data from the database and stores this data in an in-memory key-value store inside the Lisk application. We can later retrieve data from this key-value store through the exposed functions which are explained in step B down below.

We allow for any account data to be modified (read-write) and to read any transaction data (read-only). Let’s explore how we can use this store.

A/ Caching Data

The first step consists of caching the data you require in your custom transaction. The cache method will retrieve the requested data from the database and save it inside the store. This cache method accepts filters which we discuss in question 5 below.

B/ Retrieving Data

Accessing the data cached inside the store can be done using the functions it exposes. For the account_store, the following functions are exposed:

  • get(key) — Retrieve a single element from the store. The key here accepts an address.
  • getOrDefault(key) — Get account object from store or create default account if it doesn’t exist.
  • find(fn — Accepts a lambda expression for finding the data that matches the expression.
  • set(key, updatedObject) — Allows updating an account in the database (account is only read-write store).
    File name
    1html applyAsset(store) { const sender = store.account.get(this.senderId); // Save invoice count and IDs sender.asset.invoiceCount = sender.asset.invoiceCount === undefined ? 1 : ++sender.asset.invoiceCount; sender.asset.invoicesSent = sender.asset.invoicesSent === undefined ? \[this.id\] : \[...sender.asset.invoicesSent, this.id\]; store.account.set(sender.address, sender); return \[\]; }

For the transaction_store, the following functions are exposed:

  • get(key — Retrieve a single element from the store. The key here accepts a transaction id.
  • find(fn) — Accepts a lambda expression for finding the data that matches the expression.

You can find both account_store and transaction_store on GitHub. LiskHQ/lisk-sdk

How can I use Entity filters for StateStore?

The store, allows a developer to cache data using filters. Basically, you have access to all the filters available for the account and the transaction entity that the storage component exposes.

A/ Filters Usage

Every entity like account or transaction hosts a whole list of filters. You can find the filters for account and transaction in the storage component.

The piece of code below is one of the filters that has been implemented in the account entity. For now, you can use all the filters we have defined in both the account and transaction entity.

Here, we have defined a filter for the address field and expect a string as input. Next, we added two extra options for validating the format and defining a filter type: ft.TEXT. The explicit filter type is very important as it defines which filter suffixes are available for this filter.

This means we can do something like:

File name
1
2If you are not sure which filters are available, you can always call <Entity>.getFilters() to get the list of available filters.
3
4### Resources:
5
6* Filters for [accounts](https://github.com/LiskHQ/lisk-sdk/blob/development/framework/src/components/storage/entities/account.js)
7* Filters for [transactions](https://github.com/LiskHQ/lisk-sdk/blob/development/framework/src/components/storage/entities/transaction.js)
8* Filter [suffixes per type](https://github.com/LiskHQ/lisk-sdk/tree/development/framework/src/components/storage#filters)
9
10**B/ Combining Filters**
11
12We can pass filters as a **single JSON object or as an array with multiple filters**.
13
14If filters are provided as JSON objects, they will always be joined with an AND combinator. For instance, specifying filters as {name: 'Alpha', description\_like: 'Bravo'} results in fetching all results which have a name equal to Alpha and description matching Bravo.
15
16Specifying filters as an array of objects, e.g. \[{name: 'Alpha'}, {description\_like: 'Bravo'}\], will result in joining objects with OR combinator, i.e. fetching data which name equal to Alpha or description like Bravo.
17
18**C/ Supported Custom Filters for StateStore**
html this.addFilter('asset\_contains', ft.CUSTOM, { condition: "asset @

'${asset_contains:value}'::jsonb", }); this.addFilter('asset_exists', ft.CUSTOM, { condition: "asset ? '${asset_exists:value}'", }); ```

Recently, we have added two custom filters which you can see in the Gist above. The first one asset_contains allows searching for a value inside of an asset. The second one asset_exists allows for matching a whole asset object for the asset field of an account.

Resources:

What are the limitations of the StateStore?

The StateStore only allows for reading transaction data but does allow you to read and write data for account objects. It is not possible to access block data through the StateStore. Also, the StateStore is not accessible inside of the validateAsset function as it only allows for static checks.

Why can I use "this.amount" or "this.recipientId" inside of a custom transaction?

When extending from the BaseTransaction class, the constructor by default accepts a raw transaction object. The constructor will validate every property of the raw transaction and append it to the this keyword so you can use it inside of your transaction. Snippet from the constructor:

File name
1html this.amount = new BigNum( isValidNumber(tx.amount) ? (tx.amount as string | number) : '0', );

The full constructor logic can be found on GitHub.