見出し画像

Lisk SDK v6を使ったブロックチェーンアプリの作り方 その4

はじめに

こんにちは万博おじです。
今回はコマンドの作成まで行おうと思います。
前回の続きなので、見ていない方はそちらを先にどうぞ!
なお、この回で触れたファイルについては全内容を記事の最後に記載しておきますのでわからなくなったら参考にどうぞ。


モジュールの作成

参考公式ドキュメント:1. How to create a module configuration

1. モジュールの作成

以下のコマンドを実行します。

cd ~/hello
lisk generate:module hello

もしコマンドを実行後、コンソールに「? Overwrite .liskrc.json?」というメッセージが表示されたら y と入力後、ENTERキーを押しましょう。

そうすると、src/app/modules/にhelloフォルダなどが作成されます。
また、src/app/modules.tsが以下のように書き換わります。

/* eslint-disable @typescript-eslint/no-empty-function */
import { Application } from 'lisk-sdk';
import { HelloModule } from "./modules/hello/module";

// @ts-expect-error app will have typescript error for unsued variable
export const registerModules = (app: Application): void => {

    app.registerModule(new HelloModule());
};

/* eslint-disable @typescript-eslint/no-empty-function */
は不要なので削除しましょう。

モジュール作成コマンド実行後

2. オプションの追加

モジュールに対してコンフィグファイルで動きを変更できるようなオプションを提供すると使い勝手がよくなるので良いと思います。
※変更させたくない、変更する必要はないという場合は作成する必要はありません。

src/app/modules/hello/直下にschema.tsを作成し、以下を記述します。

export const configSchema = {
    $id: '/hello/config',
    type: 'object',
    properties: {
         maxMessageLength: {
            type: 'integer',
            format: 'uint32',
        },
        minMessageLength: {
            type: 'integer',
            format: 'uint32',
        },
        blacklist: {
            type: 'array',
            items: {
                type: 'string',
                minLength: 1,
                maxLength: 40,
            },
        },
    },
    required: [
        'maxMessageLength',
        'minMessageLength',
        'blacklist'
    ],
};

JSON Schemaのような形式でオプションを記載します。
なお、ここで設定している内容は以下の通りです。

  • maxMessageLength
    メッセージの最大桁数
    数値で設定

  • minMessageLength
    メッセージの最小桁数
    数値で設定

  • blacklist
    不適切と判断するメッセージ
    文字列の配列で設定
    1語あたり最小1桁、最大40桁

プログラム上で上記の内容に従った設定ができるように、src/app/modules/hello/直下にtypes.tsを作成し、以下を記述します。

import { JSONObject } from 'lisk-sdk';

export interface ModuleConfig {
    maxMessageLength: number;
    minMessageLength: number;
    blacklist: string[];
}

export type ModuleConfigJSON = JSONObject<ModuleConfig>;

schema.tsに追加したconfigSchemaに合わせてModuleConfigというインターフェースを作成しています。
configSchemaのpropertiesと内容を一致させましょう。

ここまで出来たら、src/app/modules/hello/module.tsを編集します。
編集内容は以下の通りです。
・必要なインポートを追加
・オプションの初期設定を追加
・initメソッドを修正

修正後は以下のようになります。

※はじめなので全体を記載しますが、今後は変更点のみ記載します。

/* eslint-disable class-methods-use-this */
/* eslint-disable @typescript-eslint/member-ordering */

import {
	BaseModule,
	ModuleMetadata,
	ModuleInitArgs,
	// InsertAssetContext,
	// BlockVerifyContext,
	// TransactionVerifyContext,
	// VerificationResult,
	// TransactionExecuteContext,
	// GenesisBlockExecuteContext,
	// BlockExecuteContext,
	// BlockAfterExecuteContext,
	// VerifyStatus,
	utils
} from 'lisk-sdk';
import { validator } from '@liskhq/lisk-validator';
import { HelloEndpoint } from './endpoint';
import { HelloMethod } from './method';
import { configSchema } from './schema';
import { ModuleConfigJSON } from './types';

export const defaultConfig = {
	maxMessageLength: 256,
	minMessageLength: 3,
	blacklist: ["illegalWord1"]
};

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();
	// 	// registeration of stores and events
	// }

	public metadata(): ModuleMetadata {
		return {
			...this.baseMetadata(),
			endpoints: [],
			assets: [],
		};
	}

	// Lifecycle hooks
	public async init(args: ModuleInitArgs): Promise<void> {
        // Get the module config defined in the config.json of the node
        const { moduleConfig } = args;
        // Overwrite the default module config with values from config.json, if set
        const config = utils.objects.mergeDeep({}, defaultConfig, moduleConfig) as ModuleConfigJSON;
        // Validate the config with the config schema
        validator.validate<ModuleConfigJSON>(configSchema, config);
	}

	// 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
	// return { status: VerifyStatus.OK };
	// }

	// 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> {

	// }
}

[defaultConfig]
オプションの初期設定です。
・最大桁数:256
・最小桁数:3
・ブラックリスト:illegalWord1

[initメソッド]
const { moduleConfig } = args;
引数からコンフィグファイルの設定を取得します
const config = utils.objects.mergeDeep(…);
コンフィグファイルに設定があれば初期設定を上書きします
validator.validate<ModuleConfigJSON>(…);
設定値が有効かをチェックします
schema.tsに従い型や桁数のチェックをおこないます
※ブラックリストの内容チェックは実施されません

変更箇所:インポートとdefaultConfig
変更箇所:initメソッド

オプションの初期設定を変更する場合は、前回作成したcustom_config.jsonのmodulesにhelloに対する設定を追加します。
以下、設定例です。

{
	~ 記載省略 ~
	"modules": {
		"hello": {
			"maxMessageLength": 100,
			"minMessageLength": 5,
			"blacklist": ["hoge", "fuga"]
		}
	},
	~ 記載省略 ~
}

設定内容は以下の通りです。
・最大桁数を100に設定
・最小桁数を5に設定
・ブラックリストを hoge および fuga に設定

ストアの作成

参考公式ドキュメント:2. How to create stores

1. オンチェーンストアの作成

コマンドの実行によりオンチェーンストアに登録される内容を定義します。
公式ドキュメントでは2つ作成していますが、この記事では1つだけ作成します。

src/app/modules/hello/stores/直下にmessage.tsを作成し、以下を記述します。

import { BaseStore } from 'lisk-sdk';

export interface MessageStoreData {
	message: string;
}

export const messageStoreSchema = {
	$id: '/hello/message',
	type: 'object',
	required: ['message'],
	properties: {
		message: {
			dataType: 'string',
			fieldNumber: 1,
		},
	},
};

export class MessageStore extends BaseStore<MessageStoreData> {
	public schema = messageStoreSchema;
}

モジュールの作成でも出てきたschemaおよびtypesに記載したような内容をまとめて記載していますが、schema.tsやtypes.tsに記述しmessage.tsにインポートしても良いです。

src/app/modules/hello/module.tsに作成したmessage.tsを登録します。

~ 記載省略 ~
import { MessageStore } from './stores/message';
~ 記載省略 ~
export class HelloModule extends BaseModule {
~ 記載省略 ~
	public constructor() {
		super();
		// registeration of stores and events
		this.stores.register(MessageStore, new MessageStore(this.name, 0));
	}
~ 記載省略 ~
変更箇所:インポートとconstructor

コマンドの作成

参考公式ドキュメント:3. How to create a command

1. コマンドパラメータ用のスキーマを作成

src/app/modules/hello/schema.tsに以下を追加します。

export const createHelloSchema = {
	$id: 'hello/createHello-params',
	title: 'CreateHelloCommand transaction parameter for the Hello module',
	type: 'object',
	required: ['message'],
	properties: {
		message: {
			dataType: 'string',
			fieldNumber: 1,
			minLength: 3,
			maxLength: 256,
		},
	},
};

オンチェーンストアに保存するのはメッセージのみのため、コマンド実行時のパラメータは「message」のみとしています。
なお、メッセージの最小桁数、最大桁数は、モジュールのオプションの初期設定値と同様にしておきましょう。

2. コマンドの作成

以下のコマンドを実行します。

cd ~/hello
lisk generate:command hello createHello

もしコマンドを実行後、コンソールに「? Overwrite .liskrc.json?」というメッセージが表示されたら y と入力後、ENTERキーを押しましょう。

そうすると、src/app/modules/hello/にcommandsフォルダなどが作成されます。
また、src/app/modules/hello/module.tsが書き換わります。

module.tsの変更内容
・createHelloCommandがインポートされる
・commands変数にcreateHelloCommandが追加される
※インポートの順番が変わる場合があります。

変更箇所:インポートとcommands変数

src/app/modules/hello/commands/create_hello_command.tsを以下のように修正します。

/* eslint-disable class-methods-use-this */
import {
    BaseCommand,
    CommandVerifyContext,
    CommandExecuteContext,
	VerificationResult,
	VerifyStatus,
} from 'lisk-sdk';
import { createHelloSchema } from '../schema';
import { MessageStore } from '../stores/message';
import { ModuleConfig } from '../types';

interface Params {
	message: string;
}

export class CreateHelloCommand extends BaseCommand {
	public schema = createHelloSchema;
	private _blacklist!: string[];

    public async init(config: ModuleConfig): Promise<void> {
		// Set _blacklist to the value of the blacklist defined in the module config
		this._blacklist = config.blacklist;
		// Set the max message length to the value defined in the module config
		this.schema.properties.message.maxLength = config.maxMessageLength;
		// Set the min message length to the value defined in the module config
		this.schema.properties.message.minLength = config.minMessageLength;
	}

	// eslint-disable-next-line @typescript-eslint/require-await
	public async verify(context: CommandVerifyContext<Params>): Promise<VerificationResult> {
        let validation: VerificationResult;
        const wordList = context.params.message.split(" ");
        const found = this._blacklist.filter(value => wordList.includes(value));
        if (found.length > 0) {
            context.logger.info("==== FOUND: Message contains a blacklisted word ====");
            throw new Error(
                `Illegal word in hello message: ${  found.toString()}`
            );
        } else {
            context.logger.info("==== NOT FOUND: Message contains no blacklisted words ====");
            validation = {
                status: VerifyStatus.OK
            };
        }
        return validation;
    }

	public async execute(context: CommandExecuteContext<Params>): Promise<void> {
        // 1. Get account data of the sender of the Hello transaction.
        const { senderAddress } = context.transaction;
        // 2. Get message store.
        const messageSubstore = this.stores.get(MessageStore);
        // 3. Save the Hello message to the message store, using the senderAddress as key, and the message as value.
        await messageSubstore.set(context, senderAddress, {
            message: context.params.message,
        });
    }
}

[Params]
コマンドのパラメータを設定しす。
コマンドパラメータ用のスキーマに合わせて記載しています。

[schema変数]
スキーマを設定します。
schema.tsのcreateHelloSchemaを設定しています。

[_blacklist変数]
オプションで指定したブラックリストを保持する変数です。

[initメソッド]
コマンドの初期設定を行うメソッドです。
schema.tsのcreateHelloSchemaの最大桁数、最小桁数をモジュールのオプションで上書きしています。
また、オプションのブラックリストを取得しています。

[verifyメソッド]
コマンドを実行しても良いかを判断するメソッドです。
メッセージ内にブラックリストに指定されている単語が含まれている場合はエラーとしています。

[executeメソッド]
コマンドを実行するメソッドです。
トランザクションを送信したアドレスをキーとし、メッセージをオンチェーンストアに保存しています。

3. module.tsの修正

src/app/modules/hello/module.tsのinitメソッドを以下のように変更します。

public async init(args: ModuleInitArgs): Promise<void> {
	// Get the module config defined in the config.json of the node
	const { moduleConfig } = args;
	// Overwrite the default module config with values from config.json, if set
	const config = utils.objects.mergeDeep({}, defaultConfig, moduleConfig) as ModuleConfigJSON;
	// Validate the config with the config schema
	validator.validate<ModuleConfigJSON>(configSchema, config);
	// Call the command init() method with config as parameter
	this.commands[0].init(config).catch(err => {
		console.log("Error: ", err);
	});
}

this.commands[0].init(config).catch(….)でコマンドでエラーが発生した場合に処理を終了するようにしています。

src/app/modules/hello/module.tsのverifyTransactionメソッドを以下のように変更します。

public async verifyTransaction(context: TransactionVerifyContext): Promise<VerificationResult> {
	// verify transaction will be called multiple times in the transaction pool
	context.logger.info('TX VERIFICATION');
	return { status: VerifyStatus.OK };
}

また、以下のインポートがコメントアウトされているはずなので、コメントを解除します。
・TransactionVerifyContext
・VerificationResult
・VerifyStatus

変更箇所:initメソッドとverifyTransactionメソッド

コマンドの実行

ここまで出来たらビルド後にブロックチェーンアプリを実行し、ダッシュボード(http://localhost:4005)を開きましょう。
アプリ実行は前回PM2の導入を行っている方は以下のコマンドです。

cd ~/hello
npm run build
pm2 start pm2_config.json

画面左上のCurrent heightが10秒ごとに増えていることを確認しましょう。

確認できたら、画面中央あたりにある「Invoke command」を以下の画像のように設定し、Submitボタンを押しましょう。

画像ではあえてパスフレーズを伏せています。
入力するパスフレーズは ~/hello/config/default/passphrase.json に記載されているものを使いましょう。

helloモジュールのcreateHelloコマンドを実行

Success!と言われたらOKです。

コマンド送信成功!やったね!

なお、「Invoke command」の上にある「Recent Transactions」でトランザクションが発行されたこともわかります。

トランザクションが発行されましたね!

おわりに

コマンドを作成してコマンド実行するところまでやりました。
思ったより長くなりましたが、実際にコーディングしている量はかなり少ないのでのんびり確認しながら作成してみてください。

なお、コマンド実行時に指定したメッセージが正しくオンチェーンストアに保存されたかどうかの確認は次回のお楽しみということで今回はここまでです。
お疲れさまでした!

おまけ:今回触ったソースの全内容

src/app/modules/hello/module.ts

/* eslint-disable class-methods-use-this */
/* eslint-disable @typescript-eslint/member-ordering */

import { validator } from '@liskhq/lisk-validator';
import {
	BaseModule,
	ModuleInitArgs,
	ModuleMetadata,
	// InsertAssetContext,
	// BlockVerifyContext,
	TransactionVerifyContext,
	VerificationResult,
	// TransactionExecuteContext,
	// GenesisBlockExecuteContext,
	// BlockExecuteContext,
	// BlockAfterExecuteContext,
	VerifyStatus,
	utils,
} from 'lisk-sdk';
import { CreateHelloCommand } from './commands/create_hello_command';
import { HelloEndpoint } from './endpoint';
import { HelloMethod } from './method';
import { configSchema } from './schema';
import { MessageStore } from './stores/message';
import { ModuleConfigJSON } from './types';

export const defaultConfig = {
	maxMessageLength: 256,
	minMessageLength: 3,
	blacklist: ['illegalWord1'],
};

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

	public constructor() {
		super();
		// registeration of stores and events
		this.stores.register(MessageStore, new MessageStore(this.name, 0));
	}

	public metadata(): ModuleMetadata {
		return {
			...this.baseMetadata(),
			endpoints: [],
			assets: [],
		};
	}

	// Lifecycle hooks
	public async init(args: ModuleInitArgs): Promise<void> {
		// Get the module config defined in the config.json of the node
		const { moduleConfig } = args;
		// Overwrite the default module config with values from config.json, if set
		const config = utils.objects.mergeDeep({}, defaultConfig, moduleConfig) as ModuleConfigJSON;
		// Validate the config with the config schema
		validator.validate<ModuleConfigJSON>(configSchema, config);
		// Call the command init() method with config as parameter
		this.commands[0].init(config).catch(err => {
			console.log('Error: ', err);
		});
	}

	// 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
		context.logger.info('TX VERIFICATION');
		return { status: VerifyStatus.OK };
	}

	// 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> {

	// }
}

src/app/modules/hello/schema.ts

export const configSchema = {
	$id: '/hello/config',
	type: 'object',
	properties: {
		maxMessageLength: {
			type: 'integer',
			format: 'uint32',
		},
		minMessageLength: {
			type: 'integer',
			format: 'uint32',
		},
		blacklist: {
			type: 'array',
			items: {
				type: 'string',
				minLength: 1,
				maxLength: 40,
			},
		},
	},
	required: ['maxMessageLength', 'minMessageLength', 'blacklist'],
};

export const createHelloSchema = {
	$id: 'hello/createHello-params',
	title: 'CreateHelloCommand transaction parameter for the Hello module',
	type: 'object',
	required: ['message'],
	properties: {
		message: {
			dataType: 'string',
			fieldNumber: 1,
			minLength: 3,
			maxLength: 256,
		},
	},
};

src/app/modules/hello/types.ts

import { JSONObject } from 'lisk-sdk';

export interface ModuleConfig {
	maxMessageLength: number;
	minMessageLength: number;
	blacklist: string[];
}

export type ModuleConfigJSON = JSONObject<ModuleConfig>;

src/app/modules/hello/stores/message.ts

import { BaseStore } from 'lisk-sdk';

export interface MessageStoreData {
	message: string;
}

export const messageStoreSchema = {
	$id: '/hello/message',
	type: 'object',
	required: ['message'],
	properties: {
		message: {
			dataType: 'string',
			fieldNumber: 1,
		},
	},
};

export class MessageStore extends BaseStore<MessageStoreData> {
	public schema = messageStoreSchema;
}

src/app/modules/hello/commands/create_hello_command.ts

/* eslint-disable class-methods-use-this */
import {
	BaseCommand,
	CommandVerifyContext,
	CommandExecuteContext,
	VerificationResult,
	VerifyStatus,
} from 'lisk-sdk';
import { createHelloSchema } from '../schema';
import { MessageStore } from '../stores/message';
import { ModuleConfig } from '../types';

interface Params {
	message: string;
}

export class CreateHelloCommand extends BaseCommand {
	public schema = createHelloSchema;
	private _blacklist!: string[];

	public async init(config: ModuleConfig): Promise<void> {
		// Set _blacklist to the value of the blacklist defined in the module config
		this._blacklist = config.blacklist;
		// Set the max message length to the value defined in the module config
		this.schema.properties.message.maxLength = config.maxMessageLength;
		// Set the min message length to the value defined in the module config
		this.schema.properties.message.minLength = config.minMessageLength;
	}

	// eslint-disable-next-line @typescript-eslint/require-await
	public async verify(context: CommandVerifyContext<Params>): Promise<VerificationResult> {
		let validation: VerificationResult;
		const wordList = context.params.message.split(' ');
		const found = this._blacklist.filter(value => wordList.includes(value));
		if (found.length > 0) {
			context.logger.info('==== FOUND: Message contains a blacklisted word ====');
			throw new Error(`Illegal word in hello message: ${found.toString()}`);
		} else {
			context.logger.info('==== NOT FOUND: Message contains no blacklisted words ====');
			validation = {
				status: VerifyStatus.OK,
			};
		}
		return validation;
	}

	public async execute(context: CommandExecuteContext<Params>): Promise<void> {
		// 1. Get account data of the sender of the Hello transaction.
		const { senderAddress } = context.transaction;
		// 2. Get message store.
		const messageSubstore = this.stores.get(MessageStore);
		// 3. Save the Hello message to the message store, using the senderAddress as key, and the message as value.
		await messageSubstore.set(context, senderAddress, {
			message: context.params.message,
		});
	}
}


いいなと思ったら応援しよう!