Lisk SDK v6を使ったブロックチェーンアプリの作り方 その4
はじめに
こんにちは万博おじです。
今回はコマンドの作成まで行おうと思います。
前回の続きなので、見ていない方はそちらを先にどうぞ!
なお、この回で触れたファイルについては全内容を記事の最後に記載しておきますのでわからなくなったら参考にどうぞ。
モジュールの作成
1. モジュールの作成
以下のコマンドを実行します。
cd ~/hello
lisk generate:module hello
そうすると、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());
};
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>;
ここまで出来たら、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> {
// }
}
オプションの初期設定を変更する場合は、前回作成したcustom_config.jsonのmodulesにhelloに対する設定を追加します。
以下、設定例です。
{
~ 記載省略 ~
"modules": {
"hello": {
"maxMessageLength": 100,
"minMessageLength": 5,
"blacklist": ["hoge", "fuga"]
}
},
~ 記載省略 ~
}
ストアの作成
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;
}
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));
}
~ 記載省略 ~
コマンドの作成
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,
},
},
};
2. コマンドの作成
以下のコマンドを実行します。
cd ~/hello
lisk generate:command hello createHello
そうすると、src/app/modules/hello/にcommandsフォルダなどが作成されます。
また、src/app/modules/hello/module.tsが書き換わります。
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,
});
}
}
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);
});
}
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
コマンドの実行
ここまで出来たらビルド後にブロックチェーンアプリを実行し、ダッシュボード(http://localhost:4005)を開きましょう。
アプリ実行は前回PM2の導入を行っている方は以下のコマンドです。
cd ~/hello
npm run build
pm2 start pm2_config.json
画面左上のCurrent heightが10秒ごとに増えていることを確認しましょう。
確認できたら、画面中央あたりにある「Invoke command」を以下の画像のように設定し、Submitボタンを押しましょう。
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,
});
}
}