Lisk Bills & How to Create Custom Transactions with the SDK
Lisk’s Alpha SDK Phase officially began in late July with the release of SDK 2.1.0. We decided what better way to showcase the potential of custom transactions than to create our own proof-of-concept (PoC) blockchain application. To explore the possibilities of custom transactions at their best, we decided to build an invoicing application and through this register two new custom transactions on our blockchain.
Introduction to Custom Transactions
Lisk SDK allows you to define your own custom transaction types where you can implement the required logic for your blockchain use-case. The custom transaction types are an extension to the default set of transactions that is already part of the Lisk Protocol. You can read more about the predefined types here.
The beginning of the Alpha SDK phase of our roadmap allows you to create your own proof-of-concept blockchain applications aligned with our architecture. This phase of our roadmap also allows us to get feedback on how the development experience can be improved via discussion on our community channels.
Custom Transactions to Grow the Lisk Ecosystem
Benefits of Custom Transactions
Every transaction object has the ability to store data in its "asset" field. Custom transactions make clever use of this. The use of the "asset" field allows for any JSON data to be passed to the transaction. This allows for greater flexibility and creativity when defining custom logic.
Besides that, every custom transaction can access and modify all account-related data and only read transaction-related data from the database. This allows for more advanced interactions between data and even between different custom transactions. For example, our PoC used the data from the Invoice transaction to verify the validity of the Payment transaction.
You can also create a token in the asset field with some basic transfer and verification logic. In the end, this is just another way of smart contract logic.
Let’s continue with exploring the technicals of our Lisk Bills PoC.
Lisk Bills: A Blockchain Invoicing Application
As we like the Keep it Simple and Stupid (KISS) approach, we have built a minimal frontend with React that uses the Lisk Alpha SDK to interact directly with your blockchain application. The PoC includes two actors, the client and the freelancer.
Imagine Alice (Freelancer) and Bob (Client). Bob is looking for a new logo for his website and decides to consult a freelancer. While looking for a good designer, he comes across Alice who offers some spectacular designs in her portfolio. Bob is so excited he decides to immediately employ Alice’s skillset.
A few days go by, and Alice returns the promised logo together with an invoice. However, Bob is a big fan of blockchain technology as it helps to ease the settlement process. 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.
For the above reason, Bob asks Alice to create the invoice via a Lisk custom transaction.
In order to do so, Alice first has to log in with her passphrase to the Lisk Bills application.
Custom Transaction 1: Invoice
Now Alice has been logged in, she can create an invoice. To create the custom invoice transaction, Alice has to input the following details:
- "Client" holds Bob’s Lisk address or business name.
- "RequestedAmount" holds the amount Bob is due to Alice.
- "Description" to describe the delivered design service.
The following data is stored in the asset field of the transaction. As this is a normal BaseTransaction, we can simply specify Bob’s Lisk address as the recipient for the transaction.
Before we dive into the technicals, make sure to open or clone the lisk-sdk-examples repository. The code for both custom transactions can be found in invoice/transactions/invoice_transaction.js and invoice/transactions/payment_transaction.js.
First of all, let’s take a look at the class definition. The "InvoiceTransaction" extends the "BaseTransaction" which means it’s inheriting its properties. As the name suggests, "BaseTransaction" is the most basic interface for creating new transaction types. Other transaction types also exist in the system, later we will show an example of extending the "TransferTransaction" type.
When extending the "BaseTransaction" we can provide extra business logic for the following methods: Prepare, Validate Asset, Apply Asset and Undo Asset. You can find out more about these methods in our documentation.
Also, pay attention to the static getter function for retrieving the type of the transaction. As an example, we have chosen "13" to be the type number for this transaction. Besides that, you can set the fee you want users to pay for using this transaction type. For now, we have set this to 1 LSK (10 to the 8th beddows).
The prepare function is responsible for loading the required data used inside the "applyAsset()" and "undoAsset()" function. Here, we try to load the account data for the sender as we want to add data to his "asset" field in the "applyAsset()" function. This data will be loaded from the StateStore object that provides access to data in the database.
We can cache the sender account like this by passing an array with filters.
However, we actually don’t have to manually cache the data. We can simply call the parent method for the "prepare" function in the abstract "BaseTransaction" class which will by default cache the sender account to deduct the fee in the apply step.
Before a transaction reaches the apply step it gets validated. Check the transaction’s asset correctness from the schema perspective (no access to "StateStore" here). You can invalidate the transaction by pushing an error into the result array.
As you can see, we finally use the loaded account which we put in the store during the "prepare" step. Next, we update the invoice count and record the ID of the invoice in an array with sent invoices. We will use this data in our frontend to display all invoices.
Do not underestimate the importance of the "undoAsset()" function. The Undo function allows us to rollback to a previous blockchain state. Therefore, we should exactly tell our blockchain application how it should roll back changes.
The Undo function is of most importance for the fork recovery mechanism. In case a fork occurs on a chain with tip B and we want to roll back to a common height in order to re-apply blocks up to the tip of chain A, we need the Undo function to do the actual rollback to this common height.
For the invoice proof of concept, the code reduces the "invoiceCount" and removed the invoice ID from the "invoicesSent" array.
Ok, we have explored the functions for the invoice transaction. Let’s move to the payment transaction.
Custom Transaction 2: Payment
Now Bob has received the Invoice Transaction in his wallet, he decides to pay for the invoice. In order to fulfill the transaction, we would normally send a "TransferTransaction" which is natively supported by Lisk SDK.
However, doing so would make this a very boring tutorial. Therefore, Bob decides to use another custom transaction to showcase Lisk’s possibilities. This custom Payment Transaction holds logic to verify the transferred amount is at least equal to the "RequestedAmount". Also, the transaction requires Bob to specify the ID of the invoice he wants to fulfill.
If the transferred amount is too low or the invoice ID just doesn’t exist, the transaction fails. Bob keeps his side of the agreement and sends the requested amount to Alice’s invoice ID. Bob even adds a tip for Alice’s great work.
This is how the UI implementation looks like for paying an invoice with our Lisk Bills application.
Again, let’s take a look at the class definition. The "PaymentTransaction" extends the "TransferTransaction" which means it’s inheriting its properties like a different fee and transfer-related verification checks. Also, pay attention to the static getter function for retrieving the type of the transaction. As we can not have identical transaction types, the "PaymentTransaction" has received type "14".
Also, note we don’t define a static getter function for "FEE". We didn’t implement it here as we don’t want to overwrite the "FEE" defined in the "TransferTransaction". In short, we want to use the "0.1" fee defined in "TransferTransaction".
The prepare function is responsible for loading the required data into the "store" to be used inside the "applyAsset()" and "undoAsset()" function. For the "PaymentTransaction", we are loading the transaction that holds the invoice using the ID sent with "this.asset.data".
As you might have noticed, we didn’t implement the "validateAsset()" function for the payment transaction. The only check we have to perform is validating if the sent number of tokens is at least equal to the requested number of tokens.
In order to validate this, we need access to the "StateStore" as we need to cache the invoice transaction. Because we can only perform static checks in the "validateAsset()" function that don’t use the "StateStore", this check is moved to the apply step.
The "applyAsset()" function first tries to find the corresponding invoice transaction. If this transaction exists, we further check for the number of tokens transferred is at least equal to the requested amount in the invoice. If this check succeeds, the transaction gets applied.
No rollback logic is required for the undo step of the payment transaction. We do not modify any data in the store with the "set" method, so no need for defining undo steps to revert this data change.
However, do not forget to call "super.undoAsset(store)" as the Undo step will make sure the fee Alice has paid gets returned to her account’s balance.
How to Register Custom Transactions?
Ok, we have prepared both of our custom transactions. Bob and Alice are very happy to use both transactions in order to finalize their deal. However, we don’t know yet how to register these new transactions on our blockchain application.
The invoice/index.js file holds the startup code for running your custom blockchain and also registers both transactions. It’s as simple as that!
Ok, we are all done! At last, let’s take a brief look at the considerations regarding the use of custom transactions.
Considerations of Using Custom Transactions
Currently, we expect users to run their own blockchain instance that registers their freshly created custom transaction.
It took us a few weeks to build this prototype. We have deliberately kept it simple to act as a learning resource, and as inspiration for the community. It is not production-ready.
Lisk aims to allow for creativity within the blockchain industry by providing the ability to process data with custom business logic. This concept is very similar to smart contracts as they also hold custom business logic. We are pleased to introduce you to Lisk Bills as the first example of what is possible with our SDK.
We hope this freedom will spark a whole bunch of new innovative blockchain applications build on top of Lisk using the newly released Lisk Alpha SDK. Currently, we do not plan to support custom transactions on the Lisk mainnet but they are intended to be used inside your own blockchain application.