Symbolブロックチェーン上で持ってるNFT画像を全部出す。

今回はSymbolブロックチェーン上で展開されるNFTサービスで登録した画像を表示する方法を解説します。

テーマは自分のアカウントが所有しているモザイクトークンに紐づけられた画像情報を全部表示する、ということです。現在NEMberART、NFT-Drive、COMSAなど複数のNFTサービスがある中で、もちろんNFTが紐づいていないモザイクトークンもあります。それらが混ざったなかから、いかに目的の画像を引っ張ってくるかに注目してソースコードを読み解いてみてください。

#### この記事で扱うオンチェーン画像について

Symbolブロックチェーンで扱うオンチェーン画像について、ぽっと出てきたアイデアではなく、実はTopShotなどのNFTが流行る前の2019年から検証されてきたものです。NEM/Symbolで展開される技術はこういったコミュニティによる長い期間の検証に裏付けられたものだとご理解ください。

https://qiita.com/Hishikawa/items/403f22af69891ee78870


# 共通

### 検証環境構築

https://qiita.com/nem_takanobu/items/e88d5ea76cd609e76bbf

### 画像挿入の共有化

取得した画像をimgタグに挿入するfunctionを定義しておきます。ここは使い慣れたフレームワークの書き方に変更してください。

```js
function appendImg(src){

  (tag= document.createElement('img')).src = src;
  document.getElementsByTagName('body')[0].appendChild(tag);
}

```
# NFT-Drive

### トークン作成アカウント記述型

フルオンチェーンNFTです。モザイクトークンにはその作成者アカウントを記録する場所があり、その情報は改ざん不可能なため、そのアカウントへの送金トランザクションメッセージにファイル実体が記録されています。

```js
var nglist = [];
fetch('https://nft-drive-data-explorer.tk/black_list/',)
.then((response) => {
    return response.text().then(function(text) {
        nglist = JSON.parse(text);      
        console.log(text);
    });
});

function nftdrive(mosaic){
	mosaicRepo.getMosaic(mosaic.id)
	.pipe(
		op.filter(mo=>{
			return !nglist.find(elem => elem[1] === mo.id.toHex())
		})
	)
	.subscribe(async mo=>{

		const ownerAddress = mo.ownerAddress;
		const preTxes = await txRepo.search({
			type:[
				sym.TransactionType.TRANSFER,
			],
			address:ownerAddress,group:sym.TransactionGroup.Confirmed,pageSize:10,order:sym.Order.Asc
		}).toPromise();

		if(preTxes.data.find(tx => {
			if(tx.message === undefined){
				return false;
			}else if(tx.message.payload==="Please note that this mosaic is an NFT."){
				needSample = false;
				return true;
			}else{
				return false;
			}
		})){

			const tx = await txRepo.search({
				type:[
					sym.TransactionType.AGGREGATE_COMPLETE,
					sym.TransactionType.AGGREGATE_BONDED,
				],
				address:ownerAddress,group:sym.TransactionGroup.Confirmed,pageSize:100
			}).toPromise();

			const aggTxes = [];
			for (let idx = 0; idx < tx.data.length; idx++) {
				const aggTx = await txRepo.getTransaction(tx.data[idx].transactionInfo.hash,sym.TransactionGroup.Confirmed).toPromise();

				if(aggTx.innerTransactions.find(elem => elem.type === 16724)){
					aggTxes.push(aggTx);
				}
			}

			const sotedAggTxes = aggTxes.sort(function(a, b) {

				if (Number(a.innerTransactions[0].message.payload) > Number(b.innerTransactions[0].message.payload)) {return 1;} else {return -1;}
			})

			let nftData = "";
			let header = 15;
			for (let aggTx of sotedAggTxes) {

				for(let idx = 0 + header; idx < aggTx.innerTransactions.length;idx++){
					nftData += aggTx.innerTransactions[idx].message.payload;
				}
				header = 1;
			}

			if(nftData.indexOf("data:image/") >= 0){
				appendImg(nftData);
			}
		}
	});
}
```

### 注意点

トークンに記載されたアカウント情報は削除することができません。これはファイル実体とトークンが誰にも差し替え不可能なことを意味します。NFT-Driveが提供するブラックリスト一覧を取得して不適切な画像は表示しないようにしましょう。

# COMSA(NFT)

### トークンメタデータ内トランザクションハッシュ記述型

こちらもフルオンチェーンNFTです。モザイクトークン内のメタデータにファイル実体が記録されたトランザクション情報を格納されています。

```js

function comsa(mosaic){

	mosaicRepo.getMosaic(mosaic.id)
	.subscribe(async mo=>{

		let meta = await metaRepo.search({
			targetId:mo.id,
			metadataType:sym.MetadataType.Mosaic,
			pageSize:100
		}).toPromise();

		let comsaHeader = meta.data.find(tx=>tx.metadataEntry.scopedMetadataKey.toHex() === 'DA030AA7795EBE75');
		if(comsaHeader !== undefined){

			let headerJSON = JSON.parse(comsaHeader.metadataEntry.value);
			let aggTxes1 = meta.data.find(tx=>tx.metadataEntry.scopedMetadataKey.toHex() === 'D77BFE313AF3EF1F');
			let aggTxes2 = meta.data.find(tx=>tx.metadataEntry.scopedMetadataKey.toHex() === 'AACFBE3CC93EABF3');
			let aggTxes3 = meta.data.find(tx=>tx.metadataEntry.scopedMetadataKey.toHex() === 'A0B069B710B3754C');
			let aggTxes4 = meta.data.find(tx=>tx.metadataEntry.scopedMetadataKey.toHex() === 'D75B016AA9FAC056');

			let aggTxes = JSON.parse(aggTxes1.metadataEntry.value);

			if(aggTxes2 !== undefined){
				aggTxes = aggTxes.concat(JSON.parse(aggTxes2.metadataEntry.value));
			}

			if(aggTxes3 !== undefined){
				aggTxes = aggTxes.concat(JSON.parse(aggTxes3.metadataEntry.value));
			}
			
			if(aggTxes4 !== undefined){
				aggTxes = aggTxes.concat(JSON.parse(aggTxes4.metadataEntry.value));
			}

			let nftData = "";
			let dataType = "data:" + headerJSON.mime_type + ";base64,";
			for (let idx = 0; idx < aggTxes.length; idx++) {
				const aggTx = await txRepo.getTransaction(aggTxes[idx],sym.TransactionGroup.Confirmed).toPromise();
				for(let idx = 1; idx < aggTx.innerTransactions.length;idx++){
					let payload = aggTx.innerTransactions[idx].message.payload;
					nftData += payload.slice(6);
				}
			}
			appendImg(dataType + nftData);
		}
	});
}
```
### 注意点

サンプルプログラムはそこそこ大きい画像をターゲットに使用されているメタデータを参考に推定しました。
さらに大きなデータの場合、上記プログラムでは情報が欠損する可能性があります。発見した人はご指摘ください。また、フルオンチェーンなので画像データが消失することはありませんが、その画像とモザイクトークンをリンクさせているメタデータはサービス提供者により更新が可能です。


```text:現在判明している参照されているメタデータ
metadata id         先頭Index
D77BFE313AF3EF1F	00000
AACFBE3CC93EABF3	01470
A0B069B710B3754C	02940
D75B016AA9FAC056	04410
BABD9C10F590F0F3	05880
D4B5933FA2FD62E7	07350
FA60A37C56457F1A	08820
FEDD372E157E9CF0	10290
C9384119AD73CF95	11760
EADE00D8D78AC0BD	13230
F6578214308E7990	14700
CE7226A968287482	16261
C2811F3B6F49C568	17640
886A58DBE955A788	19110
87300B99E5B10E2C	20633
EE553D7141B98753	22050
B3084C09176CA990	23520

```

@TechBureau さんから復元方法についての公式記事が出ていますのでこちらもご参考ください。
（モザイクトークンのメタデータを使わないで、データを記録した時のトランザクション履歴から復元する方法が紹介されています）

https://qiita.com/TechBureau/items/3fd93d2b5a7d10d75785


# COMSA(NCFT)



```js
Buffer = require("/node_modules/buffer").Buffer;
function comsaNcft(mosaic){

	mosaicRepo.getMosaic(mosaic.id)
	.subscribe(async mo=>{

		let meta = await metaRepo.search({
			targetId:mo.id,
			metadataType:sym.MetadataType.Mosaic,
			pageSize:100
		}).toPromise();

		let comsaNcftHeader = xx.data.find(tx=>tx.metadataEntry.scopedMetadataKey.toHex() === '8E0823CEF8A40075');
		if(comsaNcftHeader !== undefined){
			needSample = false;
			let headerJSON = JSON.parse(comsaNcftHeader.metadataEntry.value);
			let aggTxes1 = meta.data.find(tx=>tx.metadataEntry.scopedMetadataKey.toHex() === 'D77BFE313AF3EF1F');
			let aggTxes2 = meta.data.find(tx=>tx.metadataEntry.scopedMetadataKey.toHex() === 'AACFBE3CC93EABF3');
			let aggTxes3 = meta.data.find(tx=>tx.metadataEntry.scopedMetadataKey.toHex() === 'A0B069B710B3754C');
			let aggTxes4 = meta.data.find(tx=>tx.metadataEntry.scopedMetadataKey.toHex() === 'D75B016AA9FAC056');

			let aggTxes = JSON.parse(aggTxes1.metadataEntry.value);

			if(aggTxes2 !== undefined){
				aggTxes = aggTxes.concat(JSON.parse(aggTxes2.metadataEntry.value));
			}

			if(aggTxes3 !== undefined){
				aggTxes = aggTxes.concat(JSON.parse(aggTxes3.metadataEntry.value));
			}
			
			if(aggTxes4 !== undefined){
				aggTxes = aggTxes.concat(JSON.parse(aggTxes4.metadataEntry.value));
			}

			let nftData = "";
			let dataType = "data:" + headerJSON.mime_type + ";base64,";
			for(aggTx of aggTxes){

				const data = {"transactionIds": [aggTx]}
				const res =  await fetch(nodeRepo.url + "/transactions/confirmed", {
					headers: {
						"Content-Type": "application/json;charset=utf-8"
					},
					method: "POST",
					body: JSON.stringify(data)
				});
				const json = await res.json();
				const innerTxes = json[0].transaction.transactions;
				let isSkip = true;
				for(innerTx of innerTxes){
					if(isSkip){
						isSkip = false;
						continue;
					}
					nftData += innerTx.transaction.message;
				}
			}

            appendImg(dataType  + Buffer.from(nftData, "hex").toString("base64"));
		}
	});
}


```

COMSA-NFTと異なる点はモザイクのメタデータに`8E0823CEF8A40075`が含まれていることが条件となります。
また、トランザクションのメッセージ領域にRawDataで記録されているので、APIに直接アクセスしヘッダ情報にあたる1件目のトランザクションを破棄して連結します。
最後に取得した16進数文字列をbase64変換して画像出力します。


# ウクライナNFT

### トークンメタデータ内トランザクションハッシュ記述型

フルオンチェーンNFTです。

```js
function ukraine(mosaic){

    mosaicRepo.getMosaic(mosaic.id)
    .subscribe(async mo=>{

        const meta = await metaRepo.search({
            targetId:mo.id,
            metadataType:sym.MetadataType.Mosaic,
            pageSize:100
        }).toPromise();

        const ukraineHeader = meta.data.find(tx=>tx.metadataEntry.scopedMetadataKey.toHex() === '8AFD95A719B1BB90');
        if(ukraineHeader !== undefined){

          const rootTransactionHash = JSON.parse(ukraineHeader.metadataEntry.value).info.rootTransactionHash;
          const tx = await txRepo.getTransactionsById([rootTransactionHash] ,sym.TransactionGroup.Confirmed).toPromise();
          const aggTxes = [];
          for (let i = 0; i < tx[0].innerTransactions[1].message.payload.length / 64; i++) {
              aggTxes.push(tx[0].innerTransactions[1].message.payload.substr(i * 64, 64));
          }

          let nftData = "";
          for(aggTx of aggTxes){

            const data = {"transactionIds": [aggTx]}
            const res =  await fetch(NODE + "/transactions/confirmed", {
              headers: {
                "Content-Type": "application/json;charset=utf-8"
              },
              method: "POST",
              body: JSON.stringify(data)
            });
            const json = await res.json();
            const innerTxes = json[0].transaction.transactions;
            for(innerTx of innerTxes){
              nftData += innerTx.transaction.message;
            }
          }

          const buffer = sym.Convert.hexToUint8(nftData);
          const blob = new Blob( [buffer], { type: 'image/png' } );
          const url = window.URL || window.webkitURL;
          appendImg(url.createObjectURL(blob));
        }
    });
}
```

メタデータにrootTransactionHashが記録されているので、そのトランザクションを見に行きます。
そのアグリゲートトランザクションの2件目にアクセスすべきトランザクションの一覧が区切り文字無しで詰め込まれているので64文字ごとに切り出して、順に読み込んでいきましょう。
トランザクションのペイロードにRawMessage形式でPNGデータを書き込んでいます。SDKではデコード時に不具合が出る可能性があるので使えません。

# NEMber ART

※NEMber ARTはサービス終了しています。

### IPFS ファイルハッシュ指定型

一般的なNFTの指定方法です。ブロックチェーン上にファイル実体は存在せず、モザイクトークンのメタデータにIPFS上のファイル実体へのアクセス方法を記述します。


```js

function nemberart(mosaic){
	metaRepo.search({
		scopedMetadataKey:"D2E513530574930D",
		targetId:mosaic.id,
		metadataType:sym.MetadataType.Mosaic
	}).subscribe(meta =>{

		if(meta.data.length > 0){
			let value = "";
			if ( meta.data[0].metadataEntry.value.indexOf('{') >= 0) {
				value = JSON.parse(meta.data[0].metadataEntry.value);
			}else{
				value = JSON.parse(sym.Convert.decodeHex(meta.data[0].metadataEntry.value));
			}
			appendImg("https://ipfs.io/ipfs/" + value.data.media.ipfs);
		}
	});
}

```

### 注意点

ipfsは画像が頻繁に参照されているか、あるいはピン止めという費用を払わないと画像ファイルが消失してしまいます。
NEMberARTがピン留め作業を代行しているかどうかは不明です。一時前は何回かアクセスするだけでキャッシュが復旧しましたが、最近のNFTブームにより比較的早く消失している可能性もありますのでご注意ください。


# 出力

最後に出力してみましょう。

```js

var address = sym.Address.createFromRawAddress("NCESRRSDSXQW7LTYWMHZOCXAESNNBNNVXHPB6WY");

accountRepo.getAccountInfo(address)
.subscribe(accountInfo => {
	accountInfo.mosaics.forEach(mosaic=>{
		console.log(mosaic);
		nemberart(mosaic);
		nftdrive(mosaic);
		comsa(mosaic);
		comsaNcft(mosaic);
		ukraine(mosaic);
	});
});

```

無事出力されましたでしょうか？
今回のプログラムは、今後サービス提供者の仕様変更によって画像が取り出せなくなる可能性があります。また、「公開されている仕様を参考にした」わけではなく、「こうすれば表示された」ものです。なのでデータ構造を模倣したNFTを表示してしまうかもしれない点はご留意ください。