Blockchain Symbol の転送トランザクションを発行する【ピアアナウンス】

SSL パケット通信を使って API を介さずにネットワークにアナウンスすることも出来ます。

「Blockchain Symbol の転送トランザクションを発行する」をピアアナウンスに変更してみます。

https://qiita.com/ccHarvestasya/items/e79bd01b7f8a9d65a921

## 証明書の作成

証明書は下記を参照して作成。
OpenSSL のバージョンは 3.1.4 で確認しています。

https://qiita.com/ccHarvestasya/items/d38ee4a2ab5e343c2f20#%E3%83%8E%E3%83%BC%E3%83%89%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%81%AE%E7%94%9F%E6%88%90

## ピアノードにアナウンスするクラス

```SymbolAnnouncer.ts
import fs from "fs";
import tls, { ConnectionOptions } from "tls";

enum PacketType {
  /** トランザクションアナウンス */
  PUSH_TRANSACTIONS = 0x009,
}

export class SymbolCatapult {
  private connectionOptions: ConnectionOptions;

  /**
   * コンストラクタ
   * @param nodeHost ノードホスト
   * @param nodePort ノードポート(デフォルト: 7900)
   */
  constructor(private nodeHost: string, private nodePort: number = 7900) {
    this.connectionOptions = {
      host: this.nodeHost,
      port: this.nodePort,
      timeout: 3000,
      cert: fs.readFileSync("cert/node.full.crt.pem"),
      key: fs.readFileSync("cert/node.key.pem"),
      rejectUnauthorized: false,
    };
  }

  /**
   * トランザクションアナウンス
   * @param payload トランザクションペイロード
   */
  public async announceTransaction(payload: Uint8Array): Promise<void> {
    await this.catapult(PacketType.PUSH_TRANSACTIONS, payload, false);
  }

  /**
   * カタパルトサーバへパケットを送信する
   * @param packetType パケットタイプ
   * @param payload ペイロード
   * @param isResponse レスポンス有無(デフォルト: true)
   * @returns レスポンスデータ
   */
  private async catapult(
    packetType: PacketType,
    payload?: Uint8Array,
    isResponse: boolean = true
  ): Promise<Uint8Array | undefined> {
    return new Promise<Uint8Array | undefined>((resolve, reject) => {
      const payloadSize = payload ? payload.length : 0;
      let responseSize = 8; // ヘッダ分のサイズを前もって付与
      let responseData: Uint8Array | undefined = undefined;

      let socket = tls.connect(this.connectionOptions, () => {
        // Symbolパケット生成
        const headerSize = 8;
        const packetSize = headerSize + payloadSize;
        const symbolPacketBuffer = new ArrayBuffer(packetSize);
        // Symbolヘッダー編集
        const symbolHeader = new DataView(symbolPacketBuffer);
        symbolHeader.setUint32(0, packetSize, true);
        symbolHeader.setUint32(4, packetType, true);

        if (payload) {
          // Symbolペイロード編集
          const symbolPayload = new Uint8Array(
            symbolPacketBuffer,
            headerSize,
            payloadSize
          );
          symbolPayload.set(payload);
        }

        // Symbolパケット送信
        socket.write(new Uint8Array(symbolPacketBuffer));
        console.debug(`packet type ${packetType} written`);

        if (!isResponse) {
          // ソケット切断
          socket.destroy();
        }
      });

      // SSL接続時
      socket.on("secureConnect", () => {
        const peerX509 = socket.getPeerX509Certificate();
        if (!peerX509) return;
        console.log(`${peerX509}`);
      });

      // データ受信
      socket.once("data", (data) => {
        // レスポンスデータ（ヘッダ）取得
        const nodeBufferView = Buffer.from(new Uint8Array(data).buffer);
        // レスポンスサイズチェック
        const responseDataSize = nodeBufferView.readUInt32LE(0);
        if (responseDataSize === 0) {
          socket.destroy();
          reject("empty data");
        }
        // レスポンスパケットタイプチェック
        const responsePacketType = nodeBufferView.readUInt32LE(4);
        if (responsePacketType !== packetType) {
          socket.destroy();
          reject(
            `mismatch packet type: expect: ${packetType} actual: ${responsePacketType}`
          );
        }

        // ヘッダが問題なければデータ部取得
        socket.on("data", (data) => {
          const tempResponseData = new Uint8Array(data);
          console.debug(tempResponseData);
          responseSize += tempResponseData.length;
          if (!responseData) {
            // 初回
            responseData = tempResponseData;
          } else {
            // 連結
            const merged = new Uint8Array(
              responseData.length + tempResponseData.length
            );
            merged.set(responseData);
            merged.set(tempResponseData, responseData.length);
            responseData = merged;
          }
          if (responseDataSize <= responseSize) {
            // 受信が終わったら終了
            socket.end();
          }
        });
      });

      socket.on("timeout", function () {
        socket.destroy();
        reject("timeout");
      });

      socket.on("error", function (error) {
        socket.destroy();
        reject(error);
      });

      socket.on("close", function () {
        resolve(responseData);
      });
    });
  }
}
```

## アナウンス部分の変更

REST にアナウンス部分を下記に書き換えます。
送信するノードは、ノード間通信ポートが開いているノードであればどれでも良いです。
ソケット通信なのでhttp等の情報は不要です(httpと違ってトランスポート層なので...)。

```typescript
/** ノードホスト */
const NODE = "symbol02.harvestasya.com";

// Peerノードにアナウンス
const symbolCatapult = new SymbolCatapult(NODE);
await symbolCatapult.announceTransaction(transferTx.serialize());
```