Symbol対応版XEMBookを作る

Catapultエンジンを搭載したブロックチェーン、Symbol from NEMのパブリックローンチが現実味を帯びて来ました。

というわけでXEMBookをSymbol対応させました。
アルファ版として公開しています。
https://xembook.github.io/xembook/alpha.html

![スクリーンショット 2020-12-15 23.50.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29187/0dd5c65e-6e12-44b0-7ed6-baeb72de8626.png)

### XEMBookって何？
XEMBookは2017年4月ごろより、NEM1の残高やハーベストを確認するための銀行通帳(Passbook)のようなツールとして公開され、現在累計250万PVのアクセスを記録しているサービスです。TipNEMやXEMGallaryなどその時々のトレンドに合わせてページを追加してXEMを所有する多くのユーザに親しまれ、メッセージを一覧で表示できるXEMMessageや税務処理などに便利なXEMTaxなどがXEMBookシリーズとして現在でも有効活用されています。

### 解説

簡単に解説していきます。
ソースコードはこちらです。
https://github.com/xembook/xembook/blob/main/alpha.html

### HTML構造

```html
<div class="account col-sm-4">
<h2>アカウント</h2>
<dl id="account_info">
	<dt>口座名</dt><dd><span id="account_address"></span></dd>
	<dt>残高</dt><dd><span id="account_balance"></span></dd>
	<dt>重要度</dt><dd><span id="account_importance"></span></dd>
</dl>

<h2>取引所</h2>
<h2>接続ノード</h2>
<dl id="node_info">
	<dt>ノード</dt><dd><p><span id="node_host"></span></p></dd>
	<dt>ブロック</dt><dd><span id="chain_height"></span></dd>
</dl>

</div>
<div class="account col-sm-4">
<h2>ハーベスト:</h2>
<table id="harvest"  class="table">
	<thead><tr>
		<th>日時</th>
		<th>数量</th>
	</tr></thead>
</table>
<dd><a id="harvests_more" href="javascript:void(0)" >さらに読み込む</a></dd>
</div>

<div class="account col-sm-4">

<h2>送受信履歴:</h2>
<table id="table"  class="table">
	<thead><tr>
		<th>日時</th>
		<th>区分</th>
		<th>数量</th>
	</tr></thead>
</table>
<dd><a id="transfers_more" href="javascript:void(0)" >さらに読み込む</a></dd>
</div>


```

スタイル周りはbootstrap4を使用します。動的なコンテンツはjQueryで差し込みます。

```html
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" ></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<script src="symbol-sdk-pack-0.22.2.js"></script>
```

先日リリースされたばかりのsymbol-sdkのバージョン0.22.2を使用しています。

### クラス定義

```js
const NODE = 'https://d3rmzi6ltfh1jy.cloudfront.net';
const SYMBOL_EPOCH = 1573430400000;

const nem = require("/node_modules/symbol-sdk");
const op = require("/node_modules/rxjs/operators");
const rxjs = require("/node_modules/rxjs");

const chainHttp = new nem.ChainHttp(NODE);
const nodeHttp = new nem.NodeHttp(NODE);

const accountHttp = new nem.AccountHttp(NODE);
const mosaicHttp = new nem.MosaicHttp(NODE);
const nsHttp = new nem.NamespaceHttp(NODE);
const receiptHttp = new nem.ReceiptHttp(NODE);
const txHttp = new nem.TransactionHttp(NODE);
const blockHttp = new nem.BlockHttp(NODE);
const nsService = new nem.NamespaceService(nsHttp);

```

必要になりそうなクラスを定義しておきます。

### チェーン・ノード情報の取得

今回、XEMBookに新たに追加する情報です。接続するノードの最新情報を表示することで、接続ノードがいつの情報を提供しようとしているのか、どの時点までファイナライズされているのかを確認することができます。

```js
chainHttp.getChainInfo().subscribe(x=>{
	$("#chain_height").text(x.height + " [finalized:" + x.latestFinalizedBlock.height + "]");
});

nodeHttp.getNodeInfo().subscribe(x=>{
	$("#node_host").text(x.host);
});
```

ChainHttpから現在のブロック高とファイナライズ高の取得、NodeHttpから接続先のホスト名を取得します。

### アカウント情報の取得

```js
var accountInfo = accountHttp.getAccountInfo(alice);

accountInfo
.pipe(
	op.mergeMap(_=>_.mosaics),
	op.filter(_ => _.id.toHex() === "5B66E76BECAD0860"),
)
.subscribe(_=>{
	$("#account_balance").append("<dd>" + dispAmount(_.amount.toString(),6) + "</dd>");
});

accountInfo
.subscribe(_=>{

	var account_importance = Number(_.importance.toString()) / 100000000;
	account_importance = Math.round( account_importance );
	account_importance /= 10000;

	$("#account_importance").append("<dd>" + account_importance + "</dd>");
	getTransfers();
	getHarvests();

});
```
AccountHttpからAccountInfoを取得して現在の残高と重要度を取得します。最後に送受信履歴とハーベスト履歴の一覧を取得するための getTransfers()とgetHarvests()を呼び出します。


### トランザクション一覧

```js
function getTransfers(){

	txHttp.search({
		address:alice,
		group:nem.TransactionGroup.Confirmed,
		embedded:true,
		pageNumber:transferPageNumber,
		order:"desc"})
	.subscribe(_=>{
		transferPageNumber++;
		parseTx(_.data);
	});
}
```

txHttp.searchでアカウント別のトランザクションを取得できます。
取得できたらparseTxで表示用に整形していきます。

```js
function parseTx(txs){

	for(var tx of txs){
		if([
			nem.TransactionType.AGGREGATE_COMPLETE,
			nem.TransactionType.AGGREGATE_BONDED
		   ].includes(tx.type)){
 			txHttp.getTransactionsById([tx.transactionInfo.hash],nem.TransactionGroup.Confirmed)
 			.subscribe(aggTx =>{

 				parseTx(aggTx[0].innerTransactions);
 			});

		}else if(tx.type === nem.TransactionType.TRANSFER){

			xym = tx.mosaics.filter(item=> ["E74B99BA41F4AFEE","5B66E76BECAD0860"].includes(item.id.toHex()));
			for(mosaic of xym){

			}
		}
	}
}

```

構造のみを抜き出して説明します。
まず、取得したトランザクションが転送トランザクションかアグリゲートトランザクションかで処理を分けます。アグリゲートトランザクションだった場合は、さらにgetTransactionsByIdで内部トランザクションを取得して再帰的にトランザクションを解析します。転送トランザクションだった場合はモザイクの種類を検査してXYMだった場合に表示用レコードを追加します。

### 手数料計算

Symbolで大きく変わったのが手数料の計算です。詳しくはドキュメントを参考にしてください。

```js

if(alice.plain() === tx.recipientAddress.plain()){

}else{
	rxjs.zip(
		txHttp.getTransactionEffectiveFee(tx.transactionInfo.hash),
		rxjs.of({tx:tx})
	).subscribe(x => {
		$("#amount"+ x[1].tx.transactionInfo.id)
		.text(
			dispAmount(nem.UInt64.fromNumericString(x[0].toString()),6)
		);
	});
}

```
自分が送信者である場合のみ実行手数料を送金額に付加して表示します。

### ハーベスト一覧


```js
function getHarvests(){

	receiptHttp.searchReceipts({
		targetAddress:alice,
		receiptTypes:[nem.ReceiptType.Harvest_Fee],
		pageNumber:harvestPageNumber,
		pageSize:10,
		order:"desc"
	})
	.pipe(
		op.mergeMap(_=>_.data),
	)
	.subscribe(_=>{

		harvestPageNumber++;
		var receipt = _.receipts.filter(item=>{
			if(item.targetAddress){
				return item.targetAddress.plain() === alice.plain();
			}
			return false;
		});

		rxjs.zip(
			blockHttp.getBlockByHeight(_.height),
			rxjs.of({height:_.height})

		).subscribe(x => {

		})
	});
}
```

ハーベスト情報はレシートから取得します。対象アカウントとレシートタイプ`Harvest_Fee`を指定することで、対象アカウントが収穫したハーベストをレシート単位で抽出できます。抽出したレシートは自分の取り分とバリデータの取り分とインフレーション情報の３件が含まれているので、自分の取り分だけを抜き出します。また、収穫はブロック単位で記録されているので、blockHttp.getBlockByHeightでそのブロックのタイムスタンプを呼び出して収穫日時を計算します。

###　その他TIPS

##### 金額をうまいぐあいに表示する

```js
function dispAmount(amount,divisibility){

	var strNum = amount.toString();
	if(divisibility > 0){

		if(amount < Math.pow(10, divisibility)){

			return "0." + paddingAmount0(strNum,0,divisibility);

		}else{
			var r = strNum.slice(-divisibility);
			var l = strNum.substring(0,strNum.length - divisibility);
			return comma3(l) + "." + r;
		}
	}else{
		return comma3(strNum);
	}

}
function comma3(strNum){
	return strNum.replace( /(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
}

function paddingAmount0(val,char,n){
	for(; val.length < n; val= char + val);
	return val;
}
```

NEMで使用する通貨やモザイク残高は整数値で記録されています。これを実際の価値に換算する場合は可分性データから小数桁を計算しなければいけません、また可視性を向上させるために、整数部分を３桁カンマにするためのTIPSです。

##### 日時をうまい具合に表示する

```js
function dispTimeStamp(timeStamp,epoch){

	var d = new Date(timeStamp + epoch)
	var strDate = d.getFullYear()%100
		+ "-" + paddingDate0( d.getMonth() + 1 )
		+ '-' + paddingDate0( d.getDate() )
		+ ' ' + paddingDate0( d.getHours() )
		+ ':' + paddingDate0( d.getMinutes() ) ;
	return 	strDate;
}

function paddingDate0(num) {
	return ( num < 10 ) ? '0' + num  : num;
};
```

symbolが保存するタイムスタンプをJSのData関数を用いて整形するTIPSです。