速習Symbolブロックチェーン10章、監視編です。

URLとしてはこちら。

ざっくりというと、WebSocketで接続し、事前に監視したい内容を登録しておくことで、それをトリガにトランザクション内容の通知が来るということのようです。

Symbolブロックチェーンのトランザクション内容等はREST APIを呼び出すことで取得することができます。
あれなら、新規ブロックが生成されたかも(何度も呼び出すことによって)知ることができます。

が、何度も呼び出す無駄が減る&ブロックやトランザクションが追加されたことをトリガとしてイベント処理のようにするため、存在しているんでしょう。たぶん。

というか速習SymbolをやるまでWebSocketは新規ブロック生成を通知してくるだけかと思っていて、作らずに放置していました。
よって、10章をやるにあたって追加しました。いらないと思ってた。必要そうだねこれ。

さて、早速やっていきます。

実践編。

10.1 リスナー設定、10.2 受信検知

速習Symbolにありましたが、エンドポイントのフォーマットは

wss://{node url}:3001/ws

のようです。これがsslでなければ、

ws://{node url}:3000/ws

となるんでしょうかね。さて。

また、何もなければlistenerは1分で切断されるようです。他にも切断される要件あるのかな?

コードは下記。

print('10.2受信検知');

// 接続先ホスト。
var networkHostInfo = await TransactionSender.getNetworkHost();

print('aliceがbobに送信する。');

// aliceのアカウントを作成する。
var alice = await Account.createFromPrivateKey(
  await PrivateKey.create(alicePrivateKey), NetworkTypeEnum.testnet);

// bobのアカウントを作成する。
var bob = await Account.createFromPrivateKey(
await PrivateKey.create(bobPrivateKey), NetworkTypeEnum.testnet);

// リスナー条件とデータを設定する。
var listenerInfos = [
  // 承認トランザクションの検知。
  ConfirmedListenerInfo(address: bob.address, onCallback: ((dto) => print("Confirmed: ${dto.baseMap.toString()}"))),
  // 未承認トランザクションの検知。
  UnconfirmedAddedListenerInfo(address: bob.address, onCallback: (dto) => print("UnconfirmedAdded: ${dto.baseMap.toString()}"))
];

// ソケットの生成。
var listener = WSListener(networkHostInfo.networkHost, listenerInfos: listenerInfos);

listener.open();

var tx = TransferTxInfo.create(alice.publicAccount, bob.address, 
  [Mosaic(amount: Amount(1000000), id: NamespaceId("symbol.xym").getUnresolvedMosaicId())]);

// Transaction設定を行う。
var transSetting = TransactionSetting(
  signer: alice, 
  generationHash: "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4", 
  networkType: NetworkTypeEnum.testnet, 
  deadline: Deadline.create(epochAdjustment),
  fee:FeeMultiplier(100) );     // 手数料は100%。

// トランザクションの通知。アグリゲートである必要があるらしい。         
var transactionSender = TransactionSender();

await transactionSender.sendTransaction(transSetting, tx);

listener.close();

ざっくりいえばaliceがbobに送金するトランザクションを実行します。

bobアドレスを監視し、承認または未承認トランザクションが着次第、それぞれ”Comfirmed: {内容}”、”UnconfirmedAdded: {内容}”としています。

SDKを見るとなんか成形していたかもしれません(別のところだったかも)が、そのまま内容を出力していきます。

あとは過去の通り。承認するまでループで確認を行います。

実行結果は下記。

赤枠部の通り、UnconfirmedAddedおよび、Confirmedが表示されていますね。

速習Symbolのとおり、UnconfirmedAddedではまだ承認されていないため、ブロック高(height)はゼロとなっています。
しかしConfirmedの通知では承認されているため、heightが実態を持った数になっていますね。

なお、コードの通りNamespaceIDにて送信したため、mosaicsのidがNamespaceIdのものとなっています。

10.3 ブロック監視

新たに生成されたブロックを検知します。

ブロック生成は原則30秒で行われ、WebSocketのタイムアウトは1分らしいので、実際ブロック監視さえしておけば外部要因がなければほぼ、切れないらしいですね。

print('10.3ブロック監視');

// 接続先ホスト。
var networkHostInfo = await TransactionSender.getNetworkHost();

var blockQ = 0;

// リスナー条件とデータを設定する。
var listenerInfos = [
  // block生成リスナー。
  BlockListenerInfo(onCallback: ((p0) {
    print(p0.baseMap.toString());
    blockQ++;
  })),
];

// ソケットの生成。
var listener = WSListener(networkHostInfo.networkHost, listenerInfos: listenerInfos);

listener.open();

while (blockQ < 3){
  await Future.delayed(const Duration(seconds: 10));
}

listener.close();

print('finish');

生成ブロックが3件取得されるまでコンソールに出力します。

終了し次第、finishと出力して終了とします。

結果です。

色んな情報がありますね。あんまり内容はよくわかっていないですがぱっと見、

  • previousBlockHashはひとつ前のブロックのハッシュ値のようですね。
  • transactionsHashがゼロフィルであるのは、空ブロックだから?
  • timestampがあるので、生成時刻はおそらく計算できる。
  • beneficiaryAddressがハーベストとなったアドレス?
  • signerPublicKeyがブロックを生成したアカウントの公開鍵?

という感想でした。
もしsignerPublicKey=ブロック生成のアカウント、beneficiaryAddress=ノードに設定してあるハーベスト報酬の振込先ということであれば、これを監視するだけでハーベストを行ったか検知できそうではありますね。
そうなっているかはわからないですけど。いずれ調べるかもしれません。

10.4 署名要求

署名が必要なトランザクションを検知できるようです。

print('10.4署名要求');

// 接続先ホスト。
var networkHostInfo = await TransactionSender.getNetworkHost();

// aliceのアカウントを作成する。
var alice = await Account.createFromPrivateKey(
  await PrivateKey.create(alicePrivateKey), NetworkTypeEnum.testnet);

var isFinish = false;

// リスナー条件とデータを設定する。
var listenerInfos = [
  AggregateBondedAddedListenerInfo(address: alice.address, onCallback: (value) {
    print(value.baseMap.toString());
    isFinish = true;
  }),
  // block生成リスナー。裏でトランザクション実行までにリスナーが切れないように。
  BlockListenerInfo(onCallback: ((p0) {
    print('block created.');
  })),
];

// ソケットの生成。
var listener = WSListener(networkHostInfo.networkHost, listenerInfos: listenerInfos);

listener.open();

// AggregateBondedを検知するまでループする。
while (!isFinish){
  await Future.delayed(const Duration(seconds: 1));
}

listener.close();

print('finish');

AggregateBondedを検知します。
裏側でアグリゲートボンデッドトランザクションを生成するため、(実行中に切れないようにするため)block生成の検知も入れるようにしました。
AggregateBondedを検知し次第、処理は終了します。

さて、これを実行します。

実行中です。イベントを検知するまで、特に何もすることがありません。

裏側でアグリゲートボンデッドトランザクションをアナウンスします。

以前、Takshiというアカウントを作り、その親としてaliceを登録します。
なお、実行はすでに親であるCarol1が実行します。

検知されました。

AggregateTransactionのため、transactionsがあり[]で囲まれているのがEmbeddedTransaction(Inner Transaction)になっています。
また、あくまでも未承認のため、やはりheightは0となっています。

なお、ちらっとしか見ていませんが、公式SDKでは指定した自身のアドレスだけではなく、関係するアドレス(たぶん子?)を再帰的に登録する機能もあるようですね。

10.5 現場で使えるヒント。

常時コネクション。

常時コネクションする方法がありました。

さらっと見ただけなのですが、

  • レスポンスが遅いノードは採用しない
  • /node/healthが正常でないノードは採用しない
  • /network/propertiesが解放されてないノードは採用しない
  • closeしているかどうかを監視し、死んでいたら繋ぎ直そうね

というところでした。/network/propertiesはどういうあれなんでしょう。。。
解放されてない……なんらかの理由があって閉じてしまうのか、それともそういう設定があるのか、はたまたセットアップ方法の違いか。わからないんですけれども。

未署名トランザクション自動連署。

未証明トランザクションを検知し、(必要であれば署名する条件を用意して、合致していれば)自動的に署名を行うことができる、ということですね。

やってみることとします。

print('自動署名');

// 接続先ホスト。
var networkHostInfo = await TransactionSender.getNetworkHost();

// aliceのアカウントを作成する。
var alice = await Account.createFromPrivateKey(
  await PrivateKey.create(alicePrivateKey), NetworkTypeEnum.testnet);

var isFinish = false;

TransactionInfoDTO? dto;

// リスナー条件とデータを設定する。
var listenerInfos = [
  AggregateBondedAddedListenerInfo(address: alice.address, onCallback: (value) {
    dto = value;
    isFinish = true;
  }),
  // block生成リスナー。裏でトランザクション実行までにリスナーが切れないように。
  BlockListenerInfo(onCallback: ((p0) {
    print('block created.');
  })),
];

// ソケットの生成。
var listener = WSListener(networkHostInfo.networkHost, listenerInfos: listenerInfos);

listener.open();

// AggregateBondedを検知するまでループする。
while (!isFinish){
  await Future.delayed(const Duration(seconds: 1));
}

listener.close();

print(dto!.baseMap.toString());

dto as TransactionInfoDTO;
var trans = dto!.transaction;

// リスナーから取得したTransactionInfoDTOはExpandedDTOである。
trans as AggregateTransactionExpandedDTO;

// TransactionHashにaliceが署名する。
var signature = await alice.signWithHash256(trans.transactionsHash);

// 連署情報を生成する。
var cosignature = DetachedCosignature(alice.publicKey, signature, trans.transactionsHash);

var txHttp = TransactionRoutesHttp(networkHostInfo.networkHost);

txHttp.announceCosignature(cosignature);

print('finish');

AggregateBondedTransactionを検知し次第、TransactionHashを取得し、aliceが署名→通知します。

今回はalice、carol1がtakeshiの親アカウントとなっている状態で、carol1がtakeshiからbobに送金するトランザクションを通知します。

とりあえず承認されました。

感想編。

監視はいくつもあってまだわからないものもありました。そのうち、確かめるかもしれません。

WebSocketで取得したアグリゲートボンデッドトランザクションの内容についてはAggregateTransactionExtendedDTOのほうでした。

僕の見落としな気もしますが、AggregateTransactionExtendedDTO or AggregateTransactionDTOはどっちが選ばれるのか? がぱっと見わからないですね。
(そもそもanyOfが多い。。。)

基本的に単体で取得する場合はExtendedのような気もするので、そのあたり覚えてしまえば、、、ということなのかもしれませんが。

あと忘れがちだったのが、AggregateTransactionの連署を行うhashですかね。

BasicTransactionとは違い、AggregateBondedTransactionはhashに対して署名を行うということは理解しています。
が、つい、transactionsHashに署名をしてしまう。。。

正解はmetaのhashにやる(あってるよね?)ようなのですが、いまいち、ここがなんともですね。もちろん、僕が理解しているのが浅いからというのはわかるのですが。

あとリスナーはあくまでもそのアドレスが直接関連したものを検知するようなので、
マルチシグの子アドレスで何かを実行する場合、検知するのはやっぱり子アドレスで登録しておく必要があるようですね。

以上です。お疲れさまでした。

投稿者 和泉