How to create a module

How to create a new module for a Lisk blockchain client.

Complete application code

View the complete sample code of the complete "Hello World" Lisk app on GitHub in the Lisk SDK examples repository.

As defined on the Building a sidechain with the Lisk SDK page, the Hello module shall provide the following functionalities:

  1. A "Create Hello" command that stores the Hello message for the sender.

    • The Hello message has to be of type string.

    • The Hello message shall have a minimum and maximum length.

    • A blacklist of excluded words should exist. The hello message should be rejected if it includes one of the words in the blacklist.

  2. A module config, where the node operator can define:

    • maximum message length

    • minimum message length

    • word blacklist

  3. A counter, counting the total number of sent Hello messages. Anyone can request the current value of the Hello counter.

  4. Emit blockchain event immediately when a new Hello message was sent. The event includes information about the sender’s address and the Hello message.

  5. Two RPC endpoints:

    • hello_getHello: Returns the Hello message of an account. Expects the account’s address as a parameter.

    • hello_getHelloCounter: Returns the counter of total Hello messages sent in the network. Doesn’t expect any parameters.

  6. One module method:

    • hello_getHello: Works equally to the corresponding endpoint, but can be used by other modules.

Generating the module file- & folder structure

Prerequisites

To use this guide, the following criteria is assumed:

While in the root folder of the client, generate a skeleton for the new module with Lisk Commander.

The command lisk generate:module expects one argument:

  1. The module name. Needs to be unique within the client.

For a complete overview of all available options of the generate:module command, type generate:module --help.

As an example, we use the module name hello:

/hello/hello_client/
lisk generate:module hello

This will generate the following file- & folder structure:

hello/hello_client/src/app/
├── bin/
├── config/
├── src/
│    ├── app/
│    │    ├── app.ts
│    │    ├── index.ts
│    │    ├── modules/
│    │    │   └──  hello/ (1)
│    │    │      ├── endpoint.ts (2)
│    │    │      ├── events/ (3)
│    │    │      ├── method.ts (4)
│    │    │      ├── module.ts (5)
│    │    │      └── stores/ (6)
│    │    ├── modules.ts
│    │    ├── plugins/
│    │    └── plugins.ts
│    └── commands/
└── test/
    ├── integration/
    ├── network/
    └── unit/
        ├── modules/
        │    └── hello/
        │        └── modules.spec.ts (7)
        └── plugins/
1 The root folder hello/ for the module is created under src/app/modules/.
2 endpoint.ts: Contains the module endpoints. For more information on how to create endpoints for a module, check out the guide: How to create endpoints and methods.
3 events/: Contains the blockchain events of the module. This folder is empty at first. For more information on how to create a blockchain event, check out the guide: How to create a blockchain event.
4 method.ts: Contains the module methods. For more information on how to create module methods, check out the guide: How to create endpoints and methods.
5 module.ts: Contains the class of the Hello module.
6 stores/: Contains on-chain and off-chain stores of the module. This folder is empty at first. For more information on how to create a module store, check out the guide: How to create stores.
7 modules.spec.ts: Will contain unit tests for the module. For more information on how to write a unit test for a module, check out the Testing the blockchain application guide.

Lisk Commander will also automatically register the module to the client, by adding it to the file src/app/modules.ts:

hello_client/src/app/modules.ts
import { Application } from 'lisk-sdk';
import { HelloModule } from "./modules/hello/module";

export const registerModules = (app: Application): void => {
    app.registerModule(new HelloModule());
};

Now, let’s take a look at the module skeleton:

Module class & skeleton

The command generate:module already created the class HelloModule which contains skeletons for the most important components of the module.

The module class always extends from the BaseModule, which is imported from the lisk-sdk package.

However, this module is not performing any functions yet. To give the module a purpose, it is necessary to implement certain logic inside of the module.

The following guides explain how the different components of a module can be used to implement the desired logic for the module.

Module skeleton of the Hello module
hello_client/src/app/modules/hello/module.ts
import {
    BaseModule,
    ModuleInitArgs,
    InsertAssetContext,
	BlockVerifyContext,
	TransactionVerifyContext,
	VerificationResult,
	TransactionExecuteContext,
	GenesisBlockExecuteContext,
	ModuleMetadata,
	BlockExecuteContext,
	BlockAfterExecuteContext,
} from 'lisk-sdk';
import { HelloEndpoint } from './endpoint';
import { HelloMethod } from './method';

export class HelloModule extends BaseModule {
    public endpoint = new HelloEndpoint(this.stores, this.offchainStores);
    public method = new HelloMethod(this.stores, this.events);
    public commands = [];

	public constructor() {
		super();
		// registration of stores and events
	}

	public metadata(): ModuleMetadata {
		return {
			name: '',
			endpoints: [],
			commands: this.commands.map(command => ({
				name: command.name,
				params: command.schema,
			})),
			events: this.events.values().map(v => ({
				name: v.name,
				data: v.schema,
			})),
			assets: [],
		};
	}

    // Lifecycle hooks
    public async init(_args: ModuleInitArgs): Promise<void> {
		// initialize this module when starting a node
	}

	public async insertAssets(_context: InsertAssetContext) {
		// initialize block generation, add asset
	}

	public async verifyAssets(_context: BlockVerifyContext): Promise<void> {
		// verify block
	}

    // Lifecycle hooks
	public async verifyTransaction(_context: TransactionVerifyContext): Promise<VerificationResult> {
		// verify transaction will be called multiple times in the transaction pool
	}

	public async beforeCommandExecute(_context: TransactionExecuteContext): Promise<void> {
	}

	public async afterCommandExecute(_context: TransactionExecuteContext): Promise<void> {

	}
	public async initGenesisState(_context: GenesisBlockExecuteContext): Promise<void> {

	}

	public async finalizeGenesisState(_context: GenesisBlockExecuteContext): Promise<void> {

	}

	public async beforeTransactionsExecute(_context: BlockExecuteContext): Promise<void> {

	}

	public async afterTransactionsExecute(_context: BlockAfterExecuteContext): Promise<void> {

	}
}