Symbolのリスナーを複数登録・追加する方法

##はじめに
Symbolではwebsocketを利用してブロックを監視することが可能です。これはアプリケーションの幅を拡げる非常に重要な機能でブロックチェーンアプリを作成する上で覚えておくべきものかと思います。本記事ではブロックチェーンSymbolのリスナーを複数登録、またはスクリプト起動中に追加する方法を解説しますが、websocketの知識がある方にはさほど価値はないかもしれません。

自分が以前、とあることでハマった経験がありそれを解決した方法を記事にしました。もし同じような悩みをお持ちのかたの参考になれば幸いです。

__ブロック監視についての公式記事はこちら__

https://docs.symbolplatform.com/ja/guides/blockchain/listening-new-blocks.html

ある日、ひょんなことからSymbolを利用したマイレージシステムを作成したい衝動に駆られ、簡易的なものを作成しました。

https://nemlog.nem.social/blog/68675

※こちらのスクリプトはテストネットのリセットなどもあり現在は稼働していません。

簡単に説明すると、以下の３者が存在します。（１以外は複数）

・マイレージ運営
・マイレージ加盟店
・マイレージ利用者

流れは

１．加盟店は決まった加盟料を運営に支払い加盟する。
２．加盟店の受信トランザクションをリスナーで監視する。
３．利用者が加盟店でXYM支払いをすると同量のマイルが利用者に支払われる。

といったシンプルなものです。

ここでハマった点は２つです。

①加盟店数分のリスナーが起動している必要がある。しかし複数のリスナー登録の方法が分からなかった。

②スクリプト起動中に新たな加盟店が増えた場合のリスナーの自動登録方法が分からなかった。

websocketの知識がなかったために、このあたりでハマりました。

##リスナーの複数登録

まずは加盟店のリストアップをします。今回のケースであれば

・マイレージ運営に一定数以上のXYM送信とその際に登録用メッセージ"Regist"を送信している。

です。

```ts
import * as symbol from 'symbol-sdk';

const networkType = symbol.NetworkType.TEST_NET;
const nodeUrl = 'https://test.hideyoshi-node.net:3001';
const repositoryFactory = new symbol.RepositoryFactoryHttp(nodeUrl);
const TransactionRepo = new symbol.TransactionHttp(nodeUrl);

// 送金で使うので秘密鍵にしていますが、リスナー登録だけなら公開鍵で構いません
const adminPrivatekey = "D2F4CB68224057808FC2A5B28A1BDC958634FC904809D16CA8F55FBDCE8F****";
const adminAccount = symbol.Account.createFromPrivateKey(adminPrivatekey, networkType);

// APIで検索する条件です。
const searchCriteria = {
    group: symbol.TransactionGroup.Confirmed,
    recipientAddress: adminAccount.address,
}

const getShopList = async function (): Promise<string[]> {
    let shopList: string[] = [];
    const x = await TransactionRepo.search(searchCriteria).toPromise();
    // 取得したデータをさらに条件で判別します。実運用の際はページサイズやページ数も考慮する必要アリ
    for (let i = 0; i < x.data.length; i++) {
        // 今回はメッセージの内容で判別しているのでメッセージを取得し"Regist"かどうか見ています
        const message = (x.data[i] as symbol.TransferTransaction).message.payload;
        if (message == "Regist") {
            // メッセージがRegistの場合、XYMが一定数以上送信されているかどうか
            for (const mosaic of (x.data[i] as symbol.TransferTransaction).mosaics) {
                if (mosaic.id.id.toString() == new symbol.MosaicId("3A8416DB2D53B6C8").id.toString() 
                && mosaic.amount.compact() >= 10000000 
                && x.data[i].signer !== undefined) {
                    // 条件が一致する場合リストに入れていきます。今回は加盟店の公開鍵を配列に格納しています
                    shopList.push(x.data[i].signer!.publicKey);
                }
            }
        }
    }
    // 念の為重複チェック
    shopList = shopList.filter(function (ele, pos) {
        return shopList.indexOf(ele) == pos;
    })
    return shopList
}
getShopList().then(x=>console.log(x));
```

まずはここまででリスト一覧を取得します。コメントでそれぞれ意味は記載しています。もちろん運用の際は、条件などは変えて利用してください。また格納するデータも変わるかと思いますが、今回はシンプルに加盟店の公開鍵を配列に格納しています。

続いて、リストアップした加盟店をそれぞれリスナー登録していきます。

```ts
const createListeners = async function (listener: symbol.IListener, shopList: string[]) {
    // ショップリストをforで回してそれぞれリスナー登録する
    for (let shop of shopList) {
        const shopAccountAddress = symbol.PublicAccount.createFromPublicKey(shop, networkType).address
        listener.confirmed(shopAccountAddress).subscribe(
            async x => {
                // ここからはそのリスナーが反応した時にやりたいことを記述
                for (const mosaic of (x as symbol.TransferTransaction).mosaics) {
                    if (mosaic.id.id.toString() == new symbol.MosaicId("3A8416DB2D53B6C8").id.toString()
                    && (x as symbol.TransferTransaction).signer?.address.plain()) {
                        sendMileage((x as symbol.TransferTransaction).signer, mosaic.amount.compact() / 1000000)
                    }
                }
            }
        )
    }
}
```

ここはシンプルに、リストアップしたものをfor文でそれぞれリスナー登録するだけです。
この`createListeners`関数をのちほどしかるべき箇所で起動します。
各ショップのリスナーは自店舗の顧客がショップにモザイク送信をしていることを監視し、それがXYMの場合は同数のマイルを__マイレージ運営__が送信しています。その関数が`sendMileage`ですが本題とは関係ないので今回は端折ります。

##リスナーの自動登録

以下がメインのスクリプトです。

こちらを一度起動すれば、以降は基本的に加盟店リスナーは自動的に登録されていきます。


```ts
const createMainListener = async function () {
    const listener = repositoryFactory.createListener();
    // 初回起動時の加盟店リストアップ
    const shopList = await getShopList();
    listener.open().then(async () => {
        // 初回起動時のリスナー登録
        createListeners(listener, shopList);
        // 常時新たなブロックの監視（多分必要）
        listener.newBlock()
        // 加盟店アドレスがモザイクを受け取っているかの監視
        listener.confirmed(adminAccount.address).subscribe(
            async x => {
                // トランザクションは配列のためモザイク毎に見る
                for (const mosaic of (x as symbol.TransferTransaction).mosaics) {
                    // そのモザイクがXYMかどうか
                    if (mosaic.id.id.toString() == new symbol.MosaicId("3A8416DB2D53B6C8").id.toString()
                    && (x as symbol.TransferTransaction).signer?.address.plain()
                    // 加盟料を超えているかどうか
                    && mosaic.amount.compact() >= 10000000) {
                        // 条件が一致する場合、再度ショップリストの更新
                        const newShopList = await getShopList();
                        // 既に登録されているリスナーを省く、つまり既存の加盟店は削除する※ここ重要
                        const sabun = newShopList.filter(i => shopList.indexOf(i) == -1)
                        // 新規ショップのみリスナー登録
                        createListeners(listener, sabun)
                    }
                }
            }
        )
    })
}
```

こんな感じです。それぞれコメントにしています。ポイントは差分です。これを最初理解しておらず、ショップが登録されるたびに、既存のリスナーも再登録されていました。そのため、顧客が加盟店にXYM支払いをすると、`sendMileage`がリスナー分発動し、大量のトランザクションが発生するという事態になっていました。

あとは

```ts
createMainListener().then(() => {
    console.log("ok");
    process.exit(0);
}).catch(error => {
    console.error(error);
    process.exit(1);
});
```

こんな感じでメインのリスナーを起動してください。

##さいごに
ここまでご覧いただきありがとうございました。当然ながら作成したいアプリケーションに書き換えてご利用ください。

メインリスナーの条件を考える必要はあるかと思いますが、リスナーでできることは送金トランザクション受信だけじゃないはずなので、色々といじればできる幅は拡がりそうです。なお、今も変わらずwebsocketの知見があるわけではなく、大量のリスナーが登録されていいものか？などの疑問はありますので、詳しい方はご指摘あればぜひお願いします。

このあたりを理解すればもっと良い感じになり、できることも増えそうです


https://docs.symbolplatform.com/ja/api.html#websockets