Creating a module asset

How to create a new asset for a module of a blockchain application.

As defined in the Hello World application overview, the asset shall provide the following functionality:

  • Anyone can update the hello message in their account.

Sample code

View the complete sample code of this guide on GitHub in the Lisk SDK examples repository.

Prerequisites

To follow this guide, the following criteria below is assumed:

  1. Lisk Commander is installed, and a basic blockchain application is already initialized, as explained in the guide Creating a new blockchain application.

  2. a new module HelloModule is created, as explained in the previous guide Creating a module.

Generating the asset skeleton

In the root folder of the blockchain application, generate a skeleton for the new asset with Lisk Commander.

The command generate:asset expects 3 arguments:

  1. Module name: The name of the module the asset belongs to.

  2. Asset name: The name of the new asset. Needs to be a string that only consists of lower case and upper case letters [a-z, A-Z]. No numbers, hyphens, etc., are allowed.

  3. Asset ID: The ID of the asset. Needs to be unique within the module. Minimum value is 0.

For a complete overview of all available options of the generate:asset command, visit the Lisk Commander command reference.

As an example, we will add a new asset createHello to the hello module:

lisk generate:asset hello createHello 0

This will generate the following files:

Creating asset skeleton with asset name "createHello" and asset ID "0" for module "hello"
Using template "lisk-ts"
Generating asset skeleton.
Registering asset...
identical .liskrc.json
   create src/app/modules/hello/assets/create_hello_asset.ts
   create test/unit/modules/hello/assets/create_hello_asset.spec.ts

No change to package.json was detected. No package manager install will be executed.

Your asset is created and ready to use.

The file create_hello_asset.ts contains the asset skeleton and the file create_hello_asset.spec.ts contains the related unit test skeletons for the new asset.

Open the asset skeleton in create_hello_asset.ts:

src/app/modules/hello/assets/create_hello_asset.ts
import { BaseAsset, ApplyAssetContext, ValidateAssetContext } from 'lisk-sdk';

export class CreateHelloAsset extends BaseAsset {
  public name = 'createHello';
  public id = 0;

  // Define schema for asset
	public schema = {
    $id: 'hello/createHello-asset',
		title: 'CreateHelloAsset transaction asset for hello module',
		type: 'object',
		required: [],
		properties: {},
  };

  public validate({ asset }: ValidateAssetContext<{}>): void {
    // Validate your asset
  }

	// eslint-disable-next-line @typescript-eslint/require-await
  public async apply({ asset, transaction, stateStore }: ApplyAssetContext<{}>): Promise<void> {
		throw new Error('Asset "createHello" apply hook is not implemented.');
	}
}

As can be seen above, the command generate:asset already created the asset class CreateHelloAsset which contains skeletons of all important components of an asset. The only properties which are set at this point are the asset ID and the asset name, which were defined when generating the asset.

If the application is started at this point, it is already possible to create a basic createHello transaction with an empty transaction asset, sign it, and send it to the node. The node will then throw the following error:

Asset "createHello" apply hook is not implemented.

This is due to the fact that the apply() function of the asset is not implemented yet. To give the asset a purpose, it is necessary to implement certain logic inside of the apply() function of an asset.

The following sections describe an example implementation of each component of a module asset in detail.

The asset class

The asset class always extends from the BaseAsset, which is imported from the lisk-sdk package.

The properties name and id are prefilled by the values used when generating the asset skeleton was completed in the previous step.

src/app/modules/hello/assets/create_hello_asset.ts
import { BaseAsset, ApplyAssetContext, ValidateAssetContext } from 'lisk-sdk';

export class CreateHelloAsset extends BaseAsset {
  public name = 'createHello';
  public id = 0;

  // ...
}

The asset schema

The asset schema defines in which format data is sent in the transaction asset.

For more information about schemas and how they are used in the Lisk SDK, check out the Schemas.

We expect the following data in a transaction, to be able to create a new hello message:

  • helloString: The string which will be saved under helloMessage in the senders user account.

Therefore, the asset schema is adjusted accordingly as shown below:

src/app/modules/hello/assets/create_hello_asset.ts
public schema = {
    $id: 'lisk/hello/asset',
    type: 'object',
    required: ["helloString"], (1)
    properties: {
        helloString: {
            dataType: 'string', (2)
            fieldNumber: 1, (3)
            minLength: 3, (4)
            maxLength: 64, (5)
        },
    }
};
1 The property helloString is required to create a hello message.
2 string is defined as a data type for helloString.
3 The minimum length of the helloString is set to 3 characters.
4 The maximum length of the helloString is set to 64 characters.
5 The fieldNumber increments by +1 for each property in the transaction asset.

Validating the asset data

The optional function validate() validates the data of a transaction asset, before it is passed to the apply() function.

If one of these conditions is not fulfilled, then the transaction will not be processed, and an error should be thrown.

The minimum and maximum values for the different properties which are defined in The asset schema do not need to be validated again in the validate() function.

In this example, we want to validate that it is not possible to create a hello message with some illegal statement.

If any account sends a createHello transaction, with asset.helloString equal to Some illegal statement, it will throw the error Illegal hello message: Some illegal statement.

src/app/modules/hello/assets/create_hello_asset.ts
public validate({ asset }: ValidateAssetContext<{}>): void {
  if (asset.helloString == "Some illegal statement") {
      throw new Error(
          'Illegal hello message: Some illegal statement'
      );
  }
}

If the validation does not throw any errors, it means the validation has been successful, and the apply() function will be executed as the next step.

Defining the asset logic

The most important part of the module asset is the apply() function. It contains the logic of how the data in the transaction asset should be applied on the blockchain.

In this example, we use the transaction data to create a new hello message, which is added to the senders account.

Additionally, the hello counter is incremented by +1 for each applied hello transaction.

To get and set the blockchain state, the stateStore is used again, which is already known from the lifecycle hooks of the module guide.

src/app/modules/hello/assets/create_hello_asset.ts
public async apply({ asset, transaction, stateStore }: ApplyAssetContext<{}>): Promise<void> {
    // 1. Get account data of the sender of the hello transaction
    const senderAddress = transaction.senderAddress;
    const senderAccount = await stateStore.account.get(senderAddress);

    // 2. Update hello message in the senders account with thehelloString of the transaction asset
    senderAccount.hello.helloMessage = asset.helloString;
    stateStore.account.set(senderAccount.address, senderAccount);

    // 3. Get the hello counter from the database
    let counterBuffer = await stateStore.chain.get(
        CHAIN_STATE_HELLO_COUNTER
    );

    // 4. Decode the hello counter
    let counter = codec.decode(
        helloCounterSchema,
        counterBuffer
    );

    // 5. Increment the hello counter +1
    counter.helloCounter++;

    // 6. Encode the hello counter and save it back to the database
    await stateStore.chain.set(
        CHAIN_STATE_HELLO_COUNTER,
        codec.encode(helloCounterSchema, counter)
    );
}