COMSAのバンドルNFT(NCFT)情報


## 目的

[COMSA](https://comsa.io/)におけるバンドルNFT(**NCFTと呼ぶのでなはく、バンドルNFTとする**)のオンチェーン保存方法を既存のNFTとは変更しているので、既存のNFT(**ユニークNFTと呼ぶ**) と比較しながら説明してみる。



## ユニークNFTとの比較表

|項目|ユニークNFT(1.0)| ユニークNFT(1.1)　※| バンドルNFT(NCFT)|
|--- | --- | --- | --- |
| トランザクション構成 | * Mosaicトランザクション <br> * ファイルアグリゲートトランザクション <br> * 画像サムネイルファイルアグリゲートトランザクション <br> [トランザクション内容](https://qiita.com/TechBureau/items/3fd93d2b5a7d10d75785#%E3%81%A9%E3%82%93%E3%81%AA%E3%83%87%E3%83%BC%E3%82%BF%E3%81%8C%E3%81%82%E3%82%8B) | * Mosaicトランザクション <br> * ファイルアグリゲートトランザクション <br> * 画像サムネイルファイルアグリゲートトランザクション| * Mosaicトランザクション <br> * ファイルアグリゲートトランザクション <br> * 画像サムネイルファイルアグリゲートトランザクション |
| Mosaicのメタキー(nft情報)| nft| nft | ncft|
| Mosaicのメタキー(version)| comsa-nft-1.0| **comsa-nft-1.1** |comsa-ncft-1.1 |
| Mosaic Transferble | true |true | false |
| Mosaic amount | 1 | 1 | 1-10000|
| ファイル保存分割形式 | base64 | **binary** | binary |

:::note alert
※ 2022/10/31以降に作成したユニークNFTは、ユニークNFT(1.1)　となります。
:::

## 非循環型のバンドルNFTとは？

既存のユニークNFTも、バンドルNFTどちらも１つの[Mosaic](https://docs.symbol.dev/concepts/mosaic.html)を使用してNFTを表現している。
Mosaicは作成時に[Transferble](https://docs.symbol.dev/concepts/mosaic.html#transferable)を無効にすると、必ずMosaic作成者が転送元もしくは転送先にいる必要があり、 不特定多数間でやり取りができない(非循環型)Mosaicを作成することができる。
そのためCOMSAプラットフォームのみでしか使用できない限定的なMosaicだが、シリアルナンバー付きの複数単位で作成できるMosaicとした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135107/f3117f16-9b4f-fe56-eb14-c70512a0c17d.png)


## シリアルナンバー付きのバンドルNFTって？

Mosaicのamountのtokenに対して、個々にシリアルナンバーつける機能がないが、上記に記載した通り、転送する処理はCOMSAプラットフォーム上のみのため、バンドルNFTを転送する際に、必ずメッセージにシリアルナンバーを付与することで、ブロックチェーン上でも確認することができる。バンドルNFTのMosaic転送トランザクションとメッセージを確認することで、シリアル毎に履歴を追跡することができる。

COMSA Explorer上では以下のように表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135107/8c47d951-b24f-2143-dae0-ef0c78eacbb7.png)

以下では、簡単にトランザクション履歴を取得するやり方だが、そこにmessageも合わせれば分かりやすくなる。


```js
const { 
  RepositoryFactoryHttp,
  MosaicId,
  Order,
  TransactionGroup} = require('symbol-sdk');

const url = "SymbolノードのURL"
const mosaic = "MosaicId";

const searchTransaction = async (pageNumber, transactionRepository, mosaicId) => {
  return await transactionRepository.search({
    transferMosaicId: mosaicId,
    pageSize: 100,
    pageNumber: pageNumber,
    order: Order.Desc,
    group: TransactionGroup.Confirmed
  }).toPromise()
}

(async() => {
  const repo = new RepositoryFactoryHttp(url);

  const transactionRepository = repo.createTransactionRepository();
  const mosaicId = new MosaicId(mosaic);

  const histories = [];

  let check = true;
  let count = 1;

  try {
    while (check) {
      const searchtx = await searchTransaction(count, transactionRepository, mosaicId);
      histories.push(searchtx);
      searchtx.isLastPage ? check = false : count++
    }

    histories.map((history) => {
      history.data.map((t) => {
        t.mosaics.map((mosaic) => {
          console.log('Block:', t.transactionInfo.height.compact(), 'Hash: ', t.transactionInfo.hash, 'from:', t.signer.address.plain(), 'To:', t.recipientAddress.plain(), t.message.payload)
        })
      });
    })
  } catch(e) {
    console.error(e)
  }

})()
```



## ファイル保存形式の変更

ユニークNFTは、ファイルをbase64に分割して転送トランザクションのメッセージに格納していたが、バンドルNFTはファイルをバイナリで分割する方法に変更した。
リリース当初からバイナリ格納は検討していたが、symbol-sdkが正式バージョンでBufferからのRaw Message(message.toBuffer())に対応していなかったため保留していたが、ある程度の期間、テストを行ったため、 [grush](https://github.com/symbol/garush)と同様なバージョン `1.0.3-message-improvement-202111021446` を使用した。

https://www.npmjs.com/package/symbol-sdk/v/1.0.3-message-improvement-202111021446

COMSA Explorer以外でも復元することはもちろんオンチェーンに保存している限りは誰でも可能なのでサンプルコードを以下に記載。
**ただし、上記に記載した通り、現状では上記のバージョンのみで復元が可能**

```js
const {
  RepositoryFactoryHttp,
  TransactionGroup,
  MetadataType,
  KeyGenerator,
  MosaicId,
} = require('symbol-sdk');
const fileType = require('file-type');
const fs = require('fs').promises;
const lodash = require('lodash');

const url = "SymbolノードのURL"
const mosaic = "MosaicId";

// 保存先
const fileWritePath = 'outputs';


const getDataFromTransactions = async (hashList, transactionRepository) => {
  const transactions = await transactionRepository.getTransactionsById(hashList, TransactionGroup.Confirmed).toPromise()
  const transactionMap = new Map();

  transactions.forEach(transaction => {
    if (transaction.transactionInfo.hash != null) {
      transactionMap.set(transaction.transactionInfo.hash, transaction)
    }
  })

  const dataList = []

  hashList.forEach(hash => {
    const transaction = transactionMap.get(hash)
    dataList.push(transaction.innerTransactions.slice(1).map(itrans => {
      return itrans.message.toBuffer()
    }))
  });

  const flatDataList = lodash.flatMap(dataList);

  const totalLength = flatDataList.reduce((acc, value) => acc + value.length, 0);
  console.log(totalLength)
  const result = new Uint8Array(totalLength);
  let length = 0;
  for (const array of flatDataList) {
    result.set(array, length);
    length += array.length;
  }
  return result
}

const metaEntry = async (metadataRepository, id) => {
  const entries = await metadataRepository.search({
    targetId: new MosaicId(id),
    metadataType: MetadataType.Mosaic,
    pageSize: 1000,
  }).toPromise()
  const entryMap = new Map();

  entries.data.forEach((metadata) => {
    entryMap.set(metadata.metadataEntry.scopedMetadataKey.toHex(), metadata.metadataEntry.value)
  })
  return entryMap
}

(async () => {
  try {
    const repo = new RepositoryFactoryHttp(url);
    const transactionRepository = repo.createTransactionRepository();
    const metadataRepository = repo.createMetadataRepository();

    // Mosaicに紐づくメタデータを取得
    const meta = await metaEntry(metadataRepository, mosaicId);
    
    // メタデータのkey dataXからアグリゲートを取得
    let aggregateHash = []
    for (let i=1; ;i++) {
      const data = meta.get(KeyGenerator.generateUInt64Key(`data${i}`).toHex());
      if (data == null) break;
      aggregateHash.push(JSON.parse(data))
    }

    // トランザクションからバイナリを取得
    const binary = await getDataFromTransactions(aggregateHash.flat(), transactionRepository);

    // file Type取得
    const fileExtension = (await fileType.fromBuffer(binary)).ext

    // ファイル名
    const fileName = 'image.' + fileExtension;

    // ファイル作成
    const res = await fs.writeFile(fileWritePath + '/' + fileName, binary);

    console.log("### Save " + fileWritePath + '/' + fileName + " ### ")

  } catch (e) {
    console.error(e)
  }

})()
```

上記ではメタデータからバンドルNFTを復元したが、ユニークNFT同様にトランザクション履歴から追うことももちろん可能。
どのように復元するかは、ユニークNFTの内容とほぼ同一の考え方なのでここでは割愛する。

https://qiita.com/TechBureau/items/3fd93d2b5a7d10d75785#comsa%E3%82%92%E4%BB%8B%E3%81%95%E3%81%9A%E3%81%ABsymbol%E3%81%A0%E3%81%91%E3%81%A7%E7%94%BB%E5%83%8F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E5%BE%A9%E5%85%83%E3%81%99%E3%82%8B