今回はCatapultのトランザクションを送信します。
これができればもうCatapultで何だってできる(はずです)。
注)今回説明するのはXEMBook-sdkを利用したトランザクションの作成方法です。
NEM財団が推奨するnem2-sdkを使用する場合はnem developer centerを参考にしてください。
https://nemtech.github.io/ja/guides/transfer/sending-a-transfer-transaction.html
必要な作業は以下の通りです。
- 必要なデータを揃える
- データをシリアライズ化する
- シリアライズされたデータを署名する
- Catapultネットワークにアナウンスする
ではまず、データ構成です。JSONオブジェクト形式で定義していきます。
const jsonTx = {
"version": 36867,
"type": 16724,
"fee": [0, 0],
"deadline": deadline(),
"recipient": address,
"message": {
"type": 0,
"payload": "Hello XEMBook-sdk! "
},
"mosaics": [
{
"id": [853116887, 2007078553],
"amount": [0, 0]
}
]
}
NEM1のころと大きく変わっている箇所があります。
fee,amountが [0,0]という感じで配列になっていますね。
これはJavaScriptなどで小数点を含んだ桁幅の大きい数字を扱う(四則演算する)と誤差が生じてしまうので分解して指定することになります。大きな数字でなければ[1234,0]といった感じで0番目(左側)の方に指定してやります。
deadlineも配列指定になるので、uint64.fromUintというfunctionを使って以下のように出力させます。
NEMは独自のタイムスタンプを持っており西暦には依存しません。西暦で言うと2016-04-01 00:00(UTC 1459468800000ミリ秒)がCatapultの始まりと定義されているようです。
function deadline(deadlineParam) {
const NetworkTime = (new Date()).getTime() - 1459468800000;
const deadlineValue = deadlineParam || 60 * 60 * 1000;
return uint64.fromUint(deadlineValue + NetworkTime);
}
次に作成したデータをシリアライズします。
通常のWebサービスだとjsonのまま気軽になげられるのですが、ブロックチェーンではクライアント側で署名をする必要があるので、作成したデータの内容が一意になるようにシリアライズする必要があります。個人的にはここが一番ハードルが高いかなと思っています。
const serializedTx = serialize(jsonTx);
XEMBook-sdkライブラリを使うと1行でかけますが、以下のような処理を内部で行なっています。今回もNodeJS依存のflatbuffersを利用しているのでbrowserifyでバンドルしたものをrequireしておきます。
function serialize(transfer){
let xembook = require("/main.js");
var builder = xembook.getFlatbuffers();
// メッセージ部
const bytePayload = convert.hexToUint8(convert.utf8ToHex(transfer.message.payload));
const payload = createVector(builder, 1,bytePayload,1,'addInt8');
builder.startObject(2);
builder.addFieldInt8(0, transfer.message.type, 0);
builder.addFieldOffset(1, payload, 0);
const message = builder.endObject();
// モザイク部
const mosaics = [];
transfer.mosaics.forEach(mosaic => {
const id = createVector(builder, 4,mosaic.id ,4,'addInt32');
const amount = createVector(builder, 4,mosaic.amount,4,'addInt32');
builder.startObject(2);
builder.addFieldOffset(0, id, 0);
builder.addFieldOffset(1, amount, 0);
mosaics.push(builder.endObject());
});
const feeVector = createVector(builder, 4,transfer.fee ,4,'addInt32');
const mosaicsVector = createVector(builder, 4,mosaics ,4,'addOffset');
const signerVector = createVector(builder, 4,Array(...Array(32)).map(Number.prototype.valueOf, 0),4,'addInt8');
const deadlineVector = createVector(builder, 4,deadline() ,4,'addInt32');
const recipientVector = createVector(builder, 1,stringToAddress(transfer.recipient),1,'addInt8');
const signatureVector = createVector(builder, 1,Array(...Array(64)).map(Number.prototype.valueOf, 0),1,'addInt8');
builder.startObject(12);
builder.addFieldInt32(0, 149 + (16 * transfer.mosaics.length) + bytePayload.length, 0);
builder.addFieldOffset(1, signatureVector, 0);
builder.addFieldOffset(2, signerVector, 0);
builder.addFieldInt16( 3, transfer.version, 0);
builder.addFieldInt16( 4, transfer.type, 0);
builder.addFieldOffset(5, feeVector, 0);
builder.addFieldOffset(6, deadlineVector, 0);
builder.addFieldOffset(7, recipientVector, 0);
builder.addFieldInt16( 8, bytePayload.length + 1, 0);
builder.addFieldInt8( 9, transfer.mosaics.length, 0);
builder.addFieldOffset(10, message, 0);
builder.addFieldOffset(11, mosaicsVector, 0);
const codedTransfer = builder.endObject();
builder.finish(codedTransfer);
bytes = builder.asUint8Array();
let i = 0;
let resultBytes = [];
let buffer = Array.from(bytes);
resultBytes = resultBytes.concat(findParam( buffer[0], 4 + (i * 2), buffer, 4));i++;
resultBytes = resultBytes.concat(findVector(buffer[0], 4 + (i * 2), buffer, 1));i++;
resultBytes = resultBytes.concat(findVector(buffer[0], 4 + (i * 2), buffer, 1));i++;
resultBytes = resultBytes.concat(findParam( buffer[0], 4 + (i * 2), buffer, 2));i++;
resultBytes = resultBytes.concat(findParam( buffer[0], 4 + (i * 2), buffer, 2));i++;
resultBytes = resultBytes.concat(findVector(buffer[0], 4 + (i * 2), buffer, 4));i++;
resultBytes = resultBytes.concat(findVector(buffer[0], 4 + (i * 2), buffer, 4));i++;
resultBytes = resultBytes.concat(findVector(buffer[0], 4 + (i * 2), buffer, 1));i++;
resultBytes = resultBytes.concat(findParam( buffer[0], 4 + (i * 2), buffer, 2));i++;
resultBytes = resultBytes.concat(findParam( buffer[0], 4 + (i * 2), buffer, 1));i++;
resultBytes = resultBytes.concat(tableSerialize( buffer, 4 + (i * 2)));i++;
resultBytes = resultBytes.concat(tableArraySerialize(buffer, 4 + (i * 2)));i++;
return resultBytes;
}
これでシリアライズできました。大きく分けて固定長部とmosaic部とmessage部の3部分で構成されます。bufferに詰め込んでいって、バイト配列に並べて行きます。以下のようなものが出来上がります。
C000000046D0B4EB4677D2FCEA0D5242E9E8368ED49293B13F6C61A3DEADE0746835B69F9F3F9B6927A42D0C4A063357FE9DA21B75A43064D6CC99904C153832BF820607FF6E61F2A0440FB09CA7A530C0C64A275ADA3A13F60D1EC916D7F1543D7F0574039054410000000000000000FB1B5A101700000090758EB47C28D6143BAA3DE6A8D9C319B503A1BFD8E789E9E20C00020048656C6C6F204E656D322144B262C46CEABB8500000000000000001C29E1B7B29912940000000000000000
適当に改行をいれていくとこんな感じです。
C0000000 //サイズ
46D0B4EB4677D2FCEA0D5242E9E8368ED49293B13F6C61A3DEADE0746835B69F9F3F9B6927A42D0C4A063357FE9DA21B75A43064D6CC99904 C153832BF820607 //署名
FF6E61F2A0440FB09CA7A530C0C64A275ADA3A13F60D1EC916D7F1543D7F0574 //署名者公開鍵
0390 //version
5441 //type
0000000000000000 //手数料
FB1B5A1017000000 //有効期限
90758EB47C28D6143BAA3DE6A8D9C319B503A1BFD8E789E9E2 //宛先アドレス(decoded)
0C00 //メッセージ長
0200 //モザイク長
48656C6C6F204E656D3221 //メッセージ
44B262C46CEABB850000000000000000 //モザイク1
1C29E1B7B29912940000000000000000 //モザイク2
宛先アドレスに注意してください。MIJIN_TESTなのにSで始まる文字列ではありません。Sで始まるアドレスはエンコードされたもので、ここで指定するのはデコード状態のアドレスとなります。
次に署名を行います。
const signedTx = signTransaction(keyPair,serializedTx);
内部での処理は以下の通りです。
function signTransaction(keyPair,byteBuffer) {
const signingBytes = byteBuffer.slice(4 + 64 + 32);
const keyPairEncoded = KeyPair.createKeyPairFromPrivateKeyString(convert.uint8ToHex(keyPair.privateKey));
const signature = Array.from(catapult.crypto.sign( new Uint8Array(signingBytes),keyPair.publicKey, keyPair.privateKey,catapult.hash.createHasher()));
const signedTransactionBuffer = byteBuffer
.splice(0, 4)
.concat(signature)
.concat(Array.from(keyPairEncoded.publicKey))
.concat(byteBuffer.splice(64 + 32, byteBuffer.length));
const payload = convert.uint8ToHex(signedTransactionBuffer);
return {
payload,
hash: createTransactionHash(payload)
};
}
function createTransactionHash(transactionPayload) {
const byteBuffer = Array.from(convert.hexToUint8(transactionPayload));
const signingBytes = byteBuffer
.slice(4, 36)
.concat(byteBuffer.slice(4 + 64, 4 + 64 + 32))
.concat(byteBuffer.splice(4 + 64 + 32, byteBuffer.length));
const hash = new Uint8Array(32);
sha3Hasher.func(hash, signingBytes, 32);
return convert.uint8ToHex(hash);
}
これで署名完了です。
最後にCatapultネットワークにアナウンスします。
putNemInfo("/transaction/",signedTx.payload)
.then(function(res){
intervalCheckUnconfirmed = setInterval(function(){
getNemInfo("/transaction/" + signedTx.hash + "/status")
.then(function(res2){
parseTransactionStatus(res2);
})
}
,3000
);
});
これは従来のXEMBook-sdkがそのまま使えます。XEMBook-sdkはAPIのエンドポイントをむき出しで使います。そのためエンドポイントの数以上のことを覚える必要はありません。
先ほど署名で取得したsignedTxは署名本体のpayloadと確認用のhash値の2つのパラメータを持ちます。
payloadのみをAPIに指定してHTTPのPUTで投げます。
ノードに届いているとレスポンスには以下のようなメッセージが入って返されます。
packet 9 was pushed to the network via /transaction
ノードにどのように受け入れられたかはhash値を指定してGETで状態を確認します。
subscribeするとややこしくなるので、今回はポーリングを使用しています。
ここでセキュリティ的な注意点。有名なWebでのハッキング手法として送信先アドレスを署名直前にすり替えられるというものがあるので、署名後にシリアライズされた文字列からアドレスを抽出し送信直前の最終確認とするとよいでしょう。(署名後にアドレスを改ざんすることはできません)
これで /transaction/{hash}/state の返事が "Success"になれば通知完了です。
以下のようなメッセージが帰ってくる場合はトランザクションのシリアライズに失敗している可能性が高いです。
{"code":"ResourceNotFound","message":"no resource exists with id 'hash"}
少しこんがらがりましたね。整理します。
transaction/{hash}/status
transaction/{hash}
transaction/{hash}/status
は承認されていないトランザクションの状態もすべて監視可能です。なので最初はこれでネットワークに受け入れられたかどうかの確認ができます。シリアライズに失敗している場合はSuccessが返らずに"no resource exists with id 'hash'"が返ってきます。その後 "Confirmed"が確認できれば transaction/{hash}
でトランザクションの詳細を参照しに行く、という流れになります。
最後に動くもの、置いておきます。
http://xembook.net/xembook-sdk/snapshot/20190526/examples/220_transfer.html
実際に刻んだトランザクションを確認できるTransaction Viewerも準備しています。
説明は次回に。