【NEM】アドレス・HASH・ブロック高を自動判別する検索ボックスを作る

[nemアドベントカレンダー](https://qiita.com/m1z0/items/c401a63ebc879a38cae4)の15日目の記事です。

## 概要
今回は、ブロックチェーンのエクスプローラー系サイトによくある検索ボックスのNEM版を作ってみます。
取得できるデータは下記の通り

- ウォレットアドレスからウォレット情報を取得する
- トランザクションハッシュからトランザクションの内容を取得する
- ブロック高からブロックの情報を取得する

あくまで検索結果を取得する機能を作成するのが目的なので、取ってきたデータの加工やデザインについては触れません。

## 完成形
まず、完成形から。これから先の手順で詰まったら参考にしてください。

```html:sample.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>NEM統合サーチボックス</title>
    <style>
        #search-word{
            width:32em;
        }
        textarea {
            width :700px;
            height:500px;
        }
    </style>
</head>
<body>
    <h1>NEM integrated search box.</h1>
    <div class="container">
        <h2>ノード</h2>
        <input type="text" id="node" value="http://163.44.170.40">

        <h2>検索</h2>
        <div id="search-box">
            <input type="text" id="search-word">
            <button id="search-button">検索</button>
        </div>
        <h2>結果</h2>
        <div id="result-wrapper">
            <textarea id="result" readonly></textarea>
        </div>
    </div>
    
    <script src="./nem-sdk.js"></script><!-- こっちが先！-->
    <script src="./searchbox.js"></script>
</body>
</html>
```

```javascript:searchbox.js
(async function() {
    const nem = require("nem-sdk").default;
    // 接続するノード
    let node = "";
    
    // イベント設定
    document.querySelector("#search-button").addEventListener("click", onClickSearchButton);
    // ボタンクリック時
    async function onClickSearchButton(){
        // ノード取得
        node = document.querySelector("#node").value;;

        // 入力された検索ワード取得
        let searchString = document.querySelector("#search-word").value;
        // 前後に入った空白を除去する
        searchString = searchString.trim();

        // 検索を実行し、結果を取得する
        const result = await search(searchString);

        // 結果を表示する
        if(result !== null){
            // オブジェクトを見やすいJSON文字列に整形する
            const formattedResult = JSON.stringify(result, null, 4);
            document.querySelector("#result").value = formattedResult;
        }else{
            // 結果がない場合は空にする
            document.querySelector("#result").value = null;
        }
    }

    /**
     * 検索を実行して結果を返すメソッド
     * @param {string} searchString 検索するワード
     * @returns 各情報の取得結果
     */
    async function search(searchString){
        // 接続するノード情報を作成(httpsの場合は7891を設定)
        const endpoint = nem.model.objects.create("endpoint")(node, 7890);

        // デフォルトの戻り値
        let result = null;
        
        // 入力された文字の種類によって処理を振り分ける
        if(isAddress(searchString)){
            // ウォレットアドレスだったら、アカウント情報を取得する
            // アドレスからハイフンを除去する
            searchString = searchString.replace(/-/g, "");
            const accountInfo = await nem.com.requests.account.data(endpoint, searchString);
            result = accountInfo;
        } else if (isBlockHeight(searchString)) {
            // ブロック高の場合はそのブロックの情報を取得する
            const blockInfo = await nem.com.requests.chain.blockByHeight(endpoint, parseInt(searchString));
            result = blockInfo;
        } else if (isTransactionHash(searchString)){
            // トランザクションハッシュだったら、トランザクションの情報を取得する
            const txInfo = await nem.com.requests.transaction.byHash(endpoint, searchString);
            result = txInfo;
        }

        // 結果を返す
        return result;
    }


    // ウォレットアドレスかどうかを判定する
    function isAddress(address){
        // アドレスからハイフンを除去する
        address = address.replace(/-/g,"");

        // アドレスチェック
        let result = nem.model.address.isValid(address);
        // 正しいアドレスならtrue, 違ったらfalseを返す
        return result;
    }

    // ブロック高かを判定
    function isBlockHeight(blockHeight) {
        // 全て半角数値かどうかで判定（あとマイナスがつかないこと。）
        return blockHeight.match(/^-{0,1}\d+$/);
    }
    
    // トランザクションハッシュかを判定
    function isTransactionHash(txHash){
        // 文字数が64文字かつ、全て半角の英数字かどうかで判定
        // 正しければtrue, 違ったらfalseを返す
        return txHash.length === 64 && txHash.match('^[A-z0-9]+$');
    }

})();
```


## 1. 準備作業
### htmlファイルの作成
サンプルと同じHTMLファイルを作為します。

### nem-sdkの準備
やり方は色々ありますが、今回はnem-sdkのjsファイルをダウンロードして、```<script>```タグで読み込むやり方で実践します。

1. [こちら](https://raw.githubusercontent.com/QuantumMechanics/NEM-sdk/master/dist/nem-sdk.js)からSDK本体を保存する
2. 上記のhtmlと同じフォルダに配置する

### jsファイルの作成
まずは以下の通り記述して、searchbox.jsというファイル名でhtmlと同じフォルダに保存します。
※今回作成するコードはasync/await構文を使用しているため、IEなど古いブラウザでは動作しません。Chrome推奨です。

```js
(async function() {
    // sdk準備
    const nem = require("nem-sdk").default;
    // 接続するノード
    let node = "";
)();
```

## 2. イベント処理を書く
全手順で作成したJSファイル内に、検索ボタンをクリックした時の処理を記述します。
※searchメソッドはまだ作成していないので、このままではまだ動きません。

```js
(async function() {
    // sdk準備
    const nem = require("nem-sdk").default;
    // 接続するノード
    let node = "";
    
    // イベント設定
    document.querySelector("#search-button").addEventListener("click", onClickSearchButton);
    // ボタンクリック時
    async function onClickSearchButton(){
        // ノード取得
        node = document.querySelector("#node").value;;

        // 入力された検索ワード取得
        let searchString = document.querySelector("#search-word").value;
        // 前後に入った空白を除去する
        searchString = searchString.trim();

        // 検索を実行し、結果を取得する
        const result = await search(searchString);

        // 結果を表示する
        if(result !== null){
            // オブジェクトを見やすいJSON文字列に整形する
            const formattedResult = JSON.stringify(result, null, 4);
            document.querySelector("#result").value = formattedResult;
        }else{
            // 結果がない場合は空にする
            document.querySelector("#result").value = null;
        }
    }
})();
```

## 3. 検索処理を書く
前手順でボタンを押した時にsearchメソッドを呼ぶように記述していますので、そのsearchメソッドを実装しましょう。
先程のイベント処理に続けて以下を記述します。

※isから始まる各メソッドと、if文の中身はこの後実装します。

```js
    /**
     * 検索を実行して結果を返すメソッド
     * @param {string} searchString 検索するワード
     * @returns 各情報の取得結果
     */
    async function search(searchString){
        // 接続するノード情報を作成(httpsの場合は7891を設定)
        const endpoint = nem.model.objects.create("endpoint")(node, 7890);

        // デフォルトの戻り値
        let result = null;
        
        // 入力された文字の種類によって処理を振り分ける
        if(isAddress(searchString)){
        } else if (isBlockHeight(searchString)) {
        } else if (isTransactionHash(searchString)){
        }

        // 結果を返す
        return result;
    }
```

## 4. 各判定メソッドとデータ取得処理を実装する

### ①ウォレットアドレスの判定処理

入力された文字がウォレットアドレスかどうかを判定する、```isAddress```メソッドを実装します。
上記のsearchメソッドの後に以下を記述しましょう。

```js
    // ウォレットアドレスかどうかを判定する
    function isAddress(address){
        // アドレスからハイフンを除去する
        searchString= searchString.replace(/-/g,"");

        // アドレスチェック
        let result = nem.model.address.isValid(address);
        // 正しいアドレスならtrue, 違ったらfalseを返す
        return result;
    }
```

#### 解説
ウォレットアドレスかどうかを判定するには、sdkにある```nem.model.address.isValid```メソッドを使用することで可能です。
ただし、アドレスがハイフン区切りだと正しく判定できないので、```replace```メソッドでハイフンを空文字に置き換えてから呼び出すようにします。

### ②ウォレット情報の取得処理
続いて、APIからウォレットの情報を取得する処理を実装します。

searchメソッドのif文の中に以下の通り記述します。

```js
        if(isAddress(searchString)){
            // ウォレットアドレスだったら、アカウント情報を取得する
            // アドレスからハイフンを除去する
            address = address.replace("-", "");
            const accountInfo = await nem.com.requests.account.data(endpoint, searchString);
            result = accountInfo;
        } // 以下略...
```

#### 解説
ウォレット情報の取得は、```nem.com.requests.account.data```メソッドを使用することで可能です。
第1引数にはエンドポイント（接続先ノードの情報）を設定します。
第2引数にはウォレットアドレスを設定します。
このメソッドは非同期で動作するので、awaitを頭につけて同期的に呼び出すようにします。

取得したらresult変数に情報を入れてあげます。

### ③ブロック高の判定処理

入力された文字がブロック高かどうかを判定する、```isBlockHeight```メソッドを実装します。
上記のisAddressメソッドの後に以下を記述しましょう。

```js
    // ブロック高かを判定
    function isBlockHeight(blockHeight) {
        // 全て半角数値かどうかで判定（あとマイナスがつかないこと。）
        return blockHeight.match(/^-{0,1}\d+$/);
    }
```

#### 解説
ブロック高かどうかの判定には、以下の条件を使用します。

- 入力された文字が全て半角数字かどうか
- マイナスでないこと

今回は正規表現によってこの判定を行います。

### ④ブロック高の取得処理
続いて、APIからブロック高の情報を取得する処理を実装します。

searchメソッドのisBlockHeightメソッドを呼び出している箇所に以下の通り記述します。

```js
        // （省略）
        } else if(isBlockHeight(searchString)){
            // ブロック高の場合はそのブロックの情報を取得する
            const blockInfo = await nem.com.requests.chain.blockByHeight(endpoint, parseInt(searchString));
            result = blockInfo;
        } // 以下略...
```

#### 解説
ブロック高の取得は、```nem.com.requests.chain.blockByHeight```メソッドを使用することで可能です。
使用方法はウォレットアドレスの取得処理と同じです。
ただし、第2引数のブロック高は数値型である必要があるため、```parseInt```メソッドで数値型に変換してから設定します。


### ⑤トランザクションハッシュの判定処理

入力された文字がハッシュ文字列かどうかを判定する、```isTransactionHash```メソッドを実装します。
上記のisBlockHeightメソッドの後に以下を記述しましょう。

```js
    // トランザクションハッシュかを判定
    function isTransactionHash(txHash){
        // 文字数が64文字かつ、全て半角の英数字かどうかで判定
        // 正しければtrue, 違ったらfalseを返す
        return txHash.length === 64 && txHash.match('^[A-z0-9]+$');
    }
```

#### 解説
トランザクションハッシュかどうかの判定には、以下の条件を使用します。

- 入力された文字数が64文字かどうか
- 入力された文字が全て半角英数字かどうか

文字数の判定には```.length```プロパティ、文字種の判定には正規表現を使ってこの判定を行います。

### ⑥トランザクション情報の取得処理
続いて、APIからトランザクション情報を取得する処理を実装します。

searchメソッドのisTransactionHashメソッドを呼び出している箇所に以下の通り記述します。

```js
        // （省略）
        } else if(isTransactionHash(searchString)){
            // トランザクションハッシュだったら、トランザクションの情報を取得する
            const txInfo = await nem.com.requests.transaction.byHash(endpoint, searchString);
            result = txInfo;
        } 
```

#### 解説
ハッシュ値によるトランザクション情報の取得は、```nem.com.requests.transaction.byHash```メソッドを使用することで可能です。
使用方法はウォレットアドレスの取得処理と同じです。


## 5.試してみる
### ウォレット情報の取得
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525917/e60de41a-4314-f5c0-c9f2-06428fc9bd16.png)

検索欄にアドレスを入力してボタンを押すと、今のウォレット情報が取得されます。
ハイフンを入れても入れなくても検索できるので、ユーザーの視点からも使いやすくなっています。

### ブロック高の取得
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525917/7f51f3c5-4d92-a609-6757-6a5cfdda0c6e.png)

検索欄に数値のみを入力してボタンを押すと、ブロックの情報が取得されます。
試しに200万ブロック目の情報を取ってきた結果が↑になります。

### トランザクションハッシュ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525917/2fd6c1e0-d735-2f6c-b7dc-e1e4e8add154.png)

最後に、ハッシュ値を入れてみましょう。
正しいハッシュ値を入力した場合、そのトランザクション情報が取得されます。


## 6.まとめ
今回はNIS1用のSDKを使って検索ボックスを作ってみました。
SDKを利用することにより、ほぼ同じような書き方でいろんな情報を簡単に取得できることが分かって頂けたかと思います。

このようにAPIから取得したデータと、WEBサイトのデザインを組み合わせることで、オリジナルのエクスプローラーサイトを制作することができます。

Catapult対応版については追ってどこかに掲載します。