Hello world

Welcome to the step-by-step guide of creating the Hello world application with the Lisk SDK. The Hello world application is a simple blockchain app that showcases a minimal setup of a blockchain application with 1 custom transaction: the "Hello" transaction.

The purpose of the Hello world application is to explain both how to implement and use custom transactions with the Lisk SDK. This custom transaction will extract the "hello" key value from the transaction asset property, and save it to the sender’s account.

The Hello world implementation steps are listed below:

  • Steps 1-5 describes the implementation requirements on the node-side of the blockchain application.

  • Step 6 explains how to interact with the network from the client-side.

  • Step 7 explains how to override specific config values.

For the full code example please see Hello world App on Github.

1. Installation & setup

Firstly, create the root folder for the Hello world wpp and initialize the project:

mkdir hello_world (1)
cd hello_world (2)
npm init --yes (3)
1 Create the root folder for the blockchain application.
2 Navigate into the root folder.
3 Initialize the manifest file of the project.

The next step is to install the lisk-sdk package and add it to the project’s dependencies.

Please ensure that the SDK preinstall preparation steps have been completed.

Supported platforms

  • Ubuntu 16.04 (LTS) x86_64

  • Ubuntu 18.04 (LTS) x86_64

  • MacOS 10.13 (High Sierra)

  • MacOS 10.14 (Mojave)

Dependencies

Dependencies Version

Node.js

12.15.0 or higher

PostgreSQL

10+

Redis (optional)

5+

Python

2

npm install lisk-sdk (1)
npm install @liskhq/lisk-validator @liskhq/lisk-cryptography @liskhq/lisk-transactions @liskhq/lisk-constants (2)
1 Install the Lisk SDK as dependency for the node side.
2 Install Lisk Elements dependencies for the client side scripts.

If a database has not previously been created then it is necessary to create one. The default database name is lisk_dev The following command, createdb lisk_dev --owner lisk should be used. The default database user and password are lisk and password. Both the user and password can be changed in the configuration of the Lisk SDK.

Ensure to start with a fresh database.

  • Postgres system-wide

  • Postgres with Docker

For the system-wide Postgres installation:

psql -c "DROP DATABASE lisk_dev"
psql -c "CREATE DATABASE lisk_dev OWNER lisk"

If you have installed Postgres with Docker, it is necessary to run the following commands:

docker exec -ti lisk_sdk_db psql -h localhost -U lisk -d postgres -c "DROP DATABASE lisk_dev"
docker exec -ti lisk_sdk_db psql -h localhost -U lisk -d postgres -c "CREATE DATABASE lisk_dev OWNER lisk"

Create the file index.js. This will hold the logic to initialize and start the blockchain application.

2. Configure the application

Configuration is necessary to provide the basic information for building the new application.

Please execute the following command:

touch index.js

Now open the file index.js that was created with the previous step, and insert the following code shown below:

Contents of index.js
const { Application, genesisBlockDevnet, configDevnet } = require('lisk-sdk'); (1)

configDevnet.app.label = 'hello-world-blockchain-app'; (2)
//configDevnet.components.storage.user = '<username>'; (3)
//configDevnet.components.storage.password = 'password'; (4)

const app = new Application(genesisBlockDevnet, configDevnet); (5)

app (6)
    .run()
    .then(() => app.logger.info('App started...'))
    .catch(error => {
        console.error('Faced error in application', error);
        process.exit(0);
    });
See the file on GitHub: hello_world/index.js.
1 Require application class, the default genesis block and the default config for the application. The necessary dependencies are required from the lisk-sdk package. The most important one is the Application class, which is used in <5> to create the application instance. The application instance will start the whole application at the bottom of index.js.
2 Set the name of the blockchain application.
3 In the case whereby a different user other than 'lisk' was given for access to the database lisk_dev, it will be necessary to update the username in the config.
4 Uncomment this and replace password with the password for your database user.
5 Create the application instance. By sending the parameters for the genesis block and the configuration template, the application is now configured with most basic configurations to start the network.
6 The code block below starts the application and does not need to be changed.
To change any of the values for configDevnet, please see the full list of configurations for Lisk SDK and overwrite them as described in paragraph 7.

After the code block above has been added, save and close index.js. At this point, the node and the network can now be started in order to verify that the setup was successful by executing the following command below:

node index.js | npx bunyan -o short

node index.js will start the node, and
| npx bunyan -o short will pretty-print the logs in the console.

If everything is functioning correctly, the following logs listed below will be displayed:

$ node index.js | npx bunyan -o short
14:01:39.384Z  INFO lisk-framework: Booting the application with Lisk Framework(0.1.0)
14:01:39.391Z  INFO lisk-framework: Starting the app - helloWorld-blockchain-app
14:01:39.392Z  INFO lisk-framework: Initializing controller
14:01:39.392Z  INFO lisk-framework: Loading controller
14:01:39.451Z  INFO lisk-framework: Old PID: 7707
14:01:39.452Z  INFO lisk-framework: Current PID: 7732
14:01:39.467Z  INFO lisk-framework: Loading module lisk-framework-chain:0.1.0 with alias "chain"
14:01:39.613Z  INFO lisk-framework: Event network:bootstrap was subscribed but not registered to the bus yet.
14:01:39.617Z  INFO lisk-framework: Event network:bootstrap was subscribed but not registered to the bus yet.
14:01:39.682Z  INFO lisk-framework: Modules ready and launched
14:01:39.683Z  INFO lisk-framework: Event network:event was subscribed but not registered to the bus yet.
14:01:39.684Z  INFO lisk-framework: Module ready with alias: chain(lisk-framework-chain:0.1.0)
14:01:39.684Z  INFO lisk-framework: Loading module lisk-framework-network:0.1.0 with alias "network"
14:01:39.726Z  INFO lisk-framework: Blocks 1886
14:01:39.727Z  INFO lisk-framework: Genesis block matched with database
14:01:39.791Z ERROR lisk-framework: Error occurred while fetching information from 127.0.0.1:5000
14:01:39.794Z  INFO lisk-framework: Module ready with alias: network(lisk-framework-network:0.1.0)
14:01:39.795Z  INFO lisk-framework: Loading module lisk-framework-http-api:0.1.0 with alias "http_api"
14:01:39.796Z  INFO lisk-framework: Module ready with alias: http_api(lisk-framework-http-api:0.1.0)
14:01:39.797Z  INFO lisk-framework:
  Bus listening to events [ 'app:ready',
    'app:state:updated',
    'chain:bootstrap',
    'chain:blocks:change',
    'chain:signature:change',
    'chain:transactions:change',
    'chain:rounds:change',
    'chain:multisignatures:signature:change',
    'chain:multisignatures:change',
    'chain:delegates:fork',
    'chain:loader:sync',
    'chain:dapps:change',
    'chain:registeredToBus',
    'chain:loading:started',
    'chain:loading:finished',
    'network:bootstrap',
    'network:event',
    'network:registeredToBus',
    'network:loading:started',
    'network:loading:finished',
    'http_api:registeredToBus',
    'http_api:loading:started',
    'http_api:loading:finished' ]
14:01:39.799Z  INFO lisk-framework:
  Bus ready for actions [ 'app:getComponentConfig',
    'app:getApplicationState',
    'app:updateApplicationState',
    'chain:calculateSupply',
    'chain:calculateMilestone',
    'chain:calculateReward',
    'chain:generateDelegateList',
    'chain:updateForgingStatus',
    'chain:postSignature',
    'chain:getForgingStatusForAllDelegates',
    'chain:getTransactionsFromPool',
    'chain:getTransactions',
    'chain:getSignatures',
    'chain:postTransaction',
    'chain:getDelegateBlocksRewards',
    'chain:getSlotNumber',
    'chain:calcSlotRound',
    'chain:getNodeStatus',
    'chain:blocks',
    'chain:blocksCommon',
    'network:request',
    'network:emit',
    'network:getNetworkStatus',
    'network:getPeers',
    'network:getPeersCountByFilter' ]
14:01:39.800Z  INFO lisk-framework: App started...
14:01:39.818Z  INFO lisk-framework: Validating current block with height 1886
14:01:39.819Z  INFO lisk-framework: Loader->validateBlock Validating block 10258884836986606075 at height 1886
14:01:40.594Z  INFO lisk-framework: Lisk started: 0.0.0.0:4000
14:01:40.600Z  INFO lisk-framework: Verify->verifyBlock succeeded for block 10258884836986606075 at height 1886.
14:01:40.600Z  INFO lisk-framework: Loader->validateBlock Validating block succeed for 10258884836986606075 at height 1886.
14:01:40.600Z  INFO lisk-framework: Finished validating the chain. You are at height 1886.
14:01:40.601Z  INFO lisk-framework: Blockchain ready
14:01:40.602Z  INFO lisk-framework: Loading 101 delegates using encrypted passphrases from config
14:01:40.618Z  INFO lisk-framework: Forging enabled on account: 8273455169423958419L
14:01:40.621Z  INFO lisk-framework: Forging enabled on account: 12254605294831056546L
14:01:40.624Z  INFO lisk-framework: Forging enabled on account: 14018336151296112016L
14:01:40.627Z  INFO lisk-framework: Forging enabled on account: 2003981962043442425L
[...]

To stop the blockchain process, press CTRL+C.

3. Create a new transaction type

For the Hello world App, it is necessary to create a custom transaction HelloTransaction:
If the account contains an adequate enough balance to process the HelloTransaction transaction, (the fee is set to 1 LSK by default), the new "hello" property will appear into the account’s asset field. For example, after sending a valid sender id type transaction, {"type": 10, "senderId": "16313739661670634666L", …​ "asset": { "hello": "world" } }, the sender’s account will change from:
{ address: "16313739661670634666L", …​, asset: null }, to
{ "address": "16313739661670634666L", …​, "asset": {"hello": "world"}} }.

Now it is possible to define the new transaction type, HelloTransaction.

Next, create and open the file hello_transaction.js and insert the following code shown below:

Contents of hello_transaction.js
const {
    BaseTransaction,
    TransactionError
} = require('@liskhq/lisk-transactions');

class HelloTransaction extends BaseTransaction {

    /**
    * Set the `HelloTransaction` transaction TYPE to `10`.
    * Every time a transaction is received, it is differentiated by the type.
    * The first 10 types, from 0-9 is reserved for the default Lisk network functions.
    */
    static get TYPE () {
        return 20;
    }

    /**
    * Set the `HelloTransaction` transaction FEE to 1 LSK.
    * Every time a user posts a transaction to the network, the transaction fee is paid to the delegate who includes the transaction into the block that the delegate forges.
    */
    static get FEE () {
        // return `${10 ** 8}`; // (= 1 LSK)
        return `0`;
    };

    /**
    * Prepares the necessary data for the `apply` and `undo` step.
    * The "hello" property will be added only to sender's account, therefore it is the only resource required in the `applyAsset` and `undoAsset` steps.
    */
	async prepare(store) {
		await store.account.cache([
			{
				address: this.senderId,
			},
		]);
	}

    /**
    * Validation of the value of the "hello" property, defined by the `HelloTransaction` transaction signer.
    * The implementation below checks that the value of the "hello" property needs to be a string, which is not longer than 64 characters.
    */
	validateAsset() {
		const errors = [];
		if (!this.asset.hello || typeof this.asset.hello !== 'string' || this.asset.hello.length > 64) {
			errors.push(
				new TransactionError(
					'Invalid "asset.hello" defined on transaction',
					this.id,
					'.asset.hello',
					this.asset.hello,
					'A string value no longer than 64 characters',
				)
			);
		}
		return errors;
	}

    /**
    * applyAsset is where the custom logic of the Hello world app is implemented.
    * applyAsset() and undoAsset() uses the information about the sender's account from the `store`.
    * Here it is possible to store additional information regarding the accounts using the `asset` field. The content property of "hello" transaction's asset is saved into the "hello" property of the account's asset.
    */
	applyAsset(store) {
        const errors = [];
        const sender = store.account.get(this.senderId);
        if (sender.asset && sender.asset.hello) {
            errors.push(
                new TransactionError(
                    'You cannot send a hello transaction multiple times',
                    this.id,
                    '.asset.hello',
                    this.amount.toString()
                )
            );
        } else {
            const newObj = { ...sender, asset: { hello: this.asset.hello } };
            store.account.set(sender.address, newObj);
        }
        return errors; // array of TransactionErrors, returns empty array if no errors are thrown
	}

    /**
    * Inverse of `applyAsset`.
    * Undoes the changes made in applyAsset() step - reverts to the previous value of "hello" property, if not previously set this will be null.
    */
	undoAsset(store) {
		const sender = store.account.get(this.senderId);
		const oldObj = { ...sender, asset: null };
		store.account.set(sender.address, oldObj);
		return [];
	}
}

module.exports = HelloTransaction;
See the file on GitHub: hello_world/hello_transaction.js

After adding the code block above, save and close hello_transaction.js.

4. Register the new transaction type

At this point the project should have the following file structure as shown below:

hello_world
├── hello_transaction.js
├── index.js
├── node_modules
└── package.json

Add the new transaction type to your application, by registering it to the application instance inside of index.js. To create this file, please execute the command listed below:

touch index.js
It is only required to add 2 new lines, (number <2> and <7>) as shown below to the existing index.js, to register the new transaction type.
Contents of index.js
const { Application, genesisBlockDevnet, configDevnet} = require('lisk-sdk'); (1)
const HelloTransaction = require('./hello_transaction'); (2)

configDevnet.app.label = 'hello-world-blockchain-app'; (3)
//configDevnet.components.storage.user = '<username>'; (4)
//configDevnet.components.storage.password = 'password'; (5)

const app = new Application(genesisBlockDevnet, configDevnet); (6)
app.registerTransaction(HelloTransaction); (7)

app (8)
    .run()
    .then(() => app.logger.info('App started...'))
    .catch(error => {
        console.error('Faced error in application', error);
        process.exit(0);
    });
Please see the file on Github: hello_world/index.js.
1 Require application class, the default genesis block and the default config for the application.
2 New line: Require the newly created transaction type 'HelloTransaction'.
3 Change the label of the app.
4 If a different user other than 'lisk' was given for access to the database lisk_dev, then it is necessary to update the username in the config.
5 Replace password with the password for your database user.
6 Create the application instance.
7 New line: Register the 'HelloTransaction'.
8 The code block below starts the application and does not need to be changed.

After the 2 new lines shown above are added to your index.js file, save and close it.

5. Start the network

It should now be possible to start the customized blockchain network for the first time.

The parameter configDevnet, which is passed to the Application instance in step 3, is preconfigured to start the node with a set of dummy delegates, that have enabled forging by default.

These dummy delegates stabilize the new network, and ensure it is possible to test out the basic functionality of the network immediately with only one node.

This creates a simple Devnet, which is beneficial during development of the blockchain application.

The dummy delegates can be replaced with real delegates later. For this, users needs to create new secret accounts, and register themselves as delegates on the network. Then the account(s) with most tokens need to unvote the dummy delegates, and vote for the newly registered delegates instead.

To start the network, execute the following command shown below:

node index.js | npx bunyan -o short

Please check the logs in order to to verify that the network has started successfully.

If any problems occured, then the process should stop and an error with debug information will be displayed.

6. Interact with the network

Now the network is running, try to send a HelloTransaction to the node to see if it will be accepted.

As your blockchain process is running in your current console window, it is necessary to open a new window to proceed with the tutorial. Make sure to navigate into the root folder of your blockchain application in the new console window.

In the new terminal window, create a new folder client. This will hold the client-side scripts.

cd hello_world (1)
mkdir client (2)
cd client (3)
1 Check that the root folder of the Hello world application is open.
2 Create the folder for the client-side scripts inside the hello_world folder.
3 Navigate into the client folder.

Inside the client folder, create the file that will hold the code to create the transaction object: touch print_sendable_hello-world.js

Open the file print_sendable_hello-world.js and insert the following code:

Contents of client/print_sendable_hello-world.js
const HelloTransaction = require('../hello_transaction');
const { EPOCH_TIME } = require('@liskhq/lisk-constants');
const {getNetworkIdentifier} = require('@liskhq/lisk-cryptography');

const networkIdentifier = getNetworkIdentifier(
    "23ce0366ef0a14a91e5fd4b1591fc880ffbef9d988ff8bebf8f3666b0c09597d",
    "Lisk",
);

const getTimestamp = () => {
    // check config file or curl localhost:4000/api/node/constants to verify your epoc time
    const millisSinceEpoc = Date.now() - Date.parse(EPOCH_TIME);
    const inSeconds = ((millisSinceEpoc) / 1000).toFixed(0);
    return  parseInt(inSeconds);
}

const tx = new HelloTransaction({ (1)
    asset: {
        hello: 'world', (2)
    },
    networkIdentifier: networkIdentifier, (3)
    timestamp: getTimestamp(),
});

tx.sign('wagon stock borrow episode laundry kitten salute link globe zero feed marble');

console.log(tx.stringify()); (4)
process.exit(0); (5)
1 The desired transaction is created and signed.
2 The string 'world' is saved into the 'hello' asset.
3 The network identifier for the devnet.
4 The transaction is displayed as JSON object in the console.
5 Stops the process after the transaction object has been printed.

The following script will print the transaction in the console. (When it is executed the Python’s json.tool is used to prettify the output):

node print_sendable_hello-world.js | python -m json.tool

The generated transaction object should appear as shown below:

Signed Transaction object
{
    "id": "4938773042131394737",
    "type": 20,
    "timestamp": 117236669,
    "senderPublicKey": "c094ebee7ec0c50ebee32918655e089f6e1a604b83bcaa760293c61e0f18ab6f",
    "senderId": "16313739661670634666L",
    "fee": "100000000",
    "signature": "93af2e20b6e9d9ad1331b91abfaee5d7e1bfabd5d534ea8a13f0424e2c4fb5014b4f75e6c0dcb94508dc783d5e4c783e179839529abb122e0b3da0e5064fb000",
    "signatures": [],
    "asset": {
        "hello": "world"
    }
}

Now a sendable transaction object exists, whereby it will be sent to the node and processed. This can be seen by analyzing the logs.

To accomplish this, the API of the node is utilized and the created transaction object is posted to the transaction endpoint of the API.

As the API of every node is only accessible from the localhost by default, it is necessary to execute this query on the same server that your node is running on; unless the config was changed to make your API accessible to others or to the public.

Ensure your node is running before sending the transaction.
node print_sendable_hello-world.js | tee >(curl -X POST -H "Content-Type: application/json" -d @- localhost:4000/api/transactions) (1)
1 Posts the tx object to the node and displays it on the console.

If the node accepted the transaction, it should respond with the following:

{"meta":{"status":true},"data":{"message":"Transaction(s) accepted"},"links":{}}

To verify that the transaction was also included in the blockchain, query the database of your node, where the blockchain data is stored:

Check that the transaction is included into a block:

Use the id of your transaction object, that is posted to the node in the previous step.
curl -X GET "http://localhost:4000/api/transactions?id=2068453785229579460" | python -m json.tool
The example response of api/transactions endpoint shown below, displays details of the HelloTransaction:
{
    "meta": {
        "offset": 0,
        "limit": 10,
        "count": 1
    },
    "data": [
        {
            "id": "2068453785229579460",
            "height": 5,
            "blockId": "2192752984790234257",
            "type": 20,
            "timestamp": 117301134,
            "senderPublicKey": "5c554d43301786aec29a09b13b485176e81d1532347a351aeafe018c199fd7ca",
            "senderId": "11237980039345381032L",
            "fee": "100000000",
            "signature": "a93ff48809178310965dabf3612598f8b7bf83aaa59de28403a437d069a3589745c4d4b08efe2f3932d9e1e6abe8dc7fa08bfca80ff791f9d8e6e40a8e200502",
            "signatures": [],
            "asset": {
                "hello": "world"
            },
            "confirmations": 27
        }
    ],
    "links": {}
}

Check that the hello property is included into the account with the folowing command shown below:

curl -X GET "http://localhost:4000/api/accounts?address=11237980039345381032L" | python -m json.tool
The response of the api/accounts shown below, displays the hello:world property inside the sender’s account:
{
    "meta": {
        "offset": 0,
        "limit": 10
    },
    "data": [
        {
            "address": "11237980039345381032L",
            "publicKey": "5c554d43301786aec29a09b13b485176e81d1532347a351aeafe018c199fd7ca",
            "balance": "9999999900000000",
            "secondPublicKey": "",
            "asset": {
                "hello": "world"
            }
        }
    ],
    "links": {}
}

For further interaction with the network, it is possible to run the process in the background by executing the following commands:

cd hello_world (1)
pm2 start --name hello index.js (2)
pm2 stop hello (3)
pm2 start hello (4)
1 Navigate into the root folder of the Hello world application.
2 Add the application to pm2 under the name 'hello'.
3 Stop the hello app.
4 Start the hello app.

PM2 must be installed on the system in order to run these commands. Please see SDK Pre-Install section.

7. Customize the default configuration

Your project should have now the following file structure:

hello_world
├── client
│   └── print_sendable_hello-world.js
├── hello_transaction.js
├── index.js
├── node_modules
└── package.json

To run the script remotely change the configuration before creating the Application instance, in order to make the API accessible as shown below:

For more configuration options, please see the full list of configurations for Lisk SDK.
const { Application, genesisBlockDevnet, configDevnet} = require('lisk-sdk'); (1)
const HelloTransaction = require('./hello_transaction'); (2)

configDevnet.app.label = 'hello-world-blockchain-app'; (3)
//configDevnet.components.storage.user = '<username>'; (4)
//configDevnet.components.storage.password = 'password'; (5)

configDevnet.modules.http_api.access.public = true; (6)
//configDevnet.modules.http_api.access.whitelist.push('1.2.3.4'); (7)

const app = new Application(genesisBlockDevnet, configDevnet); (8)

app.registerTransaction(HelloTransaction); (9)

app (10)
    .run()
    .then(() => app.logger.info('App started...'))
    .catch(error => {
        console.error('Faced error in application', error);
        process.exit(0);
    });
1 Require application class, the default genesis block and the default config for the application.
2 Require the newly created transaction type 'HelloTransaction'.
3 Set the name of your blockchain application.
4 In the case whereby a different user than 'lisk' was given, to access to the database lisk_dev, it is necessary to update the username in the config.
5 Uncomment this and replace password with the password for your database user.
6 Make the API accessible from everywhere.
7 Example how to make the API accessible for specific IP addresses: add 1.2.3.4 IP address as whitelisted.
8 Create the application instance.
9 Register the 'HelloTransaction'.
10 The code block below starts the application and does not need to be changed.

Optional: After the first successful verification, the possibility exists to reduce the default console log level (info), and file log level (debug). This can be achieved by sending a copy of the config object, configDevnet with the customized config for the logger component as shown below:

configDevnet.components.logger.fileLogLevel = "error"; (1)
configDevnet.components.logger.consoleLogLevel = "none"; (2)
1 Will only display both log and fatal errors in the log file.
2 No logs will be visible in the console.

If so required, a frontend application can be designed such as the Lisk Explorer, which displays the user’s assets inside of their account page.

Please also see the following guide: Interact with the API.