SymbolブロックチェーンでLINE風チャットルームを作ってみた

# はじめに
Symbolブロックチェーンでネームスペースの扱い方について何も知らなかったので、理解したくてネームスペースを使ったものをなにか作ってみようと思いました。
今回は開発中のSymbolChrome拡張機能に乗せられそうだったのでネームスペース機能を使ったチャットルームを作ってみました。

# 機能概要
1. 部屋名を入力して部屋を作成し、入室する。
1. 部屋内でメッセージを送ることができる。
1. 入室した部屋で過去にメッセージは送信されていた場合はそのメッセージを表示する。

# できたもの
<iframe width="560" height="315" src="https://www.youtube.com/embed/meRpaPcZo3A" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# ネームスペースとは
そもそもネームスペースとはなんぞやいう復習。

アドレスやモザイクに別名をつけることができる機能。
アドレスやモザイクは英数字の羅列だがネームスペースを使うことで別名で指すことができる。
公式ウォレットの送金機能のようにネームスペースのアドレス解決に対応している場合は宛先欄にネームスペースを入力すると紐付けられたアドレスに対して送信することができる。

　例）
　　アドレス：TC5FTDGNLLTVQGF7CW5OVP4RKFCJRJHIEZ4GEFA　
　　上記アドレスにネームスペース「mikan」と命名
　　→宛先に「mikan」と入力すると上記アドレスに着金する。

ルートネームスペースとサブネームスペースで構成されており、ルートネームスペースはSymbolブロックチェーン上で一意、サブネームスペースはルートネームスペースが異なれば一意でなくて良い。
　　例）
　　　　`symbol.xym`
　　　　symbol:ルートネームスペース
　　　　xym:サブネームスペース

ルートネームスペースは誰でも取得できるが取得時に設定する有効期限に応じて手数料をXYMで払う必要がある。

サブネームスペースの取得は紐付けようとしているルートネームスペースのオーナーである必要がある。
　　例）
　　　　ユーザA:ルートネームスペース「teria」のオーナー
　　　　ユーザB:サブネームスペース「unk」を作って「teria.unk」にしたろ！ ←できない

https://docs.symbolplatform.com/ja/concepts/namespace.html

# 流れ
ざっくり書くと
　　1. チャットルームに見立てるアカウントを作成して
　　2. 部屋名に見立てるサブネームスペースを取得して
　　3. 1のアドレスと2のネームスペースを紐付けて
　　4. 3で紐付けたネームスペース対してメッセージを送受信

# コード
jquery使ってます。

## 事前準備
1. 部屋名となるサブネームスペースの上位となるルートネームスペースの取得
1. 部屋（アカウント）とサブネームスペースを紐付ける手数料を払えるXYM残高を持つアカウント（1.のルートネームスペースのオーナー）

## 部屋作成画面
### ノードとアカウント情報のセット
```javascript
// テストネットノード情報
const NODE_URL = 'https://sym-test-01.opening-line.jp:3000';
const GENERATION_HASH = '7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836';
const EPOCH_ADJUSTMENT = 1637848847;
const networkType = symbol.NetworkType.TEST_NET;
// サブネームスペースの取得と紐付けを行うアカウント
// ルートネームスペースを所有しているアカウントであること
const private_key = '........................................................'
const account = symbol.Account.createFromPrivateKey(private_key, networkType);
```

### サブネームスペース取得

```javascript
// ルートネームスペースは予め取得しておいたやつ
const rootNamespaceName = 'symroom';
// 作成する部屋名（サブネームスペース）
const subnamespaceName = $('#room_name').val();
// サブネームスペース取得のトランザクションを発行
const namespaceRegistrationTransaction = symbol.NamespaceRegistrationTransaction.createSubNamespace(symbol.Deadline.create(EPOCH_ADJUSTMENT), subnamespaceName, rootNamespaceName, networkType, symbol.UInt64.fromUint(2000000));
const signedCreateSubNamespaceTransaction = account.sign(namespaceRegistrationTransaction, GENERATION_HASH);
const repositoryFactory = new symbol.RepositoryFactoryHttp(NODE_URL);
const transactionHttp = repositoryFactory.createTransactionRepository();
transactionHttp.announce(signedCreateSubNamespaceTransaction).subscribe((x) => console.log(x), (err) => console.error(err));
```

### 部屋（アカウント）作成
```javascript
// 部屋となるアカウントを作成（ここにメッセージを送り合う）
const room_account = symbol.Account.generateNewAccount(symbol.NetworkType.TEST_NET);
// サブネームスペースに紐付けるアカウントの残高が0XYMだとエラーになるためXYMを持たせる
let tx = symbol.TransferTransaction.create(
    symbol.Deadline.create(EPOCH_ADJUSTMENT),
    symbol.Address.createFromRawAddress(room_account.address.address),
    [new symbol.Mosaic(new symbol.MosaicId('091F837E059AE13C'), symbol.UInt64.fromUint(1 * 1000000))],
    // それっぽくなるようについでに初期表示用メッセージを送信しておく
    symbol.PlainMessage.create('システムメッセージ：\n <br> ルームキーワード「' + subnamespaceName + '」を作成しました。'),
    symbol.NetworkType.TEST_NET,
    symbol.UInt64.fromUint(100000)
);
// 署名
let signedTx = account.sign(tx, GENERATION_HASH);
// 送信
new symbol.TransactionHttp(NODE_URL)
    .announce(signedTx)
    .subscribe(
    (x) => console.log(x),
    (err) => console.error(err)
);
```



### 部屋名（サブネームスペース）とアカウントを紐付ける
```javascript
// ネームスペースIDを取得
const namespaceId = new symbol.NamespaceId('symroom.' + subnamespaceName);
// ネームスペースと紐付けるアカウント情報を取得
const rawAddress = room_account.address.address;
const address = symbol.Address.createFromRawAddress(rawAddress);
// 紐付けるトランザクションを発行
const addressAliasTransaction = symbol.AliasTransaction.createForAddress(
    symbol.Deadline.create(EPOCH_ADJUSTMENT),
    symbol.AliasAction.Link,
    namespaceId,
    address,
    networkType,
    symbol.UInt64.fromUint(2000000),
);
const signedTransaction = account.sign(
    addressAliasTransaction,
    GENERATION_HASH,
);
transactionHttp.announce(signedTransaction)
    .subscribe(
    (x) => console.log(x),
    (err) => console.error(err),
);

```

## チャットルーム内
### 部屋（アカウント）情報の取得
```javascript
// 部屋のアカウント情報を取得
// 今回はchromeのlocalstorageにセットしたのでそこから読み出す
const namespace = 'symroom.' + localStorage.getItem('joining_room');
const namespaceId = new symbol.NamespaceId(namespace);
```

### トランザクション取得
```javascript
// 使用するリポジトリをセット
const repositoryFactory = new symbol.RepositoryFactoryHttp(NODE_URL);
const transactionHttp = repositoryFactory.createTransactionRepository();
const nsRepo = repositoryFactory.createNamespaceRepository();
// ネームスペースからアドレスを解決し、ブロックチェーン上から当該部屋名宛のトランザクションを取得
nsRepo.getLinkedAddress(namespaceId).subscribe(addr=>{
    transactionHttp.search({
        group: symbol.TransactionGroup.Confirmed,
        order:symbol.Order.Asc,
        address:addr,
        pageNumber: 1,
        pageSize: 100,
    }).subscribe((page) => messageAnalysis(page.data))
});
```

### トランザクション解析
```javascript
async function messageAnalysis(data) {
    // 部屋名を上部に表示
    $('#title').text(localStorage.getItem('joining_room'));
    let message = '';
    for (tx of data) {
        if (tx.message.type === 0) {
            // 今回はchromeのlocalstorageから自身のアドレス読み出し
            if (tx.signer.address.address == localStorage.getItem('address')) {
                // 自分の送信したやつだったら右側にメッセージを表示
                message +=
                    `<div class="line__right">
                        <div class="text">` + tx.message.payload + `</div>
                    </div>`
            } else {
                // 自分のじゃなかったら左側に送信元アドレスとメッセージを表示
                message +=
                    `<div class="line__left">
                        <figure>
                            <img src="symbol_alter_cut.png"/>
                        </figure>
                        <div class="line__left-text">
                            <div class="name">` + tx.signer.address.address + `</div>
                            <div class="text">` + tx.message.payload + `</div>
                        </div>
                    </div>`
            }
        }
    }
    // 描画
    $('#message').html(message);
}
```

### メッセージ送信
```javascript
// メッセージ送信
$('#issue_tx').click(function() {
    // ノード情報をセット
    const NODE_URL = 'http://sym-test-01.opening-line.jp:3000';
    const GENERATION_HASH = '7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836';
    const EPOCH_ADJUSTMENT = 1637848847;
    // メッセージ送信するアカウントの情報を取得
    const privateKey = localStorage.getItem('private_key');
    const account = symbol.Account.createFromPrivateKey(privateKey, symbol.NetworkType.TEST_NET);

    // ネームスペースから宛先のアドレスを解決しながらトランザクションを作成
    nsRepo.getLinkedAddress(namespaceId).subscribe(addr => {
        let tx = symbol.TransferTransaction.create(
            symbol.Deadline.create(EPOCH_ADJUSTMENT),
            symbol.Address.createFromRawAddress(addr.address),
            [new symbol.Mosaic(new symbol.MosaicId('3A8416DB2D53B6C8'), symbol.UInt64.fromUint(0 * 1000000))],
            symbol.PlainMessage.create($('#input_message').val()),
            symbol.NetworkType.TEST_NET,
            symbol.UInt64.fromUint(100000)
        );
        // 署名
        let signedTx = account.sign(tx, GENERATION_HASH);
        // 送信
        new symbol.TransactionHttp(NODE_URL)
            .announce(signedTx)
            .subscribe((x) => console.log(x), (err) => console.error(err))
    });
});
```

# まとめ
Symbolブロックチェーンでネームスペース機能を使ってLINE風チャットルームを作ってみました。
ハマったところはアカウントとネームスペースを紐付けた後のトランザクション送信部分でアドレス解決が必要なことに気づかず
そのままnamespaceidを送信先アドレスとして指定してしまって、なんで送れないんじゃー！となっていました。
ヘルプを出したら偉大な先人サッと答えを提示してくれました。
きっと、下記記事のように検証環境を用意していたのでしょう。頼もしすぎる！
自分も下記記事を参考に検証環境を構築してみようと思いました。

https://qiita.com/nem_takanobu/items/e88d5ea76cd609e76bbf