見出し画像

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

はじめに

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


モジュールのエンドポイントの作成

参考公式ドキュメント:4. How to create endpoints and methods

1. エンドポイントの作成

コマンドでオンチェーンストアに登録されたmessageを取得するエンドポイントを作成するため、src/app/modules/hello/endpoint.tsを以下のように編集します。

参考公式ドキュメントではカウンタを取得するエンドポイントについても記載がありますが、前回の記事でコマンドをオンチェーンストアに登録するのを省略しているのでここでも省略します。

import { BaseEndpoint, ModuleEndpointContext, cryptography } from 'lisk-sdk';
import { MessageStore, MessageStoreData } from './stores/message';

export class HelloEndpoint extends BaseEndpoint {
    public async getHello(ctx: ModuleEndpointContext): Promise<MessageStoreData> {
        // 1. Get message store
        const messageSubStore = this.stores.get(MessageStore);
        // 2. Get the address from the endpoint params
        const { address } = ctx.params;
        // 3. Validate address
        if (typeof address !== 'string') {
            throw new Error('Parameter address must be a string.');
        }
        cryptography.address.validateLisk32Address(address);
        // 4. Get the Hello message for the address from the message store
        const helloMessage = await messageSubStore.get(
            ctx,
            cryptography.address.getAddressFromLisk32Address(address),
        );
        // 5. Return the Hello message
        return helloMessage;
    }
}

getHelloメソッドは以下のことをしています。

[const messageSubStore = this.stores.get(MessageStore);]
オンチェーンストア「MessageStore」を取得。

[const { address } = ctx.params;]
エンドポイントのパラメータより「MessageStore」のキーであるアドレスを取得。

[if (typeof address !== 'string') {…}]
取得したアドレスが文字列かどうかを判定し、文字列でなければエラー終了。

[cryptography.address.validateLisk32Address(address);]
Lisk SDKに含まれるlisk-cryptographyのvalidateLisk32Addressにて取得したアドレスが正しいアドレスかどうかを判定。
正しくなければエラー終了。

[const helloMessage = await messageSubStore.get(…);]
取得したアドレスをキーにMessageStoreからコマンドにより登録されたmessageを取得。

return helloMessage;
呼び出し元へmessageを返却。

2. エンドポイント用のスキーマの作成

エンドポイントを実行する際のリクエストパラメータおよびエンドポイントの実行結果(レスポンス)を設定するため、src/app/modules/hello/schema.tsに以下を追加します。

export const getHelloRequestSchema = {
	$id: 'modules/hello/endpoint/getHelloRequest',
	type: 'object',
	required: ['address'],
	properties: {
		address: {
			type: 'string',
			format: 'lisk32',
		},
	},
};

export const getHelloResponseSchema = {
	$id: 'modules/hello/endpoint/getHello',
	type: 'object',
	required: ['message'],
	properties: {
		message: {
			type: 'string',
			format: 'utf8'
		},
	},
};

[getHelloRequestSchema]
エンドポイント「getHello」ではMessageStoreからmessageを取得するためにキーとしてアドレスを使用しています。
そのため、リクエスト用のスキーマとしてはアドレスに対する定義が必要になります。

[getHelloResponseSchema]
エンドポイント「getHello」は処理結果としてアドレスをキーにMessageStoreに保存されているmessageを返却します。
そのため、レスポンス用のスキーマとしてはmessageに対する定義が必要になります。

3. エンドポイントを使用するための設定

エンドポイントを作成しただけでは使用できないため、src/app/modules/hello/module.ts を以下のように編集します。

~記載省略~
import { configSchema, getHelloRequestSchema, getHelloResponseSchema } from './schema';
~記載省略~
	public metadata(): ModuleMetadata {
		return {
			endpoints: [
				{
					name: this.endpoint.getHello.name,
					request: getHelloRequestSchema,
					response: getHelloResponseSchema,
				},
			],
			commands: this.commands.map(command => ({
				name: command.name,
				params: command.schema,
			})),
			events: this.events.values().map(v => ({
				name: v.name,
				data: v.schema,
			})),
			assets: [],
			stores: [],
		};
	}
~記載省略~
インポートの追加
metadataメソッドの修正

エンドポイントの実行

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

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

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

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

前回、コマンドを実行する際に指定したパスフレーズが、~/hello/config/default/passphrase.json に記載されているものだった場合、アドレスは ~/hello/config/default/dev-validators.json に記載されている1つ目のアドレスを指定しましょう。

helloモジュールのgetHelloエンドポイントを実行

Success!と言われ、コマンドを実行した際に設定したmessageが表示されたらOKです。

イベント実行成功!いぇーい!

コマンドを実行する際のmessageを変更後に再度エンドポイントを実行すると返される結果も変わるので何度か試してみてね!
なお、オンチェーンストアに登録されるのはブロック生成後なので、反映まで最長10秒程度かかります。

ぬるぽ

おわりに

今回はエンドポイントの作成をしましたがいかがでしたでしょうか?
次回はイベントとプラグインの作成あたりを予定していますが、正直なところ、ここまで出来れば最低限のブロックチェーンアプリが作成出来るようになったと言っても過言ではありません!
フロントエンドを作成すれば公開することも出来ると思います。
なんか作りたいなーと思っている人はとりあえずここまでを目標に頑張ってみてはいかがでしょうか?

ということでお疲れさまでした!

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

src/app/modules/hello/endpoint.ts

import { BaseEndpoint, ModuleEndpointContext, cryptography } from 'lisk-sdk';
import { MessageStore, MessageStoreData } from './stores/message';

export class HelloEndpoint extends BaseEndpoint {
    public async getHello(ctx: ModuleEndpointContext): Promise<MessageStoreData> {
        // 1. Get message store
        const messageSubStore = this.stores.get(MessageStore);
        // 2. Get the address from the endpoint params
        const { address } = ctx.params;
        // 3. Validate address
        if (typeof address !== 'string') {
            throw new Error('Parameter address must be a string.');
        }
        cryptography.address.validateLisk32Address(address);
        // 4. Get the Hello message for the address from the message store
        const helloMessage = await messageSubStore.get(
            ctx,
            cryptography.address.getAddressFromLisk32Address(address),
        );
        // 5. Return the Hello message
        return helloMessage;
    }
}

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,
		},
	},
};

export const getHelloRequestSchema = {
	$id: 'modules/hello/endpoint/getHelloRequest',
	type: 'object',
	required: ['address'],
	properties: {
		address: {
			type: 'string',
			format: 'lisk32',
		},
	},
};

export const getHelloResponseSchema = {
	$id: 'modules/hello/endpoint/getHello',
	type: 'object',
	required: ['message'],
	properties: {
		message: {
			type: 'string',
			format: 'utf8'
		},
	},
};

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, getHelloRequestSchema, getHelloResponseSchema } 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 {
			endpoints: [
				{
					name: this.endpoint.getHello.name,
					request: getHelloRequestSchema,
					response: getHelloResponseSchema,
				},
			],
			commands: this.commands.map(command => ({
				name: command.name,
				params: command.schema,
			})),
			events: this.events.values().map(v => ({
				name: v.name,
				data: v.schema,
			})),
			assets: [],
			stores: [],
		};
	}

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

	// }
}

この記事が気に入ったらサポートをしてみませんか?