14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

nem / symbolAdvent Calendar 2022

Day 4

Rest改造

Last updated at Posted at 2022-12-03

RestはCatapultと分離している

Peerノード、APIノード、Dualノード、様々なノードの種類を聞いたことがあるのではないでしょうか。ではこれらのノードの違いは何なのかというと、Restがあるか無いかが主な違いになります

  • Peer: Catapultのみ
  • API: Catapult+Rest
  • Dual: Catapult+Rest
  • Voting: Catapult: Rest
    細かく言えばCatapultとRestをつなぐbrokerが存在します(NodeとRestは生きてるのにbrokerが死んでるとかよくありますよね?)

Rest改造のすゝめ

CcatapultのRestはなんとNode.jsで動作しており普段Symbolで使用しているNode.jsの知識をそのまま利用できますCatapultはC++で面倒そう...といったかたでも改造しやすいのではないのかなと思います。

そして、RestはCatapultのMongoDBと直接通信ができます。つまり超高速でデータの収集が行えてしまうわけです。トランザクションの履歴からハーベスト履歴、その他今まで時間をかけていた作業が一瞬にて完結してしまうわけです。トランザクションのページ数制限なんかも全くありません。XEMBOOKだろうとなんだろうと一瞬です。無敵です。
今までRestのレート制限に困っていた人は少しやりたくなってきたのではないでしょうか、やりたくなってきましたよね(?)

とはいっても、SDKのようにドキュメントがあるわけでもなく、実装例や記事が多いわけでもないので他のトランザクションに対して実行されている処理を写経しながら処理を考える必要があります。もしよければ是非一緒に開拓しましょう

改造しよう

今回は以前公開した、ComsaNFTのパーサの実装を行ってみます。Comsaのトランザクション数が膨大になることはなんとなく想像できると思うので、Rest改造による絶大な効果を実感してもらいやすいのではないかなと思います。やることは偉大な先人と同じです

リポジトリをクローン & restへ移動

git clone https://github.com/symbol/symbol
cd symbol/client/rest

スキーマの定義

client/rest/src/catapult-sdk/plugins/content.js
const ModelType = require('../model/ModelType');

const content = {
    registerSchema: builder => {
        builder.addSchema('content.state', {
            'version':   ModelType.int,
        });
        builder.addSchema('content', {
            'info': ModelType.int,
            'mosaicAlias': { type: ModelType.array, schemaName: 'content.state' }
        });
    },

    registerCodecs: () => {},
};

module.exports = content;

システムへの登録

client/rest/src/catapult-sdk/plugins/catapultModelSystem.js
+const content = require('./content');
...
	transfer
	transfer,
+	content
};

登録ができたら、アクセス時の処理を書いていきます。つまりDBとやりとりするコアな部分を書いていきます
plugingsの中にディレクトリを作ってください

DB

収集対象を定義し、クエリを作成する役割を果たします
今回はComsaのパーサを実装するため、モザイクのメータデータを収集した後にトランザクションのハッシュからデータを収集するといった処理を行う必要があります。
そこでmetadataEntrytransactionsByHashesの2つをつくっておきます。今回はmosaicのmetadataから流用して作ってみました

client/rest/src/plugins/content/ContentDb.js
const { buildOffsetCondition } = require('../../db/dbUtils');
const { convertToLong } = require('../../db/dbUtils');
const MongoDb = require('mongodb');

const { Long } = MongoDb;

class ContentDb {
    constructor(db) {
        this.catapultDb = db;
    }

	metadataEntry(targetId, metadataType, options) {
		const sortingOptions = { id: '_id' };

		let conditions = {};
		conditions['metadataEntry.targetId'] = convertToLong(targetId);
		conditions['metadataEntry.metadataType'] = 1;//mosaic
		const offsetCondition = buildOffsetCondition(options, sortingOptions);
		if (offsetCondition)
			conditions = Object.assign(conditions, offsetCondition);

		if (undefined !== targetId)
			conditions['metadataEntry.targetId'] = convertToLong(targetId);

		if (undefined !== metadataType)
			conditions['metadataEntry.metadataType'] = metadataType;

		const sortConditions = { [sortingOptions[options.sortField]]: options.sortDirection };
		return this.catapultDb.queryPagedDocuments(conditions, [], sortConditions, 'metadata', options);
	}

	transactionsByHashes(hashes){
		return this.catapultDb.transactionsByHashes("confirmed", hashes);
	}
}

module.exports = ContentDb;

route

アクセス時の処理を記述します。Restはrestifyを利用して作成されており、ルートは次のように登録します

server.get('/content/comsa/:mosaicId', (req, res, next) => {
}

このようにした場合(http://hogehoge:3000/comsa/content/:mosaicId)のようにアクセスした場合処理が実行されるようになります
また、mosaicIdは次のようにして取得できます

const targetId = convertToLong(routeUtils.parseArgument(req.params, 'mosaicId', uint64.fromHex));

ページ数の制限はこのように指定することで強行突破が可能です

const options = { sortField: 'id', sortDirection: 1, pageSize: 1000, pageNumber: 1 };

あとはDBから情報をとってきて、Comsa公式の情報をもとにそのままパーサを実装してあげます。

復元が完了したら送信するようにしてあげます

routeUtils.createSender('content').sendContent(res, next)(
    Buffer.from(payload.filter(v => v).sort(numCompare).map((r) => r.split('#', 2)[1]).join(), "base64"),
	mime
);
client/rest/src/plugins/content/contentRoutes.js
const routeUtils = require('../../routes/routeUtils');
const { convertToLong } = require('../../db/dbUtils');
const catapult = require('../../catapult-sdk');
const { uint64 } = catapult.utils;
const errors = require('../../server/errors');

const is_tx = data => {
	try {
		let res = JSON.parse(data.buffer.toString());
		if(Array.isArray(res))return res.map(value=>{return routeUtils.namedParserMap.hash256(value)});
		else return undefined
	} catch (error) {
		return undefined
	}
}

const numCompare = (a, b) => {
	const numA = Number(a.split('#', 2)[0]);
	const numB = Number(b.split('#', 2)[0]);

	let comparison = 0;
	if (numA > numB) {
		comparison = 1;
	} else if (numA < numB) {
		comparison = -1;
	}
	return comparison;
}
module.exports = {
    register: (server, db, services) => {

		server.get('/content/comsa/:mosaicId', (req, res, next) => {
			try{
				const targetId = convertToLong(routeUtils.parseArgument(req.params, 'mosaicId', uint64.fromHex));
				const metadataType = 1;
				const options = { sortField: 'id', sortDirection: 1, pageSize: 1000, pageNumber: 1 };
				return db.metadataEntry(targetId, metadataType, options)
					.then(result => {
						const meta = result.data;
						const hashes = [];
						const payload = [];
						let mime = "";
						meta.forEach(m=>{
							const res = is_tx(m.metadataEntry.value);
							if(res!==undefined)hashes.push(...res);
						});
						// parse txs
						db.transactionsByHashes(hashes)
							.then(txs=>{
								if(txs.length>400)throw 'Large file is not allowed';
								txs.forEach(agg=>{
									if(!(agg.transaction.transactions.length > 1 &&
										agg.transaction.transactions[1].transaction.message?.buffer.slice(1,).toString().match(/^00000#/) != null &&
										agg.transaction.transactions.length < 99 )){
											if(mime=="")mime = JSON.parse(agg.transaction.transactions[0].transaction.message?.buffer.slice(1,).toString()).mime_type;
											agg.transaction.transactions.slice(1,).forEach(itx=>{
											payload.push(itx.transaction.message.buffer.slice(1,).toString());
										})
									}
								});
								routeUtils.createSender('content').sendContent(res, next)(
									Buffer.from(payload.filter(v => v).sort(numCompare).map((r) => r.split('#', 2)[1]).join(), "base64"),
									mime
									);
							});
							});
			}catch(e){
				res.send(errors.createInternalError('error retrieving data'));
			}
			next();
		}
	)}
};

モジュールのエクスポート

db,routesを出力します

client/rest/src/plugins/content/content.js
const ContentDb = require('./ContentDb');
const contentRoutes = require('./contentRoutes');

module.exports = {
    createDb: db => new ContentDb(db),

    registerTransactionStates: () => {},

    registerMessageChannels: () => {},

    registerRoutes: (...args) => {
        contentRoutes.register(...args);
    }
};

RouteUtilの改造

今回はNFTをデコードしてファイルを送信するように改造しますが、Restはjsonやtextの送信を目的として開発されており、ファイルを送信する機能がついていません。そこでrestifyの仕様とにらめっこしながらファイルの送信機能をcreateSenderに追加して実装してあげます

client/rest/src/routes/routeUtils.js
sendContent(res, next) {
    return (data, mime) => {
        res.setHeader('content-type', mime);
        res.write(data);
        res.end();
        next();
    };
}

ビルド

あとはDocker用にビルドしてsymbol-bootstrapで使えるようにしてあげれば正常に動作します。Dockerfileを書きます(レポジトリに見当たらなかったのでDiscordで質問したところ、過去のrestにあることを教えてもらえました)

FROM node:16

WORKDIR /app
COPY . .
RUN npm install
RUN node --version && npm --version
EXPOSE 3000

ビルドします

docker build -t monakajp/contetntrest:latest .

実行

ビルドできたらカスタムプリセットで作成したrestを指定し、起動してみましょう

custom.yml
symbolRestImage: monakajp/contetntrest:latest
symbol-bootstrap start -p mainnet -a dual -c custom.yml

起動後,一旦止めます

symbol-bootstrap stop
nano target/gateways/rest-gateway/rest.json

rest.jsonにプラグインを追加し、symbol-bootstrapで使えるように改良します

  "extensions": [
    "accountLink",
    "aggregate",
    "lockHash",
    "lockSecret",
    "mosaic",
    "metadata",
    "multisig",
    "namespace",
    "receipts",
    "restrictions",
    "transfer",
    "content"
  ],

ここでrest.jsonを変更するのは良いのですが、

symbol-bootstrap start --upgrade
symbol-bootstrap config

のようなコマンドを実行するとsymbol-bootstrapデフォルトのプリセットが上書きされて元に戻ってしまうので書き換えのたびに変更が必要です

/content/comsa/:mosaicIdにアクセスし、ファイルが帰ってくれば完成です

ソース

もともとこのソースは公開を実はこのソースの中身に別の新トランザクションの定義が混じっています。Restは事前に定義された情報以外返すことができないため、プラグイン等で新トランザクションを実装した場合データがRestから情報がとれないといった問題が発生してしまうのです。もし新トランザクションを作る際は参考にしてみてください

14
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?