目的
COMSAにおけるバンドルNFT(NCFTと呼ぶのでなはく、バンドルNFTとする)のオンチェーン保存方法を既存のNFTとは変更しているので、既存のNFT(ユニークNFTと呼ぶ) と比較しながら説明してみる。
ユニークNFTとの比較表
項目 | ユニークNFT(1.0) | ユニークNFT(1.1) ※ | バンドルNFT(NCFT) |
---|---|---|---|
トランザクション構成 | * Mosaicトランザクション * ファイルアグリゲートトランザクション * 画像サムネイルファイルアグリゲートトランザクション トランザクション内容 |
* Mosaicトランザクション * ファイルアグリゲートトランザクション * 画像サムネイルファイルアグリゲートトランザクション |
* Mosaicトランザクション * ファイルアグリゲートトランザクション * 画像サムネイルファイルアグリゲートトランザクション |
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 |
※ 2022/10/31以降に作成したユニークNFTは、ユニークNFT(1.1) となります。
非循環型のバンドルNFTとは?
既存のユニークNFTも、バンドルNFTどちらも1つのMosaicを使用してNFTを表現している。
Mosaicは作成時にTransferbleを無効にすると、必ずMosaic作成者が転送元もしくは転送先にいる必要があり、 不特定多数間でやり取りができない(非循環型)Mosaicを作成することができる。
そのためCOMSAプラットフォームのみでしか使用できない限定的なMosaicだが、シリアルナンバー付きの複数単位で作成できるMosaicとした。
シリアルナンバー付きのバンドルNFTって?
Mosaicのamountのtokenに対して、個々にシリアルナンバーつける機能がないが、上記に記載した通り、転送する処理はCOMSAプラットフォーム上のみのため、バンドルNFTを転送する際に、必ずメッセージにシリアルナンバーを付与することで、ブロックチェーン上でも確認することができる。バンドルNFTのMosaic転送トランザクションとメッセージを確認することで、シリアル毎に履歴を追跡することができる。
COMSA Explorer上では以下のように表示される。
以下では、簡単にトランザクション履歴を取得するやり方だが、そこにmessageも合わせれば分かりやすくなる。
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と同様なバージョン 1.0.3-message-improvement-202111021446
を使用した。
COMSA Explorer以外でも復元することはもちろんオンチェーンに保存している限りは誰でも可能なのでサンプルコードを以下に記載。
ただし、上記に記載した通り、現状では上記のバージョンのみで復元が可能
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の内容とほぼ同一の考え方なのでここでは割愛する。