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
スキーマの定義
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;
システムへの登録
+const content = require('./content');
...
transfer
transfer,
+ content
};
登録ができたら、アクセス時の処理を書いていきます。つまりDBとやりとりするコアな部分を書いていきます
plugingsの中にディレクトリを作ってください
DB
収集対象を定義し、クエリを作成する役割を果たします
今回はComsaのパーサを実装するため、モザイクのメータデータを収集した後にトランザクションのハッシュからデータを収集するといった処理を行う必要があります。
そこでmetadataEntry
とtransactionsByHashes
の2つをつくっておきます。今回はmosaicのmetadataから流用して作ってみました
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
);
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を出力します
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
に追加して実装してあげます
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を指定し、起動してみましょう
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から情報がとれないといった問題が発生してしまうのです。もし新トランザクションを作る際は参考にしてみてください