実践低レベル Symbol -ノード情報-

## パケット読み込みクラス
パケットの取り扱いは面倒な所があるので、一旦読みだけのクラスを作成します。
```PacketBuffer.ts
export class PacketBuffer {
  private _length: number;
  private _index: number;

  constructor(private readonly _buffer: Buffer) {
    this._length = _buffer.length;
    this._index = 0;
  }

  addOffset(addOffset: number): number {
    this._index += addOffset;
    return this._index;
  }

  readUInt8(addOffset: number = 0): number {
    this.addOffset(addOffset);
    const readData = this._buffer.readUInt8(this._index);
    this._index += 1;
    return readData;
  }

  readUInt16LE(addOffset: number = 0): number {
    this.addOffset(addOffset);
    const readData = this._buffer.readUInt16LE(this._index);
    this._index += 2;
    return readData;
  }

  readUInt32LE(addOffset: number = 0): number {
    this.addOffset(addOffset);
    const readData = this._buffer.readUInt32LE(this._index);
    this._index += 4;
    return readData;
  }

  readBigUInt64LE(addOffset: number = 0): bigint {
    this.addOffset(addOffset);
    const readData = this._buffer.readBigUInt64LE(this._index);
    this._index += 8;
    return readData;
  }

  readString(length: number, addOffset: number = 0): string {
    this.addOffset(addOffset);
    const readData = this._buffer.toString('utf8', this._index, this._index + length);
    this._index += length;
    return readData;
  }

  readHexString(length: number, addOffset: number = 0): string {
    this.addOffset(addOffset);
    const readData = this._buffer.toString('hex', this._index, this._index + length);
    this._index += length;
    return readData;
  }

  get index(): number {
    return this._index;
  }

  get length(): number {
    return this._length;
  }
}
```

## ノード情報格納クラス
以下は、ノード情報を格納するクラスです。
Restゲートウェイの `/node/info` で取得出来るフォーマットとほぼ同じにしています。
違うところは、証明書期限を追加しているところだけです。
```NodeInfo.ts
export class NodeInfo {
  /**
   * コンストラクタ
   * @param version バージョン
   * @param publicKey 公開鍵
   * @param networkGenerationHashSeed ネットワークジェネレーションハッシュシード
   * @param roles ロール
   * @param port ポート
   * @param networkIdentifier ネットワーク識別子
   * @param host ホスト
   * @param friendlyName フレンドリーネーム
   * @param nodePublicKey ノード公開鍵
   * @param certificateExpirationDate 証明書有効期限
   */
  constructor(
    /**
     * バージョン
     */
    public readonly version: number,

    /**
     * 公開鍵
     */
    public readonly publicKey: string,

    /**
     * ネットワークジェネレーションハッシュ
     */
    public readonly networkGenerationHashSeed: string,

    /**
     * ロール
     */
    public readonly roles: number,

    /**
     * ポート
     */
    public readonly port: number,

    /**
     * ネットワークID
     */
    public readonly networkIdentifier: number,

    /**
     * ホスト
     */
    public host: string,

    /**
     * フレンドリー名
     */
    public readonly friendlyName: string,

    /**
     * ノード公開鍵
     */
    public readonly nodePublicKey: string,

    /**
     * 証明書有効期限
     */
    public readonly certificateExpirationDate?: Date
  ) {}
}
```

## ノード情報取得クラス
前回作成した `Catpult.ts` を編集します。
ノードと接続した際についでに証明書の期限も取得するようにしています。
```Catpult.ts
import { NodeInfo } from './NodeInfo.js';
import { PacketBuffer } from './PacketBuffer.js';
import { SslSocket } from './SslSocket.js';

export class Catpult extends SslSocket {
  /** パケットタイプ */
  private PacketType = {
    NODE_DISCOVERY_PULL_PING: 0x111,
  };

  /**
   * NodeInfo取得
   * @returns 成功: NodeInfo, 失敗: undefined
   */
  async getNodeInfo(): Promise<NodeInfo | undefined> {
    let nodeInfo: NodeInfo | undefined = undefined;
    try {
      // ピア問合せ
      const socketData = await this.requestSocket(
        this.PacketType.NODE_DISCOVERY_PULL_PING
      );
      if (!socketData) return undefined;
      // 編集
      const nodeBufferView = new PacketBuffer(Buffer.from(socketData));
      const version = nodeBufferView.readUInt32LE(4);
      const publicKey = nodeBufferView.readHexString(32).toUpperCase();
      const networkGenerationHashSeed = nodeBufferView
        .readHexString(32)
        .toUpperCase();
      const roles = nodeBufferView.readUInt32LE();
      const port = nodeBufferView.readUInt16LE();
      const networkIdentifier = nodeBufferView.readUInt8();
      const hostLength = nodeBufferView.readUInt8();
      const friendlyNameLength = nodeBufferView.readUInt8();
      const host = nodeBufferView.readString(hostLength);
      const friendlyName = nodeBufferView.readString(friendlyNameLength);
      // 証明書有効期限、ノード公開鍵取得
      const nodePublicKey = this._x509Certificate!.publicKey.export({
        format: 'der',
        type: 'spki',
      })
        .toString('hex', 12, 44)
        .toUpperCase();
      const validTo = this._x509Certificate!.validTo;
      const validToDate = new Date(validTo);
      const certificateExpirationDate = validToDate;
      nodeInfo = new NodeInfo(
        version,
        publicKey,
        networkGenerationHashSeed,
        roles,
        port,
        networkIdentifier,
        host,
        friendlyName,
        nodePublicKey,
        certificateExpirationDate
      );
    } catch (e) {
      nodeInfo = undefined;
    }
    return nodeInfo;
  }
}
```

## メイン
`Catpult` のインスタンスを生成して呼び出すだけです。
```main.ts
import { Catpult } from './Catpult.js';

const catpult = new Catpult('sakia.harvestasya.com');
const nodeInfo = await catpult.getNodeInfo();
console.log(nodeInfo);
```


## 実行
ノード側の設定を何もしない無い場合、連続してアクセスすると拒否されるので30秒から1分ほど待ってから再実行してください。
```
> yarn tsx ./src/main.ts
NodeInfo {
  version: 16777990,
  publicKey: '7587ECE8D3FA11A075E533E83F2F1CC8E09F7D2E1D1BD547A44AC5D4D4C78242',
  networkGenerationHashSeed: '49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4',
  roles: 1,
  port: 7900,
  networkIdentifier: 152,
  host: 'symbol02.harvestasya.com',
  friendlyName: '_Symbol_TestNet_HarvestasyaNode02/.',
  nodePublicKey: '5CFF56769EABA56315233552C849BA9FF6A72130F6AF802996C2EB0EA05AAD6F',
  certificateExpirationDate: 2024-11-10T21:26:25.000Z
}
```