SYMBOL の「 Peer Node List 」を作って aLice と連携してみた

# はじめに

2023年9月 SymbolのモバイルノードがAndroidのアプリ上で建てられるようになりました📱✨
開発者は [tqm](https://twitter.com/tqm1990)さんです。

https://coinpost.jp/?post_type=pressrelease&p=483392

ついにノードが持ち運べる時代が来たか✨
ということで、さっそくモバイルノードに挑戦する方が出てきました👀

ただ、モバイルノードは、ピアノードという種類のノードで、
当時はピアノードの情報がまとまっているものが無かったので、
じゃ、見やすいリストを作ってみようかなと思ったのがはじまりでした。

リストが完成してからは、このリストから直接モバイルで、ハーベスト設定が出来たら便利よね🤔

ってことで aLiceとの連携に挑戦してみる事にしました。

開発のモチベーションを上げるために、QUESTも行いました。
応援していただいた皆様、本当にありがとうございました🙏

https://quest-bc.com/quest/tUXxP7EzN7leg3CJDKSB?jid=GZhIvHaUSvYhE156V4xf

**< Peer Node List >**
➡️　https://ventus-wallet.net/peernode/

![スクリーンショット 2023-12-06 21.01.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408917/7f968ec5-e144-c864-d5fb-f92ce89ff53e.png)

# 環境
使用言語 : HTML, javascript, CSS
助っ人 : ChatGPT 3.5　


# 時短の為にChatGPTを使用

このリストを作成する上で、ChatGPT君にはたくさん質問をしました(笑)

https://chat.openai.com

動的に表を作成したい。とか。
1番左の列は通し番号にしたい。とか。
Version毎に色分けしたい。と要望を出すと
サンプルコードを教えてくれます。

で、それをいじりながら組み立てて行くと、動的な表が完成します。
便利な世の中になりましたね☺️👍



# ポイント解説

ピアノードの情報は Statistics Service から取得しています。

### Statistics Service とは？

これは、Symbol Explorer でも利用されている統計サービスです。
Symbolのノードを定期的に巡回し、情報を収集しています。

＜使用したサービス＞
* MainNet は、https://mainnet.dusanjp.com:3004/nodes (づ〜さん提供)
* TestNet は、https://testnet.dusanjp.com:3004/nodes (づ〜さん提供)


＊コアチーム提供のもの 
https://symbol.services/nodes
https://testnet.symbol.services/nodes
ではなく、づ〜さん作成のサービスを利用させてもらっています。

＊ Statistics Service は自分で構築することも出来ます。
参考記事：

https://qiita.com/ishidad2/items/c5ddd290b09f9ccd2dcd

***

***ピアノードの情報を取得する***

fetch('https://mainnet.dusanjp.com:3004/nodes') でノード情報を取得して、その中からピアノード(roles:1)だけを抽出しています。
※ 限定をしなければ、全ての種類のノード情報が取得出来ます。

```javascript:
fetch(URL)
      .then((response) => {
        // レスポンスが成功したかどうかを確認
        if (!response.ok) {
          throw new Error(`Network response was not ok: ${response.status}`);
        }
        // JSONデータを解析して返す
        return response.json();
      })
      .then((data) => {
       // rolesプロパティが1の要素のみを抽出
       const filteredData = data.filter((item) => item.roles === 1);


      })
```

***

***「 Chain Height 」 や 「 Finalize Height 」 を表示する***

正常稼働しているノードは

< MainNet >

https://symbol.services/nodes?filter=suggested&limit=3&ssl=true

< TestNet >

https://testnet.symbol.services/nodes?filter=suggested&limit=3&ssl=true

これらのエンドポイントを利用して取得しています。

limit=3 と指定すると、ランダムに3つのノードを取得します。

ページをリロードする度にノードは更新されます。
***

***特定のノードに委任する***

**Peer Node List** の1番左の列の数字をクリックすると　
Num_Click( ) 関数が実行されます。

```javascript:Num_Click( )

function Num_Click(friendlyName, host, hostDetail, version, node_publicKey, NET_Type) {
      const arrayBuffer = new TextEncoder().encode(`https://ventus-wallet.net/peernode/delegate.html?friendlyName=${encodeURIComponent(friendlyName)}&host=${encodeURIComponent(host)}&hostDetail=${encodeURIComponent(hostDetail)}&version=${encodeURIComponent(version)}&node_publicKey=${encodeURIComponent(node_publicKey)}&NET_Type=${encodeURIComponent(NET_Type)}`);
      const callback = Array.from(new Uint8Array(arrayBuffer), byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase();
      const encodedUrl = `alice://sign?type=request_pubkey&callback=${callback}`;
      window.location.href = encodedUrl;
    }

```

数字をクリックすると **aLiceアプリ** が起動し、公開鍵を渡して良いか確認のメッセージが出ます。 そして、**callback** でノードの情報を **delegate.html** に渡しています。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408917/c2f54f64-7c86-c7eb-559f-00a98f03b9b3.jpeg">



**このノードに委任する** ボタンを押すと、ハーベスト設定の為のトランザクションが送信されます。

キーリンクが設定されている場合は、全て解除し、再度リンクをしていますので、デスクトップウォレット等でハーベスト停止の作業は必要ありません。**ワントランザクション** でハーベスト設定が可能となっています。

トランザクションが承認されるとハーベスト設定完了となります。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408917/c3601f2f-2655-ef6e-2404-782b52c985da.jpeg">

https://x.com/mikunNEM/status/1727741182418256318?s=20

***
**aLice アプリダウンロード**

< Android >

https://play.google.com/store/apps/details?id=com.pine.alice&pli=1

< iOS >

https://apps.apple.com/us/app/alice-sign/id6449146041

**aLiceの使い方**

https://note.com/nononon_symbol/n/n87e8ef3f89e0

**aLiceの開発者向けドキュメントはこちら** 

https://github.com/0x070696E65/aLice

ワントランザクションのハーベスト設定のコードは [だい](https://twitter.com/ishidad2)さん の記事を参考にしています。

https://qiita.com/ishidad2/items/e3abdb7a29454b86f99b

***

以下 index.html と delegate.html は、[「Peer Node List」](https://ventus-wallet.net/peernode/) の全コードです。

<details><summary>index.html</summary><div>

```html:index.html
<!DOCTYPE html>
<html>

<head>
  <title>Peer Node List</title>
  <style>
    body {
      margin-left: 40px;
    }

    /* テーブル全体の枠線スタイル */
    table {
      border-collapse: collapse;
      width: 1800px;
      table-layout: fixed;
      /* 列の幅を固定する */
    }

    /* ヘッダ行のスタイル */
    th {
      background-color: #fde8bf;
      text-align: left;
      white-space: nowrap;
      text-overflow: ellipsis;
    }

    /* セルのスタイル */
    th,
    td {
      border: 1px solid #000;
      padding: 8px;
      white-space: normal;
      text-overflow: ellipsis;
    }

    /* 通し番号セルを右寄せに設定 */
    td.row-number {
      text-align: right;
    }

    /* 列の幅を固定 */
    td.col-1,
    th.col-1 {
      width: 2%;
      max-width: 2%;
      text-align: center;
    }

    td.col-2,
    th.col-2 {
      width: 18%;
      max-width: 18%;
      text-align: center;
    }

    td.col-3,
    th.col-3 {
      width: 15%;
      max-width: 15%;
      text-align: center;
    }

    td.col-4,
    th.col-4 {
      width: 10%;
      max-width: 10%;
      text-align: center;
    }

    td.col-5,
    th.col-5 {
      width: 5%;
      max-width: 5%;
      text-align: center;
    }

    td.col-6,
    th.col-6 {
      width: 50%;
      max-width: 50%;
      text-align: center;
    }

    .aLice_button {
      font-size: 20px;
      width: 200px;
      height: 40px;
      background-image: linear-gradient(45deg, #ffffff 0%, #69fe05 100%);
      color: rgb(255, 255, 255);
      border-radius: 16px;
      cursor: pointer;
      box-shadow: 2px 2px 2px rgb(102, 113, 102);
    }

    .aLice_button:hover {
      background-image: linear-gradient(45deg, #ffffff7f 0%, #ffffff7f 100%);
    }

    #date {
      font-family: 'Orbitron', sans-serif;
      font-size: 20px;
      color: #03a83a;
      text-shadow: 0 0 20px #0aafe6;
      line-height: 1.2;
    }

    #clock {
      font-family: 'Orbitron', sans-serif;
      font-size: 50px;
      color: #855DF7;
      text-shadow: 0 0 20px #0aafe6;
      line-height: 1.2;
      width: 200px;
      /* 時計の幅固定 */
      margin: 0px 30px 0px 0px;
      text-align: center;
    }

    .chain_height {
      font-family: 'Orbitron', sans-serif;
      font-size: 30px;
      color: #03a83a;
      text-shadow: 0 0 20px #0aafe6;
      line-height: 1.2;
    }

    .finalized_height {
      font-family: 'Orbitron', sans-serif;
      font-size: 30px;
      color: #6D77F6;
      text-shadow: 0 0 20px #0aafe6;
      line-height: 1.2;
    }
  </style>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <script src="https://code.jquery.com/jquery-3.7.1.slim.min.js"></script>
  <script type="text/javascript" src="https://xembook.github.io/nem2-browserify/symbol-sdk-pack-2.0.4.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/2.1.2/sweetalert.min.js"></script>
</head>

<body>
  <h1>SYMBOL</h1>
  <br>
  <div id="date"></div>
  <div id="clock"></div>
  <br>
  <button class="aLice_button"><a href="https://play.google.com/store/apps/details?id=com.pine.alice&pli=1"> aLice for
      Android</a></button>
  <button class="aLice_button"><a href="https://apps.apple.com/us/app/alice-sign/id6449146041">aLice for
      iOS</a></button>
  <button class="aLice_button"><a href="https://note.com/nononon_symbol/n/n87e8ef3f89e0"
      target="_blank">使い方</a></button>
  <br>
  <br>
  <br>
  <h2 style="text-align:left">Peer Node List (Main Net)</h2>
  <div
    style="font-family: 'Orbitron', sans-serif; font-size: 15px;color: #03a83a;text-shadow: 0 0 20px #0aafe6;line-height: 1.2;">
    Chain Height</div>
  <br>
  <div id="chain_height_M" class="chain_height"></div>
  <br>
  <div
    style="font-family: 'Orbitron', sans-serif; font-size: 15px;color: #6D77F6;text-shadow: 0 0 20px #0aafe6;line-height: 1.2;">
    Finalized Height</div>
  <br>
  <div id="finalized_height_M" class="finalized_height"></div>
  <br>

  <table id="data-table">
    <thead>
      <tr>
        <th class="col-1">#</th>
        <th class="col-2">FriendlyName</th>
        <th class="col-3">Host</th>
        <th class="col-4">Host Detail</th>
        <th class="col-5">Version</th>
        <th class="col-6">PublicKey</th>
      </tr>
    </thead>
    <tbody>
      <!-- 表データはここに挿入されます -->
    </tbody>
  </table>
  <br>
  <br>
  <h2 style="text-align:left">Peer Node List (Test Net)</h2>

  <div
    style="font-family: 'Orbitron', sans-serif; font-size: 15px;color: #03a83a;text-shadow: 0 0 20px #0aafe6;line-height: 1.2;">
    Chain Height</div>
  <br>
  <div id="chain_height_T" class="chain_height"></div>
  <br>
  <div
    style="font-family: 'Orbitron', sans-serif; font-size: 15px;color: #6D77F6;text-shadow: 0 0 20px #0aafe6;line-height: 1.2;">
    Finalized Height</div>
  <br>
  <div id="finalized_height_T" class="finalized_height"></div>
  <br>

  <table id="data-table2">
    <thead>
      <tr>
        <th class="col-1">#</th>
        <th class="col-2">FriendlyName</th>
        <th class="col-3">Host</th>
        <th class="col-4">Host Detail</th>
        <th class="col-5">Version</th>
        <th class="col-6">PublicKey</th>
      </tr>
    </thead>
    <tbody>
      <!-- 表データはここに挿入されます -->
    </tbody>
  </table>

  <script>

    let URL = 'https://mainnet.dusanjp.com:3004/nodes'  // mainnet

    // fetch()を使用してデータを取得
    fetch(URL)
      .then((response) => {
        // レスポンスが成功したかどうかを確認
        if (!response.ok) {
          throw new Error(`Network response was not ok: ${response.status}`);
        }
        // JSONデータを解析して返す
        return response.json();
      })
      .then((data) => {
        // rolesプロパティが1の要素のみを抽出
        const filteredData = data.filter((item) => item.roles === 1);

        // テーブルのtbody要素を取得
        const tbody = document.querySelector('#data-table tbody');

        // 通し番号用の変数
        let rowNum = 0;
        let NET_Type;

        // 抽出したデータを表に挿入
        filteredData.forEach((item) => {
          if (item.hostDetail) {

            if (item.version === 16777989) {
              rowNum++; // 通し番号をインクリメント      
              const row = document.createElement('tr');
              row.innerHTML = `
              <td class="row-number"><a href="#" onclick="Num_Click('${item.friendlyName}', '${item.host}', '${item.hostDetail ? item.hostDetail.location : ''}', '${item.version}', '${item.publicKey}', 'MainNet');">${rowNum}</a></td>
              <td>${item.friendlyName}</td>
              <td>${item.host}</td>
              <td>${item.hostDetail.location}</td>
              <td>${item.version}</td>
              <td>${item.publicKey}</td>
            `;
              tbody.appendChild(row);
            }
            if (item.version === 16777990) {
              rowNum++; // 通し番号をインクリメント      
              const row = document.createElement('tr');
              row.innerHTML = `
              <td class="row-number"><a href="#" onclick="Num_Click('${item.friendlyName}', '${item.host}', '${item.hostDetail ? item.hostDetail.location : ''}', '${item.version}', '${item.publicKey}', 'MainNet');">${rowNum}</a></td>
              <td>${item.friendlyName}</td>
              <td>${item.host}</td>
              <td>${item.hostDetail.location}</td>
              <td>${item.version}</td>
              <td>${item.publicKey}</td>
            `;
              tbody.appendChild(row);
            }
          } else {
            if (item.version === 16777989) {
              rowNum++; // 通し番号をインクリメント
              const row = document.createElement('tr');
              row.innerHTML = `
              <td class="row-number"><a href="#" onclick="Num_Click('${item.friendlyName}', '${item.host}', 'Mobile 📱', '${item.version}', '${item.publicKey}', 'MainNet');">${rowNum}</a></td>
              <td>${item.friendlyName}</td>
              <td>${item.host}</td>
              <td>Mobile 📱</td>
              <td>${item.version}</td>
              <td>${item.publicKey}</td>
            `;
              tbody.appendChild(row);
            }
            if (item.version === 16777990) {
              rowNum++; // 通し番号をインクリメント
              const row = document.createElement('tr');
              row.innerHTML = `
              <td class="row-number"><a href="#" onclick="Num_Click('${item.friendlyName}', '${item.host}', '${item.hostDetail ? item.hostDetail.location : ''}', '${item.version}', '${item.publicKey}', 'MainNet');">${rowNum}</a></td>
              <td>${item.friendlyName}</td>
              <td>${item.host}</td>
              <td></td>
              <td>${item.version}</td>
              <td>${item.publicKey}</td>
            `;
              tbody.appendChild(row);
            }

          }

          // 特定の列の特定の数字の背景色を変更
          const cells = document.querySelectorAll('tbody td:nth-child(5)'); // Role列を選択

          cells.forEach((cell) => {
            if (cell.textContent === '16777989') { // 条件を確認
              cell.style.backgroundColor = 'aquamarine'; // 背景色を変更  
            }
            if (cell.textContent === '16777990') { // 条件を確認
              cell.style.backgroundColor = 'skyblue'; // 背景色を変更  
            }
          });
        });
      })
      .catch((error) => {
        // エラーが発生した場合はコンソールに表示
        console.error("Fetch error:", error);
      });

    ////////////////////////////////////////////////////////////////////////

    URL = 'https://testnet.dusanjp.com:3004/nodes'   // testnet

    // fetch()を使用してデータを取得
    fetch(URL)
      .then((response) => {
        // レスポンスが成功したかどうかを確認
        if (!response.ok) {
          throw new Error(`Network response was not ok: ${response.status}`);
        }
        // JSONデータを解析して返す
        return response.json();
      })
      .then((data) => {
        // rolesプロパティが1の要素のみを抽出
        const filteredData = data.filter((item) => item.roles === 1);

        // テーブルのtbody要素を取得
        const tbody = document.querySelector('#data-table2 tbody');

        // 通し番号用の変数
        let rowNum = 0;

        // 抽出したデータを表に挿入
        filteredData.forEach((item) => {
          if (item.hostDetail) {
            if (item.version === 16777989) {
              rowNum++; // 通し番号をインクリメント      
              const row = document.createElement('tr');
              row.innerHTML = `
              <td class="row-number"><a href="#" onclick="Num_Click('${item.friendlyName}', '${item.host}', '${item.hostDetail ? item.hostDetail.location : ''}', '${item.version}', '${item.publicKey}', 'TestNet');">${rowNum}</a></td>
              <td>${item.friendlyName}</td>
              <td>${item.host}</td>
              <td>${item.hostDetail.location}</td>
              <td>${item.version}</td>
              <td>${item.publicKey}</td>
            `;
              tbody.appendChild(row);
            }
            if (item.version === 16777990) {
              rowNum++; // 通し番号をインクリメント      
              const row = document.createElement('tr');
              row.innerHTML = `
              <td class="row-number"><a href="#" onclick="Num_Click('${item.friendlyName}', '${item.host}', '${item.hostDetail ? item.hostDetail.location : ''}', '${item.version}', '${item.publicKey}', 'TestNet');">${rowNum}</a></td>
              <td>${item.friendlyName}</td>
              <td>${item.host}</td>
              <td>${item.hostDetail.location}</td>
              <td>${item.version}</td>
              <td>${item.publicKey}</td>
            `;
              tbody.appendChild(row);
            }
          } else {
            if (item.version === 16777989) {
              rowNum++; // 通し番号をインクリメント
              const row = document.createElement('tr');
              row.innerHTML = `
              <td class="row-number"><a href="#" onclick="Num_Click('${item.friendlyName}', '${item.host}', 'Mobile 📱', '${item.version}', '${item.publicKey}', 'TestNet');">${rowNum}</a></td>
              <td>${item.friendlyName}</td>
              <td>${item.host}</td>
              <td>Mobile 📱</td>
              <td>${item.version}</td>
              <td>${item.publicKey}</td>
            `;
              tbody.appendChild(row);
            }
            if (item.version === 16777990) {
              rowNum++; // 通し番号をインクリメント
              const row = document.createElement('tr');
              row.innerHTML = `
              <td class="row-number"><a href="#" onclick="Num_Click('${item.friendlyName}', '${item.host}', '${item.hostDetail ? item.hostDetail.location : ''}', '${item.version}', '${item.publicKey}', 'TestNet');">${rowNum}</a></td>
              <td>${item.friendlyName}</td>
              <td>${item.host}</td>
              <td></td>
              <td>${item.version}</td>
              <td>${item.publicKey}</td>
            `;
              tbody.appendChild(row);
            }

          }

          // 特定の列の特定の数字の背景色を変更
          const cells = document.querySelectorAll('tbody td:nth-child(5)'); // Role列を選択

          cells.forEach((cell) => {
            if (cell.textContent === '16777989') { // 条件を確認
              cell.style.backgroundColor = 'aquamarine'; // 背景色を変更  
            }
            if (cell.textContent === '16777990') { // 条件を確認
              cell.style.backgroundColor = "skyblue"; // 背景色を変更  
            }
          });
        });
      })
      .catch((error) => {
        // エラーが発生した場合はコンソールに表示
        console.error("Fetch error:", error);
      });

    function Num_Click(friendlyName, host, hostDetail, version, node_publicKey, NET_Type) {
      const arrayBuffer = new TextEncoder().encode(`https://ventus-wallet.net/peernode/delegate.html?friendlyName=${encodeURIComponent(friendlyName)}&host=${encodeURIComponent(host)}&hostDetail=${encodeURIComponent(hostDetail)}&version=${encodeURIComponent(version)}&node_publicKey=${encodeURIComponent(node_publicKey)}&NET_Type=${encodeURIComponent(NET_Type)}`);
      const callback = Array.from(new Uint8Array(arrayBuffer), byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase();
      const encodedUrl = `alice://sign?type=request_pubkey&callback=${callback}`;
      window.location.href = encodedUrl;
    }

    ////////////////////////////////// chain height   Finalize height < MainNet > ////////////////////////////////////////////////////////////////

    const sym = require('/node_modules/symbol-sdk');
    let URL_M;
    let NODE_M;

    // エンドポイントのURL
    const endpointUrl_M = "https://symbol.services/nodes?filter=suggested&limit=3&ssl=true";

    // fetch()を使用してデータを取得
    fetch(endpointUrl_M)
      .then((response) => {
        // レスポンスが成功したかどうかを確認
        if (!response.ok) {
          throw new Error(`Network response was not ok: ${response.status}`);
        }
        // JSONデータを解析して返す
        return response.json();
      })
      .then((data) => {

        URL_M = data[0].host;
        NODE_M = `https://${URL_M}:3001`

        const chainHttp = new sym.ChainHttp(NODE_M);
        const nodeHttp = new sym.NodeHttp(NODE_M);

        nodeHttp.getNodeInfo().subscribe(x => {
          $("#node_host").text(`🖥️ ${x.host} 🖥️`);
          console.log("x.host　", x.host.toString());
        });

        function updateData() {
          chainHttp.getChainInfo().subscribe(x => {
            $("#chain_height_M").text(`⛓️ ${x.height} ⛓️`);
            //console.log("x.height", x.height.toString());

            $("#finalized_height_M").text(`🔒 ${x.latestFinalizedBlock.height} 🔒`);
          });

        }

        // 最初の実行
        updateData();

        // 1秒ごとにデータを更新
        setInterval(updateData, 1000);

      })
      .catch((error) => {
        console.error("Fetch error:", error);
      });


    ////////////////////////////////// chain height   Finalize height < TestNet > ////////////////////

    //const sym = require('/node_modules/symbol-sdk');
    let URL_T;
    let NODE_T;

    // エンドポイントのURL
    const endpointUrl_T = "https://testnet.symbol.services/nodes?filter=suggested&limit=3&ssl=true";

    // fetch()を使用してデータを取得
    fetch(endpointUrl_T)
      .then((response) => {
        // レスポンスが成功したかどうかを確認
        if (!response.ok) {
          throw new Error(`Network response was not ok: ${response.status}`);
        }
        // JSONデータを解析して返す
        return response.json();
      })
      .then((data) => {
        // JSONデータをコンソールに表示

        //console.log(data[0].host);
        URL_T = data[0].host;
        NODE_T = `https://${URL_T}:3001`

        const chainHttp = new sym.ChainHttp(NODE_T);
        const nodeHttp = new sym.NodeHttp(NODE_T);

        nodeHttp.getNodeInfo().subscribe(x => {
          $("#node_host").text(`🖥️ ${x.host} 🖥️`);
          console.log("x.host　", x.host.toString());
        });

        function updateData() {
          chainHttp.getChainInfo().subscribe(x => {
            $("#chain_height_T").text(`⛓️ ${x.height} ⛓️`);
           //console.log("x.height", x.height.toString());

            $("#finalized_height_T").text(`🔒 ${x.latestFinalizedBlock.height} 🔒`);
          });

        }

        // 最初の実行
        updateData();

        // 1秒ごとにデータを更新
        setInterval(updateData, 1000);

      })
      .catch((error) => {
        console.error("Fetch error:", error);
      });

    function updateClock() {
      var now = new Date();
      var hours = now.getHours().toString().padStart(2, "0");
      var minutes = now.getMinutes().toString().padStart(2, "0");
      var seconds = now.getSeconds().toString().padStart(2, "0");
      var timeString = hours + ":" + minutes + ":" + seconds;

      document.getElementById("clock").textContent = timeString;

      var year = now.getFullYear();
      var month = (now.getMonth() + 1).toString().padStart(2, "0");
      var day = now.getDate().toString().padStart(2, "0");
      var daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
      var dayOfWeek = daysOfWeek[now.getDay()]; // 曜日の短縮表記

      var dateString = year + "-" + month + "-" + day + " / " + dayOfWeek;

      document.getElementById("date").textContent = dateString;
    }

    // 初回の表示
    updateClock();

    // 1秒ごとに更新
    setInterval(updateClock, 1000);

  </script>
</body>

</html>

```

</div></details>

<details><summary>delegate.html</summary><div>

```html:delegate.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <script type="text/javascript" src="https://xembook.github.io/nem2-browserify/symbol-sdk-pack-2.0.4.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/2.1.2/sweetalert.min.js"></script>
</head>

<style>
  table {
    table-layout:fixed;
    width:100%;
  }

  table td,
  table th {
    padding: 8px;
    font-size: 14px;
    white-space: nowrap;   /* テキストを折り返さずに表示 */
    overflow: hidden;      /* オーバーフローした部分を隠す */
    text-overflow: ellipsis; /* オーバーフローした部分を省略記号で表示 */
  }
</style>

<body>
  <table border="1">
    <tr>
      <th></th>
      <th>Peer Node info</th>
    </tr>
    <tr>
      <td>Friendly Name</td>
      <td id="friendlyName"></td>
    </tr>
    <tr>
      <td>Host</td>
      <td id="host"></td>
    </tr>
    <tr>
      <td>Host Detail</td>
      <td id="hostDetail"></td>
    </tr>
    <tr>
      <td>Version</td>
      <td id="version"></td>
    </tr>
    <tr>
      <td>PublicKey</td>
      <td id="node_publicKey"></td>
    </tr>
    <tr>
      <td>Network Type</td>
      <td id="networkType"></td>
    </tr>
    <tr style="height: 20em">
      <td colspan="2" style="text-align: center;">
        <button id="delegateButton" style="font-size: 14px;
        width: 50%;
        height: 40px;
        background-image: linear-gradient(45deg, #f707ff 0%, #af05fe 100%);
        color: rgb(255, 255, 255);
        border-radius: 16px;
        cursor: pointer;
        box-shadow: 2px 2px 2px rgb(102, 113, 102);">このノードに委任する</button>
      </td>
    </tr>
  </table>
  <br>
  <br>
  <script>
    try {
      const queryString = window.location.search;
      if (!queryString) {
        console.error('正しい情報が取得できませんでした。');
        throw new Error('正しい情報が取得できませんでした。');
      }
      
      const params = new URLSearchParams(queryString);
      const aLice_pubKey = params.get('pubkey');
      const friendlyName = params.get('friendlyName');
      const host = params.get('host');
      const hostDetail = params.get('hostDetail');
      const version = params.get('version');
      const node_publicKey = params.get('node_publicKey');
      const aLice_NET_Type = params.get('network');
      let networkType = params.get('NET_Type');


      if (aLice_NET_Type !== networkType) {
        swal(`ネットワークエラー`, `aLiceのメインアカウントを確認してください。

        MainNet :　N _____
        TestNet :　T _____
        `);
      }

      if (friendlyName == null || host == null || hostDetail == null || version == null || node_publicKey == null || networkType == null) {
        console.error('正しい情報が取得できませんでした。', friendlyName, host, hostDetail, version, node_publicKey, networkType);
        throw new Error('正しい情報が取得できませんでした。');
      }

      document.getElementById('friendlyName').textContent = friendlyName;
      document.getElementById('host').textContent = host;
      document.getElementById('hostDetail').textContent = hostDetail;
      document.getElementById('version').textContent = version;
      document.getElementById('node_publicKey').textContent = node_publicKey;
      document.getElementById('networkType').textContent = networkType;


      if (networkType === 'MainNet') {
        networkType = 104;

      }
      if (networkType === 'TestNet') {
        networkType = 152;
      }


      const sym = require('/node_modules/symbol-sdk');

      // MAIN NET
      const NODE_M = 'https://symbol-mikun.net:3001'
      const repo_M = new sym.RepositoryFactoryHttp(NODE_M);
      const accountRepo_M = repo_M.createAccountRepository();
      const EPOCH_M = 1615853185;

      // TEST NET
      const NODE_T = 'https://testnet1.symbol-mikun.net:3001'
      const repo_T = new sym.RepositoryFactoryHttp(NODE_T);
      const accountRepo_T = repo_T.createAccountRepository();
      const EPOCH_T = 1667250467;

      let NODE;
      let accountRepo;
      let epochAdjustment;

      const aLicePublicAccount = sym.PublicAccount.createFromPublicKey(
        aLice_pubKey,
        networkType       
      );

      const address = sym.Address.createFromRawAddress(aLicePublicAccount.address.address);

      const check_netType = address.address.charAt(0);     // 1文字目を抽出

      if (check_netType === 'N') {           //ネットワークの判別　 メインネット 
        NODE = NODE_M;
        accountRepo = accountRepo_M;
        epochAdjustment = EPOCH_M;

        console.log("MAIN_NET");
      } else
        if (check_netType === 'T') {      // テストネット
          NODE = NODE_T;
          accountRepo = accountRepo_T;
          epochAdjustment = EPOCH_T;

          console.log("TEST_NET");
        }


      accountRepo.getAccountInfo(address)
        .toPromise()
        .then((accountInfo) => {
          const totalChainImportance = 78429286;

          let accountImportance = Number(accountInfo.importance.toString()) / totalChainImportance;
          if (accountImportance > 0) {
            accountImportance = Math.round(accountImportance);
            accountImportance /= 1000000;
            console.log("Importance=====", accountImportance)
          } else {
            swal(`インポータンスが無効です！`, `
                ${address.address}

                アカウントに 10,000 XYM 以上を保有して、
                約12時間経つとインポータンスが有効になります                   
                `);
            return;
          }

          let transactionList = [];

          //リモートアカウントの生成
          const remoteAccount = sym.Account.generateNewAccount(networkType);
          //VRFアカウントの生成
          const vrfAccount = sym.Account.generateNewAccount(networkType);

          //委任しているようであれば解除トランザクション作成
          if (accountInfo.supplementalPublicKeys.linked) {
            //AccountKeyLinkTransaction （解除）
            const accountUnLink_tx = sym.AccountKeyLinkTransaction.create(
              sym.Deadline.create(epochAdjustment),
              accountInfo.supplementalPublicKeys.linked.publicKey,
              sym.LinkAction.Unlink,
              networkType,
            );
            transactionList.push(accountUnLink_tx.toAggregate(aLicePublicAccount));
          }

          if (accountInfo.supplementalPublicKeys.vrf) {
            //VrfKeyLinkTransaction （解除）
            const vrfUnLink_tx = sym.VrfKeyLinkTransaction.create(
              sym.Deadline.create(epochAdjustment),
              accountInfo.supplementalPublicKeys.vrf.publicKey,
              sym.LinkAction.Unlink,
              networkType,
            );
            transactionList.push(vrfUnLink_tx.toAggregate(aLicePublicAccount));
          }

          if (accountInfo.supplementalPublicKeys.node) {
            //NodeKeyLinkTransaction （解除）
            const nodeUnLink_tx = sym.NodeKeyLinkTransaction.create(
              sym.Deadline.create(epochAdjustment),
              accountInfo.supplementalPublicKeys.node.publicKey,
              sym.LinkAction.Unlink,
              networkType,
            );
            transactionList.push(nodeUnLink_tx.toAggregate(aLicePublicAccount));
          }

          //AccountKeyLinkTransaction （リンク）                          
          const accountLink_tx = sym.AccountKeyLinkTransaction.create(
            sym.Deadline.create(epochAdjustment),
            remoteAccount.publicKey,
            sym.LinkAction.Link,
            networkType,
          );
          transactionList.push(accountLink_tx.toAggregate(aLicePublicAccount));

          //VrfKeyLinkTransaction （リンク）
          const vrfLink_tx = sym.VrfKeyLinkTransaction.create(
            sym.Deadline.create(epochAdjustment),
            vrfAccount.publicKey,
            sym.LinkAction.Link,
            networkType,
          );
          transactionList.push(vrfLink_tx.toAggregate(aLicePublicAccount));

          //NodeKeyLinkTransaction （リンク）
          const nodeLink_tx = sym.NodeKeyLinkTransaction.create(
            sym.Deadline.create(epochAdjustment),
            node_publicKey,
            sym.LinkAction.Link,
            networkType,
          );
          transactionList.push(nodeLink_tx.toAggregate(aLicePublicAccount));

          //PersistentDelegationRequestTransactionを作成
          const persistentDelegationRequest_tx = sym.PersistentDelegationRequestTransaction.createPersistentDelegationRequestTransaction(
            sym.Deadline.create(epochAdjustment),
            remoteAccount.privateKey,
            vrfAccount.privateKey,
            node_publicKey,
            networkType,
          );
          transactionList.push(persistentDelegationRequest_tx.toAggregate(aLicePublicAccount));

          //アグリゲートでまとめる
          const aggregate_tx = sym.AggregateTransaction.createComplete(
            sym.Deadline.create(epochAdjustment),
            transactionList,
            networkType,
            [],
          ).setMaxFeeForAggregate(100);

          const payload = aggregate_tx.serialize();
          document.getElementById("delegateButton").onclick = () => {
            window.location.href = `alice://sign?data=${payload}&type=request_sign_transaction&node=${sym.Convert.utf8ToHex(NODE)}&method=announce`;
          };
        });
    } catch (e) {
      swal(`エラーが発生しました！`, `${e}`);
    }
  </script>
</body>

</html>
```

</div></details>

***

# まとめ

ざっくりとポイントだけを解説してみましたが、全コード公開してますので、参考にしてみてください。
今回、aLice と連携するにあたっては、開発者の [toshi](https://twitter.com/toshiya_ma)さんからの多大なるサポートをいただき、完成まで辿り着く事が出来ました。ありがとうございました🙏

Symbolでは、困った時に優しく教えてくれる開発者さんが沢山いますので、分からない時は恥ずかしがらず、どんどん質問していきましょう☺️

**Symbol 開発者が集まる Discord サーバーはこちら💁‍♀️**

https://discord.gg/5CKEnxsJkX

# 最後に
以下は宣伝です。

NPO法人 NEMTUS では、今年もブロックチェーンを用いた開発をテーマにハッカソンを開催します。
今年のテーマは、Real Impact　～実社会における課題解決～
キックオフイベント、開発合宿イベントやピッチイベントなどのイベントも開催予定です。
ぜひお気軽にご参加ください☺️


https://hackathon-2024.nemtus.com