Lisk SDKでブロックチェーンアプリケーションを作る

# はじめに
[Lisk](https://lisk.com/)って名前を知っていても[Lisk SDK](https://lisk.com/documentation/lisk-sdk/index.html)に触れたことがある人は少ないのでは、ということで今回記事にしました。
丁度いいところに、[アドベントカレンダー](https://qiita.com/advent-calendar/2021/blockchain)もあったしね。
構築手順を書いているので少し長いですがお付き合いください。

https://lisk.com/


# Lisk SDK ってなんぞ
ブロックチェーンとその上で動作するアプリケーション(ブロックチェーンアプリケーション)を構築するための開発キット。
これを使用して作成されたアプリケーションは独自のブロックチェーン上で動作します。
また、将来的にサイドチェーンとしてLiskにつながることが可能になります。
なお、JavaScript、TypeScriptで構築できるので非常に楽。(いや、ホントに)

:::note info
サイドチェーンとしてLiskと繋ぐ部分(インターオペラビリティ周り)はまだ開発中
サイドチェーンとなる必要がないなら、今のままでも新プロジェクトとして立ち上げられるくらいの実力有り
:::

https://github.com/LiskHQ/lisk-sdk

https://lisk.com/documentation/lisk-sdk/index.html


# 推奨構築環境
### OS

- ubuntu 
18.04 (LTS) 
20.04 (LTS)

- mac 
10.13 (High Sierra) 
10.14 (Mojave) 
10.15 (Catalina) 
11.04 (Big Sur)

:::note warn
Windowsは残念ながら...
:::

### RAM
2GB以上

### Node.js
v12.22.7 (NPM version 6.14.15)

:::note warn
v12系以外を使用するとうまく動作しません
:::


# 構築方法

### 1. 下準備
とりあえずこの辺いれとく

```
sudo apt-get install curl wget tar unzip zip ntp jq git build-essential
```

### 2. Lisk Commander の導入

```
npm i -g lisk-commander
```

:::note info
とても便利なCLI
入れなくても構築できるが、入れておいて損はなし
今回の記事では入れている前提
:::

### 3. ブロックチェーンアプリケーションのプロジェクトを作成

```
lisk init omikuji
```

:::note info
lisk init [アプリケーション名]
:::

### 4. 作成したプロジェクトを開く

:::note info
以降 Visual Studio Codeなどを使って作業するのをおすすめ
:::

![1_init.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2342376/964feac9-d759-9524-0f77-53bc2bac8fdb.png)

### 5. 便利なプラグインの追加

以下のファイルを編集

``` typescript:/src/app/plugin.ts
import { Application, HTTPAPIPlugin } from 'lisk-sdk';    // add HTTPAPIPlugin
import { DashboardPlugin } from '@liskhq/lisk-framework-dashboard-plugin';    // add DashboardPlugin

// @ts-expect-error Unused variable error happens here until at least one module is registered
export const registerPlugins = (app: Application): void => {
    app.registerPlugin(HTTPAPIPlugin);    // register HTTPAPIPlugin
    app.registerPlugin(DashboardPlugin);    // register DashboardPlugin
};
```

:::note info
HTTPAPIPlugin：ブラウザ等でAPIをたたきたいときに必須
DashboardPlugin：構築したアプリケーションの動作確認に使用できるので開発中は入れておくと便利
:::


### 6. モジュールとアセットの作成

:::note info
Lisk SDKではモジュールとアセットで1つのトランザクションとして扱われます。
:::

**Lisk標準のモジュール・アセット(一部抜粋)**

| moduleID | assetID | name |
|:--------:|:-------:|:----:|
| 2 | 0 | token:transfer |
| 5 | 0 | dpos:register|
| 5 | 1 | dpos:vote|
| 5 | 2 | dpos:unlock|

今回はおみくじを作りたいので、以下のような感じにします。

| moduleID | assetID | name |
|:--------:|:-------:|:----:|
| 3535 | 0 | omikuji:pull|

ということで

```
lisk generate:module omikuji 3535
lisk generate:asset omikuji pull 0
```

:::note info
lisk generate:module [モジュール名] [モジュールID]
lisk generate:asset [モジュール名] [アセット名] [アセットID]
:::

ここまでやると以下のファイルが作成されます。

``` typescript:/src/app/modules/omikuji/omikuji_module.ts
/* eslint-disable class-methods-use-this */
import {
    AfterBlockApplyContext,
    AfterGenesisBlockApplyContext, BaseModule,
    BeforeBlockApplyContext, TransactionApplyContext
} from 'lisk-sdk';
import { PullAsset } from "./assets/pull_asset";

export class OmikujiModule extends BaseModule {

    ......

    public name = 'omikuji';
    public transactionAssets = [new PullAsset()];
    public events = [
        // Example below
        // 'omikuji:newBlock',
    ];
    public id = 3535;

    ......

    public async afterGenesisBlockApply(_input: AfterGenesisBlockApplyContext) {
        // Get any data from genesis block, for example get all genesis accounts
        // const genesisAccounts = genesisBlock.header.asset.accounts;
    }
}

```


``` typescript:/src/app/modules/omikuji/asset/pull_asset.ts
import { BaseAsset, ApplyAssetContext, ValidateAssetContext } from 'lisk-sdk';

export class PullAsset extends BaseAsset {
    public name = 'pull';
    public id = 0;

    // Define schema for asset
    public schema = {
        $id: 'omikuji/pull-asset',
        title: 'PullAsset transaction asset for omikuji 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 "pull" apply hook is not implemented.');
    }
}
```

また、以下のファイルに作成されたモジュールが自動で追加されます。

``` typescript:/src/app/modules.ts
import { Application } from 'lisk-sdk';
import { OmikujiModule } from "./modules/omikuji/omikuji_module";    // add

// @ts-expect-error Unused variable error happens here until at least one module is registered
export const registerModules = (app: Application): void => {
    app.registerModule(OmikujiModule);    // add
};
```

### 7. モジュールとアセットの編集
あとは自由にどんなアプリケーションにしたいか考えながら作成されたファイルを編集していきます。
今回のおみくじは以下のような仕様

- 1LSK以上つかってお願いすると「大吉、吉、中吉、小吉、末吉、凶、大凶」を教えてくれる
- 1LSK未満だと神様に怒られる
- おみくじの結果はアカウント情報のomikuji.kekkaに設定される
- おみくじを引く際のパラメータは {"onegai": 数値}

``` typescript:/src/app/modules/omikuji/omikuji_module.ts
/* eslint-disable class-methods-use-this */
import {
    AfterBlockApplyContext,
    AfterGenesisBlockApplyContext, BaseModule,
    BeforeBlockApplyContext, TransactionApplyContext
} from 'lisk-sdk';
import { PullAsset } from "./assets/pull_asset";

export class OmikujiModule extends BaseModule {
    public actions = {};
    public reducers = {};
    public name = 'omikuji';
    public transactionAssets = [new PullAsset()];
    public events = [];
    public id = 3535;

    // アカウント情報に追加するおみくじの結果を設定するためのスキーマの設定
    public accountSchema = {
        type: 'object',
        properties: {
            kekka: {
                fieldNumber: 1,
                dataType: 'string',
                maxLength: 64,
            },
        },
        default: {
            kekka: '',
        },
    };

    // Lifecycle hooks
    public async beforeBlockApply(_input: BeforeBlockApplyContext) {}
    public async afterBlockApply(_input: AfterBlockApplyContext) {}
    public async beforeTransactionApply(_input: TransactionApplyContext) {}
    public async afterTransactionApply(_input: TransactionApplyContext) {}
    public async afterGenesisBlockApply(_input: AfterGenesisBlockApplyContext) {}
}
```

``` typescript:/src/app/modules/omikuji/asset/pull_asset.ts
import { BaseAsset, ApplyAssetContext, ValidateAssetContext, cryptography } from 'lisk-sdk';

export class PullAsset extends BaseAsset {
    public name = 'pull';
      public id = 0;

    // トランザクション実行時のパラメータ用のスキーマを設定
    public schema = {
        $id: 'omikuji/pull-asset',
        title: 'PullAsset transaction asset for omikuji module',
        type: 'object',
        required: ["onegai"],
        properties: {
            onegai: {
                dataType: 'uint64', 
                fieldNumber: 1,
            },
        },
    };

    public validate({ asset }: ValidateAssetContext<{onegai:BigInt}>): void {
        // 1LSK未満なら怒る
        if (asset.onegai < BigInt(100000000)) {
            throw new Error(
                '1LSKくらいはらうのじゃー'
            );
        }
    }

    // eslint-disable-next-line @typescript-eslint/require-await
    public async apply({ asset, transaction, reducerHandler, stateStore }: ApplyAssetContext<{onegai:BigInt}>): Promise<void> {
        // おみくじ
        const kuji = ["大吉","吉","中吉","小吉", "末吉", "凶", "大凶"];
        const kekka = kuji[parseInt(cryptography.bufferToHex(transaction.id).slice(0, 8), 16) % kuji.length];

        // 送信者の情報に結果を設定
        const senderAddress = transaction.senderAddress;
        const senderAccount:{address: Buffer, omikuji:{kekka:string}} = await stateStore.account.get(senderAddress);
        senderAccount.omikuji.kekka = kekka;    
        stateStore.account.set(senderAccount.address, senderAccount);

        // 送信者の残高からお願いに使ったLSKを減らす
        await reducerHandler.invoke("token:debit", {
            address: senderAddress,
            amount: asset.onegai,
        });
    }
}
```

### 8. Genesis Blockの作成準備

```
./bin/run genesis-block:create --output config/default
```

**A genesis_block file already exists at the given location. Do you want to overwrite it?**と聞かれたら**y**
すると、config/defaultに以下のファイルが生成されます

- accounts.json
- forging_info.json
- genesis_block.json
- password.json

:::note alert
config/default内の各JSONファイル(config.jsonを含む)はGitHubなどで公開する際は十分気をつけましょう。
ここで生成されるファイルを公開し、そのままアプリをリリースしようものなら大変なことになります。
絶対にアカウントのパスフレーズや、パスワードは公開してはいけません。
:::

![2_create-genesis.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2342376/e7ee3f28-f0bf-f483-6cab-b1332d1c3e6f.png)

forging_info.jsonおよびpassword.jsonの内容をconfig.jsonに反映

```
tmp=$(mktemp)
jq '.forging.delegates = input' config/default/config.json config/default/forging_info.json > "$tmp" && mv "$tmp" config/default/config.json
jq '.forging += input' config/default/config.json config/default/password.json > "$tmp" && mv "$tmp" config/default/config.json
```

### 9. ブロックチェーンアプリケーションの起動

```
./bin/run start --api-ws
```

### 10. 動作確認
ブラウザから http://localhost:4005 にアクセス
![3_dashboad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2342376/29913c38-aaec-ed6b-89f7-91927f9196a9.png)

Send transactionから作成したモジュール・アセットを選択して「Submit」
![9_omikuji.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2342376/bb6239c9-ac94-be39-6c3d-3da6f35425bb.png)

:::note info
動作確認の際は accounts.json に記載されているものを使用すると楽
なお、1LSK = 100,000,000
:::

成功するとこんな感じ
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2342376/11e9dd5e-1be9-9f52-f9b8-67f9613049aa.png)

onegai を 1LSK未満にすると怒られる
![10_kekka_fail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2342376/c8871e51-e814-5ab3-bf16-a22eb39f4551.png)

HttpAPIで成功した際のトランザクションIDを確認してみる
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2342376/bb5e4f32-8e1d-156a-f6f3-72358737f4da.png)

HttpAPIでおみくじの結果(送信者のアカウント情報)を確認してみる
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2342376/ec6aa171-f242-ceff-5d4c-705788692d54.png)

「大凶」だったみたいです...

# ということで
SDKの機能の1割も使ってないんじゃないかというくらい簡単なアプリでしたがなんとなくイメージは掴めたでしょうか？
必要スペックも低い、ブロックチェーン構築も楽、アプリケーションの実装も楽
言うことなしだと思いません！？
ぜひ触ってみてくださいね！

なお、開発で困ったことがあれば [Lisk 公式Discord](https://lisk.chat) や、[Lisk Dev forum](https://dev.lisk.com/) で尋ねるとコミュニティの誰かがきっと助けてくれます！

また、ブロックチェーンアプリケーションを本格的なプロジェクトとして構築していくのであれば[Grant Program](https://lisk.com/grant-program)に応募してみてはどうでしょうか？

読んでいただきありがとうございました！
お疲れ様でした！

https://lisk.com/documentation/lisk-sdk/index.html

https://lisk.chat

https://dev.lisk.com

https://lisk.com/grant-program

### 補足：ブロックチェーンアプリケーションの削除

```
rm -rf ~/.lisk/[アプリケーション名]
```

データのみ消す場合は

```
rm -rf ~/.lisk/[アプリケーション名]/data
```