Windows と TypeScript ではじめる Symbol 通信 13【アカウント情報】


## ノード情報の取得

### ノードへリクエスト

アカウント情報を取得するパケットタイプは、`0x443`です。

```typescript:Catapult.ts
/** パケットタイプ */
private PacketType = {
  PULL_BLOCK: 0x0_04,
  CHAIN_STATISTICS: 0x0_05,
  PULL_BLOCKS: 0x0_08,
  PUSH_TRANSACTIONS: 0x0_09,
  PUSH_PARTIAL_TRANSACTIONS: 0x1_00,
  NODE_DISCOVERY_PULL_PING: 0x1_11,
  TIME_SYNC_NETWORK_TIME: 0x1_20,
  FINALIZATION_STATISTICS: 0x1_32,
  UNLOCKED_ACCOUNTS: 0x3_04,
  ACCOUNT_INFOS: 0x4_43,
  METADATA_INFOS: 0x4_44,
}

async getAccountInfos(addresses: Uint8Array[]) {
  const accountInfos: AccountInfo[] = []
  try {
    const payload = Buffer.concat(addresses)
    const socketData = await this.request(this.PacketType.ACCOUNT_INFOS, payload)
    if (socketData) {
      let offset = 0
      do {
        const accountInfo = AccountInfo.deserialize(socketData.slice(offset))
        accountInfos.push(accountInfo)
        offset += accountInfo.accountInfoSize
      } while (offset < socketData.length)
    }
  } catch (e) {
    if (e instanceof Error) console.error(e.message)
    else console.error(e)
  } finally {
    this.close()
  }
  return accountInfos
}
```

### レスポンスデータの解析

シリアライズされたデータが返ってくるので、デシリアライズします。

```typescript:src/models/AccountInfo.ts
import { Address, models } from 'symbol-sdk/symbol'
import { PacketBuffer } from '../PacketBuffer.js'

// AccountType          Unlinked: 0x00, Main: 0x01, Remote: 0x02, Remote_Unlinked: 0x03
// AccountStateFormat   Regular(通常のアカウント): 0, HighValue(収穫可能なアカウント): 1
// AccountKeyTypeFlags  Unset: 0x00, Linked: 0x01, Node: 0x02, VRF: 0x04

export class AccountInfo {
  constructor(
    public accountInfoSize: number,
    public accountDataSize: number,
    public accountInfoKey: string,
    public version: number,
    public address: Address,
    public addressHeight: models.Height,
    public publicKey: models.PublicKey,
    public publicKeyHeight: models.Height,
    public accountType: number,
    public accountStateFormat: number,
    public accountKeyTypeFlags: number,
    public votingPublickeysCount: number,
    public linkedPublicKey: models.PublicKey | undefined,
    public nodePublicKey: models.PublicKey | undefined,
    public vrfPublicKey: models.PublicKey | undefined,
    public votingKeys: VotingKey[],
    public importanceScore: bigint | undefined,
    public importanceHeight: models.Height | undefined,
    public activityBuckets: ActivityBucket[],
    public balancesCount: number,
    public balances: models.Mosaic[]
  ) {}

  static deserialize(payload: Uint8Array) {
    const packetBuf = new PacketBuffer(Buffer.from(payload))
    const accountInfoSize = packetBuf.readUInt32LE()
    const accountDataSize = packetBuf.readUInt32LE()
    const accountInfoKey = packetBuf.readHexString(24).toUpperCase()
    const version = packetBuf.readUInt16LE()
    const address = Address.fromDecodedAddressHexString(packetBuf.readHexString(24))
    const addressHeight = new models.Height(packetBuf.readBigUInt64LE())
    const publicKey = new models.PublicKey(packetBuf.readHexString(32))
    const publicKeyHeight = new models.Height(packetBuf.readBigUInt64LE())
    const accountType = packetBuf.readUInt8()
    const accountStateFormat = packetBuf.readUInt8()
    const accountKeyTypeFlags = packetBuf.readUInt8()
    const votingKeysCount = packetBuf.readUInt8()
    let linkedPublicKey
    if (accountKeyTypeFlags & 0x01) {
      linkedPublicKey = new models.PublicKey(packetBuf.readHexString(32))
    }
    let nodePublicKey
    if (accountKeyTypeFlags & 0x02) {
      nodePublicKey = new models.PublicKey(packetBuf.readHexString(32))
    }
    let vrfPublicKey
    if (accountKeyTypeFlags & 0x04) {
      vrfPublicKey = new models.PublicKey(packetBuf.readHexString(32))
    }
    const votingKeys: VotingKey[] = []
    for (let i = 0; i < votingKeysCount; i++) {
      const votingPublicKey = new models.PublicKey(packetBuf.readHexString(32))
      const startEpoch = packetBuf.readUInt32LE()
      const endEpoch = packetBuf.readUInt32LE()
      const votingKey = new VotingKey(votingPublicKey, startEpoch, endEpoch)
      votingKeys.push(votingKey)
    }
    let importanceScore
    let importanceHeight
    const heightActivityBuckets: ActivityBucket[] = []
    if (accountStateFormat === 1) {
      importanceScore = packetBuf.readBigUInt64LE()
      importanceHeight = new models.Height(packetBuf.readBigUInt64LE())
      for (let i = 0; i < 5; i++) {
        const startHeight = new models.Height(packetBuf.readBigUInt64LE())
        const totalFeesPaid = new models.Amount(packetBuf.readBigUInt64LE())
        const beneficiaryCount = packetBuf.readUInt32LE()
        const rawScore = packetBuf.readBigUInt64LE()
        const activityBucket = new ActivityBucket(
          startHeight,
          totalFeesPaid,
          beneficiaryCount,
          rawScore
        )
        heightActivityBuckets.push(activityBucket)
      }
    }
    const balancesCount = packetBuf.readUInt16LE()
    const balances: models.Mosaic[] = []
    for (let i = 0; i < balancesCount; i++) {
      const mosaic = new models.Mosaic()
      mosaic.mosaicId = new models.MosaicId(packetBuf.readBigUInt64LE())
      mosaic.amount = new models.Amount(packetBuf.readBigUInt64LE())
      balances.push(mosaic)
    }

    return new AccountInfo(
      accountInfoSize,
      accountDataSize,
      accountInfoKey,
      version,
      address,
      addressHeight,
      publicKey,
      publicKeyHeight,
      accountType,
      accountStateFormat,
      accountKeyTypeFlags,
      votingKeysCount,
      linkedPublicKey,
      nodePublicKey,
      vrfPublicKey,
      votingKeys,
      importanceScore,
      importanceHeight,
      heightActivityBuckets,
      balancesCount,
      balances
    )
  }

  toJson() {
    const linkedPublicKey = this.linkedPublicKey
      ? { publicKey: this.linkedPublicKey?.toString() }
      : undefined
    const nodePublicKey = this.nodePublicKey
      ? { publicKey: this.nodePublicKey?.toString() }
      : undefined
    const vrfPublicKey = this.vrfPublicKey
      ? { publicKey: this.vrfPublicKey?.toString() }
      : undefined
    const votingKeys =
      this.votingKeys.length !== 0 ? this.votingKeys.map((v) => v.toJson()) : undefined
    return {
      version: this.version,
      address: this.address.toString(),
      addressHeight: this.addressHeight.value.toString(),
      publicKey: this.publicKey.toString(),
      publicKeyHeight: this.publicKeyHeight.value.toString(),
      accountType: this.accountType,
      supplementalPublicKeys: {
        linked: linkedPublicKey,
        node: nodePublicKey,
        vrf: vrfPublicKey,
        voting: votingKeys,
      },
      activityBuckets: this.activityBuckets.map((a) => a.toJson()),
      mosaics: this.balances.map((b) => b.toJson()),
      importance: this.importanceScore?.toString() ?? '0',
      importanceHeight: this.importanceHeight?.value.toString() ?? '0',
    }
  }
}

export class SupplementalPublicKeys {
  constructor(
    public linked: SupplementalPublicKey | undefined,
    public node: SupplementalPublicKey | undefined,
    public vrf: SupplementalPublicKey | undefined,
    public voting: VotingKey[]
  ) {}

  toJson() {
    return {
      linked: this.linked?.toString(),
      node: this.node?.toString(),
      vrf: this.vrf?.toString(),
      voting: this.voting.map((v) => v.toJson()),
    }
  }
}

export class SupplementalPublicKey {
  constructor(public publicKey: models.PublicKey) {}
  toJson() {
    return {
      publicKey: this.publicKey.toString(),
    }
  }
}

export class VotingKey {
  constructor(
    public publickey: models.PublicKey,
    public startEpoch: number,
    public endEpoch: number
  ) {}

  toJson() {
    return {
      publickey: this.publickey.toString(),
      startEpoch: this.startEpoch.toString(),
      endEpoch: this.endEpoch.toString(),
    }
  }
}

export class ActivityBucket {
  constructor(
    public startHeight: models.Height,
    public totalFeesPaid: models.Amount,
    public beneficiaryCount: number,
    public rawScore: bigint
  ) {}

  toJson() {
    return {
      startHeight: this.startHeight.value.toString(),
      totalFeesPaid: this.totalFeesPaid.value.toString(),
      beneficiaryCount: this.beneficiaryCount,
      rawScore: this.rawScore.toString(),
    }
  }
}
```

## メインの作成

実行するコードを作成します。

```typescript:src/mainAccountInfo.ts
import { Address } from 'symbol-sdk/symbol'
import { Catapult } from './Catapult.js'
import { utils } from 'symbol-sdk'

const addresses = [
  new Address('TADGQ7GDVIIOPD7TICWISNLARICML2TSFGEJWIA').bytes,
  new Address('TBZN46UIU5BFLJI46VB4JTHHCE5EN2RFLR7NX3A').bytes,
  new Address('TBJHAFYICU7LOTDROE7QVR6FN5O6KZYOAYLVSVA').bytes,
  new Address('TAX3CCCOQTC2JC5FA23DTUVIQKOMUO2NWFXAUXA').bytes,
  new Address('TATVSNP4N3IUP7VS57R7KSWZ7CKKX5U3YDPSOMQ').bytes,
]

const catapult = new Catapult(
  'cert/ca.crt.pem',
  'cert/node.crt.pem',
  'cert/node.key.pem',
  '127.0.0.1'
)
const accountInfos = await catapult.getAccountInfos(addresses)
for (const accountInfo of accountInfos) {
  console.log(JSON.stringify(accountInfo.toJson(), null, 2))
}
```

### 実行

```posh
yarn tsx .\src\mainAccountInfo.ts
```

実行すると以下の様に結果が出力されます。

```plaintext
socket connected.
{
  "version": 1,
  "address": "TADGQ7GDVIIOPD7TICWISNLARICML2TSFGEJWIA",
  "addressHeight": "144956",
  "publicKey": "03A9EF5A45C8D4D059D3E9C95DC5956091E3E855DC812F363DB41D987BF927A6",
  "publicKeyHeight": "144961",
  "accountType": 1,
  "supplementalPublicKeys": {
    "linked": {
      "publicKey": "B697066EC0419DF7354727822BB83581BC17E99D82914623700272BCA9C9B355"
    },
    "vrf": {
      "publicKey": "97D8B26A629FB6AF1CAA06441AEBDA8D8504F3E867292C01F9581A5C80FE5702"
    },
    "voting": [
      {
        "publickey": "EC167F9C009F64F183DEE8FECFA737DDFFB3FD6BC01C82E681ED8886D6D0D656",
        "startEpoch": "1568",
        "endEpoch": "2287"
      }
    ]
  },
  "activityBuckets": [
    {
      "startHeight": "1965240",
      "totalFeesPaid": "0",
      "beneficiaryCount": 0,
      "rawScore": "14178634374253"
    },
    {
      "startHeight": "1965060",
      "totalFeesPaid": "0",
      "beneficiaryCount": 0,
      "rawScore": "14177684407803"
    },
    {
      "startHeight": "1964880",
      "totalFeesPaid": "0",
      "beneficiaryCount": 0,
      "rawScore": "14177007248007"
    },
    {
      "startHeight": "1964700",
      "totalFeesPaid": "0",
      "beneficiaryCount": 0,
      "rawScore": "14176147652219"
    },
    {
      "startHeight": "1964520",
      "totalFeesPaid": "0",
      "beneficiaryCount": 0,
      "rawScore": "14174655376668"
    }
  ],
  "mosaics": [
    {
      "mosaicId": "8268645399043017678",
      "amount": "15515353361466"
    }
  ],
  "importance": "14177684407803",
  "importanceHeight": "1965240"
}
{
  "version": 1,
  "address": "TBZN46UIU5BFLJI46VB4JTHHCE5EN2RFLR7NX3A",
...
socket close: 1091
```