DenoでSymbol Cliツールを作った

この記事は [nem /symbol Advent Calendar 2022](https://qiita.com/advent-calendar/2022/nem) 18日目の記事です。

[前回の記事](https://qiita.com/naoki_maeda/items/aa2e022f9975aaf12af6)が2019年で3年前でしたので、だいぶ久しぶりになります。

## Deno Symbol Cli

Symbol Block Chainを扱うcliをDenoで書きました。
https://github.com/naoki-maeda/deno-symbol-cli

公式が開発したcliがあったのですが、なぜかarchiveされていたのと出力がちょっとクセがある感じだったので、シンプルにjsonで出力したかったというのもあり、作ってみました。
https://github.com/symbol/symbol-cli

あとはDenoがnpm対応をして、Denoを使ってみたかったという気持ちがありました。
https://deno.com/blog/changes

Denoを使ってみての感想としては、Denoええなあというところでそのあたりはまた後ほど書きます。

deno-symbol-cliはsymbol-cliほど多機能ではなく、とりあえずは単にAPIを叩いてトランザクションやアカウントの確認とか、payloadの中身確認などの表示ぐらいです。開発補助ツールとして使用しています。
エラーハンドリングとかもあまりしていませんので、ご使用の際はご注意ください。

## Install

`deno` がまず必要です。
このあたりを参考にインストールしてください。
https://deno.land/manual@v1.29.0/getting_started/installation

インストールできれば `deno install` で `deno-symbol-cli` をインストールします。
`--name` はcliのコマンド名を自由に設定してください。
良い名前が思いつかなくて、私はdeno-symbolを略して `desy` で実行しています。

`--unstable` はnpmのパッケージを使うために必要です。
symbol-sdkやsymbol-openapi-typescript-fetch-clientなどを使用しています。

`--allow-*` は実行するパーミッションを設定します。
パーミッションを設定できるのはDenoの良いところだと思います。
後述しますが、接続先を制限することなどもできます。

```sh
deno install --force --name desy --allow-env --allow-read --allow-net --unstable https://deno.land/x/deno_symbol_cli@0.1.2/main.ts
```

インストールできれば、PATHを設定します。
すでに設定している場合は不要です。

コマンド例

```sh
echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
```

参考
https://deno.land/manual@v1.29.0/tools/script_installer#script-installer


## Usage

helpを表示しています。

```

  Usage:   Deno Symbol Cli
  Version: 0.1.2          

  Description:

    Symbol Cli tool with Deno

  Options:

    -h, --help                  - Show this help.                                         
    -V, --version               - Show the version number for this program.               
    -e, --endpoint  [endpoint]  - Symbol API Endpoint                        (Default: "")

  Commands:

    help      [command]  - Show this help or the help of a sub-command.
    tx        <hash>     - Get transaction info from hash              
    payload   <payload>  - Get decoded transaction from payload        
    account   <account>  - Get account info from address or public key 
    block     <height>   - Get block info from block number            
    mosaic    <id>       - Get mosaic info from mosaic id              
    chain                - Get current chain info                      
    validate  <address>  - Check if valid address
```

一部サンプルを紹介します。

### tx

トランザクションの詳細を表示します。
-eにエンドポイント、引数にトランザクションハッシュを設定します。
Explorerに設定されていたノードの1つを使用させていただいてます、ありがとうございます。

```sh
desy tx -e https://001-sai-dual.symboltest.net:3001 80C1EA76D7CB2E6176965D9163B26EBD5B29441E9C1667ACC9BEA0970DECF943

{
  "id": "639AC24D8EFA5A77741C0504",
  "meta": {
    "height": "86590",
    "hash": "80C1EA76D7CB2E6176965D9163B26EBD5B29441E9C1667ACC9BEA0970DECF943",
    "merkleComponentHash": "80C1EA76D7CB2E6176965D9163B26EBD5B29441E9C1667ACC9BEA0970DECF943",
    "index": 0,
    "timestamp": "3836154260",
    "feeMultiplier": 1136363
  },
  "transaction": {
    "size": 176,
    "signature": "8F48269B848CA61E313C53C9DED53FE587FB196B7773578C38490580E8BA16A84AE343595AC0192AA855687A986B586147E8EF092696EA23926F8FD1717BB105",
    "signerPublicKey": "81EA7C15E7EC06261C9F654F54EAC4748CFCF00E09A8FE47779ACD14A7602004",
    "version": 1,
    "network": 152,
    "type": 16724,
    "maxFee": "200000000",
    "deadline": "3843329258",
    "recipientAddress": "98AD63F8D6D8F6D81DD84889E021D4DAFFE11625B3BFC6F7",
    "mosaics": [
      {
        "id": "72C0212E67A08BCE",
        "amount": "300000000"
      }
    ]
  }
}
```

### payload

payloadからトランザクションの詳細を取得します。

```sh
desy payload B800000000000000B32D24AC856CC7DF377D17665956FAB3DB3566E696A19217F5CAF58FA8FFE794BDEFCC91A7275BD60D1FAB2F7F2E564918105003B4D08
269CCEB8FC997E50E01064067C0A7D35B79128562F9B0B88D9309A234EA362DC8E91D181A4AB36A510C0000000001985441E047000000000000990531AA0C000000986E2B2B7DEBD6D3D311D58BD5272E71EAFF69FD1A600B4808000
100000000003CE19A057E831F090100000000000000006D657373616765
{
  "transaction": {
    "type": 16724,
    "network": 152,
    "version": 1,
    "maxFee": "18400",
    "deadline": "54394946969",
    "signature": "B32D24AC856CC7DF377D17665956FAB3DB3566E696A19217F5CAF58FA8FFE794BDEFCC91A7275BD60D1FAB2F7F2E564918105003B4D08269CCEB8FC997E50E01",
    "signerPublicKey": "064067C0A7D35B79128562F9B0B88D9309A234EA362DC8E91D181A4AB36A510C",
    "recipientAddress": {
      "address": "TBXCWK355PLNHUYR2WF5KJZOOHVP62P5DJQAWSA",
      "networkType": 152
    },
    "mosaics": [
      {
        "amount": "1",
        "id": "091F837E059AE13C"
      }
    ],
    "message": "006D657373616765"
  }
}
```

### account

```sh
$ desy account -e https://001-sai-dual.symboltest.net:3001 TARDV42KTAIZEF64EQT4NXT7K55DHWBEFIXVJQY
{
  "id": "63616A02E1C4141A71FF485C",
  "account": {
    "version": 1,
    "address": "98223AF34A98119217DC2427C6DE7F577A33D8242A2F54C3",
    "addressHeight": "1",
    "publicKey": "81EA7C15E7EC06261C9F654F54EAC4748CFCF00E09A8FE47779ACD14A7602004",
    "publicKeyHeight": "4706",
    "accountType": 0,
    "supplementalPublicKeys": {},
    "activityBuckets": [
      {
        "startHeight": "86580",
        "totalFeesPaid": "199999888",
        "beneficiaryCount": 0,
        "rawScore": "351774314155995"
      },
      {
        "startHeight": "86400",
        "totalFeesPaid": "199999888",
        "beneficiaryCount": 0,
        "rawScore": "351776311901277"
      },
      {
        "startHeight": "86220",
        "totalFeesPaid": "0",
        "beneficiaryCount": 0,
        "rawScore": "351777859753096"
      },
      {
        "startHeight": "86040",
        "totalFeesPaid": "199999888",
        "beneficiaryCount": 0,
        "rawScore": "351780520028457"
      },
      {
        "startHeight": "85860",
        "totalFeesPaid": "199999888",
        "beneficiaryCount": 0,
        "rawScore": "351783136454326"
      }
    ],
    "mosaics": [
      {
        "id": "72C0212E67A08BCE",
        "amount": "370971832675744"
      }
    ],
    "importance": "351774314155995",
    "importanceHeight": "86580"
  }
}
```

-cオプションを使うとMosaicをdivisibilityを考慮した数字で表示します。

```sh
desy account -e https://001-sai-dual.symboltest.net:3001 TARDV42KTAIZEF64EQT4NXT7K55DHWBEFIXVJQY -c
{
  "id": "63616A02E1C4141A71FF485C",
  "account": {
    "version": 1,
    "address": "98223AF34A98119217DC2427C6DE7F577A33D8242A2F54C3",
    "addressHeight": "1",
    "publicKey": "81EA7C15E7EC06261C9F654F54EAC4748CFCF00E09A8FE47779ACD14A7602004",
    "publicKeyHeight": "4706",
    "accountType": 0,
    "supplementalPublicKeys": {},
    "activityBuckets": [
      {
        "startHeight": "86580",
        "totalFeesPaid": "199999888",
        "beneficiaryCount": 0,
        "rawScore": "351774314155995"
      },
      {
        "startHeight": "86400",
        "totalFeesPaid": "199999888",
        "beneficiaryCount": 0,
        "rawScore": "351776311901277"
      },
      {
        "startHeight": "86220",
        "totalFeesPaid": "0",
        "beneficiaryCount": 0,
        "rawScore": "351777859753096"
      },
      {
        "startHeight": "86040",
        "totalFeesPaid": "199999888",
        "beneficiaryCount": 0,
        "rawScore": "351780520028457"
      },
      {
        "startHeight": "85860",
        "totalFeesPaid": "199999888",
        "beneficiaryCount": 0,
        "rawScore": "351783136454326"
      }
    ],
    "mosaics": [
      {
        "id": "72C0212E67A08BCE",
        "amount": "370971832.675744" // ここがconvertされている
      }
    ],
    "importance": "351774314155995",
    "importanceHeight": "86580"
  }
}
```

こういう感じでシンプルにjsonで表示するだけのツールです。

## Denoの良いところ

Denoを触ってみて、とても開発体験が良かったので私が思った良いとこを紹介したいと思います。

- パーミッションをコマンドごとに設定できる
- typescriptをすぐ実行できる
- コマンドラインツールのcliffyが使いやすい
- deno.landのpublishが楽

### パーミッションを設定できる

ファイルシステムやネットワークアクセスなどは明示的に許可しないと実行できません。
参考 https://deno.land/manual@v1.28.3/basics/permissions

例えば今回でいうと、特定のノードのエンドポイントにしかアクセスしたくない場合は（そういうケースがあるのかは置いておいて）以下のようにできます。

```sh
deno install --force --name desy --allow-env --allow-read --allow-net=001-sai-dual.symboltest.net --unstable https://deno.land/x/deno_symbol_cli@0.1.2/main.ts
```

### typescriptをすぐ実行できる

ts-nodeなどの選択肢はあるかと思いますが、typescriptを実行するのは準備が面倒だと思ってます。
サクッと `deno run` で実行できるのはとても良いです。
開発するにはeslintとかprettierとかも必要になるかと思いますが、denoにはデフォルトで `deno fmt` と `deno lint` があるのも嬉しいところです。

### コマンドラインツールのcliffyが使いやすい

私が知らないだけかもですが、TypescriptやNode.jsでコマンドライン作成ツールで使いやすいと感じるものはあまりありませんでした。
今回使用したcliffyはとても使いやすく、これから簡単なcliツールはこれで作ろうと思いました。

cliffy
https://cliffy.io/
https://deno.land/x/cliffy@v0.25.5

今回のdeno-symbol-cliの例を載せておきます。
endpointだけglobalなoptionとして設定しています。

```ts
new Command()
  .name("Deno Symbol Cli")
  .version("0.1.2")
  .description("Symbol Cli tool with Deno")
  .default("help")
  .globalOption("-e, --endpoint [endpoint:string]", "Symbol API Endpoint", {
    default: "",
  })
  .globalType("endpoint", new StringType())
  // help
  .command("help", new HelpCommand())
  // tx
  .command("tx <hash:string>", "Get transaction info from hash")
  .option("-s, --status [status:boolean]", "Get transaction status info only", {
    default: false,
  })
  .action(async (option, hash) => await tx(String(option.endpoint), hash, option.status))
  // payload
  .command("payload <payload:string>", "Get decoded transaction from payload")
  .action((_, payloadStr) => payload(payloadStr))
  // account
  .command(
    "account <account:string>",
    "Get account info from address or public key",
  )
  .option(
    "-c, --convert [convert:boolean]",
    "Get converted Mosaics account info",
    {
      default: false,
    },
  )
  .action(async (option, accountStr) => await account(String(option.endpoint), accountStr, option.convert))
  // block
  .command("block <height:string>", "Get block info from block number")
  .action(async (option, height) => await block(String(option.endpoint), height))
  // mosaic
  .command("mosaic <id:string>", "Get mosaic info from mosaic id")
  .action(async (option, id) => await mosaic(String(option.endpoint), id))
  // chain
  .command("chain", "Get current chain info")
  .action(async (option) => await chain(String(option.endpoint)))
  // validate
  .command("validate <address:string>", "Check if valid address")
  .action((_, address) => validate(address))
  .parse(Deno.args);
```

このようにメソッドチェインで繋げていきます。
actionを設定することで、設定したコマンドで実行してくれます。
helpも自動で作成してくれ、Usageに載せているのがhelpを実行したものです。

cliffyシンプルでとても良いと思います。

## deno.landのpublishが楽

こちらの通りにGitHubにwebhookを設定するだけです。
https://deno.land/add_module

あとはtag切ってpushすれば自動で `deno.land/x` に反映されます。
deno_symbol_cliです。
https://deno.land/x/deno_symbol_cli@0.1.2

## Deno所感

Denoについては、やはりnpmに対応してくれたのは既存のpackageを使えますし、非常にありがたいです。
npmのcompileにも対応してくれて、バイナリ化できればさらにありがたいです。
issueはあってそのうち対応されると思うので、気長に待ちたいと思います。
https://github.com/denoland/deno/issues/16632

今回触ってみてDenoはとても開発体験が良いと思いました。
簡単なscript的なものはPythonで書くことが多かったのですが、これからはDenoで書くようになる気がします。

## まとめ

deno-symbol-cliとDenoで開発してみて良いと感じたところを紹介しました。
多分これから機能もちょこちょこ追加していくと思います。
バグや要望などあれば気軽にコメントやissueなどあげていただければと思います。