NEM Libraryを使ってみる(3) ウォレットの管理

NEM Libraryにはウォレットを管理するクラスも用意されています。

Walletの生成

秘密鍵からアドレスを生成する場合はWalletクラスを継承したSimpleWalletクラスを利用します。

let password: Password = new Password("passwordstr");
let privateKey: string = "privatekeyxxxxxxxxxxxxxxxxxxxxxxxx";

let simpleWallet: SimpleWallet = SimpleWallet.createWithPrivateKey("nextem test", password, privateKey);

生成されたSimpleWalletは以下のようなオブジェクトになり、秘密鍵は暗号化されています。

{
    "name":"nextem test",
    "network":104,"
    address": {
        "value":"ND6VJMWYX7CZRPLYXX566DP2Z6O7ZKYG745EV2AX",
        "networkType":104
    },
    "creationDate":"2017-08-26T11:29:37.729",
    "schema":1,
    "encryptedPrivateKey": {
        "encryptedKey":"encryptedxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "iv":"ivxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
}

ちなみに秘密鍵をランダム生成するには、SimpleWalletクラスのcreateメソッドを利用します。

let simpleWallet: SimpleWallet = SimpleWallet.create("random wallet", password);

Walletの利用

公開鍵や秘密鍵を利用するには、openメソッドでAccountクラスを作成して利用します。 秘密鍵はprivateになっているので、秘密鍵を直接操作するのではなく、戻ってくるAccountクラスの各メソッドを利用してトランザクションへの署名や、メッセージの暗号化、復号化を実施します。

let account: Account = simpleWallet.open(password);
account.signTransaction(transaction)

バックアップ

WalletのバックアップはwriteWLTFileメソッドを利用します。 stringが返ってくるので適当に保存します。

let mimetype = 'application/octet-stream';
let url = window.URL.createObjectURL(new Blob([simpleWallet.writeWLTFile()], { 'type': mimetype }));
let a = document.createElement('a');

a.target = '_blank';
a.download = simpleWallet.name + ".wlt";
a.href = url;
a.click();

バックアップからの復元

wltファイルからWalletを復元するには、wltファイルの文字列を抽出した上でreadFromWLTメソッドを利用します。

なお、NanoWalletから出力したwltファイルは形式が異なるようで読み込めませんでした。

let simpleWallet = SimpleWallet.readFromWLT("wltstrxxxxxxxxxxxxxxxxxxxxxxxxxxxx");

このあたり自前で実装するのも面倒なので、WEBサイトなどに導入する際には地味に便利なのではないでしょうか。


※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD

NEM Libraryを使ってみる(2) トランスファートランザクション

次にxemや各種モザイクの送信方法についてです。

xemの送信

xemを送信するには、TransactionHttpクラスのannounceTransactionメソッドを利用します。

送信する署名済トランザクションを作成するには、TransferTransactionクラスのcreateメソッドでトランザクションを作成し、AccountクラスのsignTransactionメソッドで署名します。

let account = Account.createWithPrivateKey("privatexxxxxxxxxxxxx");

let tx = TransferTransaction.create(
    TimeWindow.createWithDeadline(),
    new Address("NCS5BI-MFLIOP-5TMLKM-INCN5C-4PQ5VK-YXK7BB-VGYX"),
    new XEM(0.1),
    EmptyMessage
  );
let signedTransaction: SignedTransaction = account.signTransaction(tx);

let transactionHttp = new TransactionHttp();
transactionHttp.announceTransaction(signedTransaction)
  .subscribe( x => console.log(x));

SUCCESSが返ってきました。

{
  "type":1,
  "code":1,
  "message":"SUCCESS",
  "transactionHash":{
    "data":"28b15df7e2a28e215385ce50ba2e7efb9efe0943db8f585576e071638bd8aa28"
  },
  "innerTransactionHash":{}
}

モザイクの送信

モザイクを送信するには、MosaicHttpクラスのgetMosaicTransferableWithAmountMosaicTransferableオブジェクトを作成し、配列にまとめてからTransferTransactionクラスのcreateWithMosaicsメソッドでトランザクションを作成します。 getMosaicTransferableWithAmountにxemを喰わせるとエラーになったので、個別に配列に追加しました。

ちなみにquantityを1にした場合、divisibilityがいくつであってもしっかりと1送られます。何気にハマるポイントなので気にしなくていいのはありがたいです。

Observable.from([
  {mosaic: new MosaicId("nextem", "nex"), quantity: 1},
  {mosaic: new MosaicId("nextem.ex", "photon"), quantity: 1},
  {mosaic: new MosaicId("nextem.ex", "higgs"), quantity: 1}
]).flatMap(mosaicWithAmount => mosaicHttp.getMosaicTransferableWithAmount(
    mosaicWithAmount.mosaic,
    mosaicWithAmount.quantity
  ))
  .toArray()
  .map(mosaics => {
    mosaics.unshift(new XEM(1));
    return mosaics;
  })
  .map(mosaics => TransferTransaction.createWithMosaics(
      TimeWindow.createWithDeadline(),
      new Address("NCS5BI-MFLIOP-5TMLKM-INCN5C-4PQ5VK-YXK7BB-VGYX"),
      mosaics,
      EmptyMessage
    )
  )
  .map(transaction => account.signTransaction(transaction))
  .flatMap(signedTransaction => transactionHttp.announceTransaction(signedTransaction))
  .subscribe(nemAnnounceResult => {
      console.log(nemAnnounceResult);
  });

残高の確認と送受信。これでもうほとんどのことは出来てしまいます。しかも容易に。

feeも安くなってネームスペースを初めて取ったなんて人も少しずつ出てきているようですし、さらに一歩進めて何か作ってみるというのはいかがでしょうか。


※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD

NEM Libraryを使ってみる(1) 初期設定と各種アカウント情報

以前試してみたNEM-sdkよりも、明らかに使いやすそうなNEM LibraryをAngularで動かしてみます。

TypeScriptできれいに書かれていて、RxJSが使われているのでAngularとも相性が良く、モダンJavaScriptプログラミングのいい教材になると思います。

また、NISからは配列で返ってきて操作しにくいモザイクのpropertiesがちゃんとプロパティになっていたり、ややこしいことで有名なdivisibility周辺も勝手にうまいことやってくれるので、NISの変な癖みたいな部分をきれいに吸収してくれています。

さらにはMultisigAggregateModificationTransactionなどにも対応しているので、機能的にも十分な機能を備えています。

初期設定

npmでパッケージをインストールします。

$ npm install nem-library --save

NEM Libraryはメインネットでもテストネットでも使えるので、app.module.tsあたりでどちらか指定しておきます。

app.module.ts

import { NEMLibrary, NetworkTypes } from "nem-library";

NEMLibrary.bootstrap(NetworkTypes.MAIN_NET);

ちなみに自分の環境(@angular/cli: 1.3.1)では以下のエラーが出ました。

In ambi ent enum declarations member initializer must be constant expression.

これはpackage.jsonにあるdevDependencies部分のtypescriptのバージョンを最新にしてnpm installし直したところ解決しました。 Angular CLIを使って普通にプロジェクト生成した場合のデフォルトのバージョンが2.3.3でしたが、2.4以降が必要なんだと思います。

NISへの接続

NEM LibraryでNISへアクセスする場合、用途に応じてAccountHttpTransactionHttpMosaicHttpなどのHttpEndpointを継承したクラスを利用します。

HttpEndpointを継承したクラスはデフォルトでConnection Poolを使用しているので、引数無しで呼べば接続に成功するまで自動的にNISノードのリストに次々接続していきます。 (NanoWalletもこのような仕組みにすれば、開いたら残高がゼロなんです!!みたいな質問もなくなっていいんじゃないかと思うんですがどうなんでしょう。)

Account情報

AccountHttpを利用するとアカウントの様々な情報を参照出来ます。

xemの残高や公開鍵などの情報を参照するには、getFromAddressメソッドを利用します。 ちなみに一度もトランザクションを発行していないアドレスだと、Not a valid public keyのエラーが出ます。

let accountHttp: AccountHttp = new AccountHttp();
let address: Address = new Address("NDLHY5-KMQTAT-AR7IBR-BF32MA-QWDK73-33VNI2-MD5W");

accountHttp.getFromAddress(address)
  .subscribe(accountInfoWithMetaData => {
    console.log("getFromAddress", accountInfoWithMetaData);
  });

以下のようなAccountInfoWithMetaDataオブジェクトが返ります。

{
  "balance":{
      "balance":32799996,
      "vestedBalance":32796162,
      "unvestedBalance":3834
    },
  "importance":0,
  "publicAccount":{
    "address":{
      "value":"NDLHY5KMQTATAR7IBRBF32MAQWDK7333VNI2MD5W",
      "networkType":104
    },
    "publicKey":"099132a49ed0c15936a464cf6ef43120f01fa88835803593571882feea6161db"
  },
  "harvestedBlocks":0,
  "status":"LOCKED",
  "remoteStatus":"INACTIVE",
  "cosignatoryOf":[],
  "cosignatories":[]
}

パブリックキーからAccountInfoWithMetaDataを参照するには、getFromPublicKeyメソッドを利用します、

let publicKey: string = "099132a49ed0c15936a464cf6ef43120f01fa88835803593571882feea6161db";

accountHttp.getFromPublicKey(publicKey)
  .subscribe(accountInfoWithMetaData => {
    console.log("getFromPublicKey", accountInfoWithMetaData);
  });

モザイク情報

モザイクの情報を取得するにはMosaicHttpを利用します。

AccountHttpgetMosaicOwnedByAddressで所有モザイクの一覧を取得し、MosaicHttpgetMosaicDefinitionメソッドでそれぞれのモザイクの定義情報を取得します。 xemはgetMosaicDefinitionでエラーになったのでnemネームスペースでフィルターしました。 ※中の人のアドバイスを受けて、RxJSを全力で活用したいい感じのコードに修正しました。

accountHttp.getMosaicOwnedByAddress(address)
    .flatMap(_ => _)
    .filter(mosaic => mosaic.mosaicId.namespaceId !== "nem")
    .flatMap(mosaic => {
      return mosaicHttp.getMosaicDefinition(mosaic.mosaicId)
        .map(mosaicDefinition => <any>{
          mosaicOwnedByTheUser: mosaic,
          mosaicDefinition: mosaicDefinition
        })
    })
    .subscribe(mosaicInformation => {
      console.log(mosaicInformation.mosaicDefinition.id, mosaicInformation.mosaicDefinition);
      console.log(mosaicInformation.mosaicDefinition.id, mosaicInformation.mosaicOwnedByTheUser.quantity / (10 ^ mosaicInformation.mosaicDefinition.properties.divisibility));
    });

MosaicDefinitionは以下のようなオブジェクトが返ります。

{
  "creator":{
    "address":{"value":"NDY4RHUZ3CZOZ53O5HNEXTEM7UF5X3MMDGH4IMAD","networkType":104},
    "publicKey":"506e25d76aba0bc36dcf28127626a61f1d49ed45352f64fafaa72243c3e9e0ba"
  },
  "id":{"namespaceId":"nextem.ex","name":"photon"},
  "description":"Let there be light.",
  "properties":{
    "initialSupply":1000000,
    "supplyMutable":true,
    "transferable":false,
    "divisibility":0
  },
  "metaId":75
}

アドレスが所有するモザイクの定義と残高を一度に取得するにはAccountOwnedMosaicsServiceを利用します。

let accountOwnedMosaics = new AccountOwnedMosaicsService(new AccountHttp(), new MosaicHttp());
accountOwnedMosaics.fromAddress(address)
  .subscribe(mosaics => {
    console.log("accountOwnedMosaics", mosaics);
  });

以下のようなMosaicTransferableオブジェクト配列が返ります。

[
  {
    "mosaicId":{"namespaceId":"nem","name":"xem"},
    "properties":{
      "initialSupply":8999999999,
      "supplyMutable":false,
      "transferable":true,
      "divisibility":6
    },
    "amount":32.799996
  },
  {
    "mosaicId":{"namespaceId":"nextem.ex","name":"higgs"},
    "properties":{
      "initialSupply":1000000,
      "supplyMutable":true,
      "transferable":true,
      "divisibility":6
    },
    "levy":{
      "type":2,
      "recipient":{
        "value":"NDY4RHUZ3CZOZ53O5HNEXTEM7UF5X3MMDGH4IMAD",
        "networkType":104
      },
      "mosaicId":{"namespaceId":"nem","name":"xem"},
      "fee":10000
    },
    "amount":95.999999
  }
]

トランザクション

トランザクションを参照するには、allTransactionsincomingTransactionsunconfirmedTransactionsなどのメソッドを利用します。 pageSizeを指定しないとデフォルトでは10になります。最小5、最大100です。

accountHttp.allTransactions(address, undefined, undefined, 100)
  .subscribe(transaction => {
    console.log("allTransactions", transaction);
  });

以下のようなTransactionオブジェクト配列が返ります。

[
  {
    "type":257,
    "version":1744830466,
    "timeWindow":{
      "deadline":"2017-08-26T23:49:09",
      "timeStamp":"2017-08-26T21:49:09"
    },
    "signature":"f5d861c62a2abd11a1517911a74123a4bf5a9ec749500bd0f14dcd8059ff3ec5814025969ddf3d79fbd0cbbec81d2f9519bf5c96fcccf76155c67450c86ee20b",
    "signer":{
      "address":{
        "value":"NDLHY5KMQTATAR7IBRBF32MAQWDK7333VNI2MD5W",
        "networkType":104
      },
      "publicKey":"099132a49ed0c15936a464cf6ef43120f01fa88835803593571882feea6161db"
    },
    "transactionInfo":{
      "height":1257790,
      "id":1006473,
      "hash":{
        "data":"39d46a2fcbea6fed97518e0e77fae424c6dc5200bfa1169a76110ba0c84269ce"
      }
    },
    "fee":200000,
    "recipient":{
      "value":"NCS5BIMFLIOP5TMLKMINCN5C4PQ5VKYXK7BBVGYX",
      "networkType":104
    },
    "amount":1000000,
    "message":{
      "payload":""
    },
    "mosaics":[
      {"mosaicId":{"namespaceId":"nem","name":"xem"},"quantity":1000000},
      {"mosaicId":{"namespaceId":"nextem.ex","name":"higgs"},"quantity":1000000},
      {"mosaicId":{"namespaceId":"nextem.ex","name":"photon"},"quantity":1},
      {"mosaicId":{"namespaceId":"nextem","name":"nex"},"quantity":1}
    ]
  }
]

もうこれ完全に暗号通貨の知識いらないですね。


※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD

NEMのマルチシグを使ったトラストレスな取引

そもそもの仕様を理解していなかったようで、コメントで頂いた指摘を受け修正しました。 (2of2のマルチシグ ⇒ 2of3のマルチシグ)

やっぱりしっかり検証してないとダメですね。(まだしてない


今回は、前々から色々考えて実験したりしていたことと、夜中に突然目覚めて関連することで思いついたことがあったので併せて残しておきます。

真夜中に目覚めて初めに考えたことがマルチシグの活用だなんて、我ながら消耗していますね。

ちなみに、結論から言っておくとあんまり価値のない話なんですが、考えていると結構面白かったので。

まず、Catapultのホワイトペーパーに以下のようなことが実現出来ると書いてありました。

ブロックチェーン上でアセットを取引するための、組み込み型の預託(エスクロー)サービス。トランザクション型コントラクト。

これがあればDEXも簡単に実装出来そうですし、待ち遠しい限りです。

取引所があるだけで、今までただ投げ合っていた純粋に無価値なトークンが、ほぼ無価値なトークンに格上げされます。 まぁほぼ無価値なことに変わりはありませんが、ゼロかゼロでないかは大きな差ですね。

ただ、実際にCatapultがNEMにまでフィードバックされるのはいつでしょう?夏なのか、秋なのか、冬なのか、または。。 まぁいつになるのか分かりませんので、それまでのつなぎのサービスがあっても良いと思います。

ということで、Mosaic交換の肝になる資金のエスクローについて、現状の標準機能でどこまで実装できるのかについて考えてみました。

資金のエスクロー

NEMの標準機能であるマルチシグアドレスを使って実現します。

以下をそれぞれ交換するとします。

  • アドレスA nem:xem
  • アドレスB nextem:nex

資金の移動用に以下アドレスを利用します。

  • アドレスC
  • アドレスD
  • アドレスE ダミーアドレス

リファンド用トランザクションの作成

以下のトランザクションをtimestampを1時間後など未来の時間にして作成します。

  • 「アドレスCからアドレスAへnem:xemを送金」をラップしたマルチシグトランザクションにアドレスBの秘密鍵を使って署名。
  • 「アドレスCからアドレスBへnextem:nexを送金」をラップしたマルチシグトランザクションにアドレスAの秘密鍵を使って署名。

それぞれの署名済みトランザクションをブロードキャストせずにアドレスAとアドレスBで交換します。 交換が終わったら、それぞれ署名に問題ないか事前に検証しておきます。

エスクロー用アドレスの作成

アドレスCを2of2マルチシグアドレス(連署人がアドレスAとアドレスB)に変換します。

以下の指摘の通り、連署人を勝手に外せる2of2ではダメみたいです。

id:mizunashi_rin 2017-06-26 13:41:50 NEMのMultisignature aggregate modification transactionの仕様で、N-of-Nのmultisig構成を組んでいる場合は、N-1の署名(つまりIsserのみ)で連署名者を削除できます。 この様な仕様を盛り込んだ形で、再度事例を具現化した記事を読みたいです♪ feeに関しては、1ヶ月後には1/20になっていると思われます。

誰も秘密鍵を知らないことが既知のダミーアドレスEを連署人に加えた2of3にすることで、連署人の追加削除を禁止します。

アドレスCを2of3マルチシグアドレス(連署人がアドレスAとアドレスBとダミーアドレスE)に変換します。

アドレスCへそれぞれ交換したいMosaicと必要なfeeを入金します。

2of3なのでもう資金はどちらかが勝手に動かすことは出来ません。

また、片方が入金しなかったような場合も、交換した署名済みのトランザクションがあるので、1時間後には自分の資金をリファンドすることが出来ます。

交換処理用アドレスの作成

BTCのようにひとつのトランザクションで同時に複数のアドレス宛に送金出来れば簡単なのですが、NEMではそれぞれへの送金を一つのトランザクションにまとめることが出来ないので、もうワンクッション必要になります。

複数アドレス宛には送れませんが、複数Mosaicを一つのアドレスに送ることは可能なので、それを利用します。

以下のトランザクションを作成します。

  • 「アドレスDからアドレスAへnextem:nexを送金」をラップしたマルチシグトランザクションにアドレスBの秘密鍵を使って署名。
  • 「アドレスDからアドレスBへnem:xemを送金」をラップしたマルチシグトランザクションにアドレスAの秘密鍵を使って署名。

それぞれの署名済みトランザクションをブロードキャストせずにアドレスAとアドレスBで交換します。 交換が終わったら、それぞれ署名に問題ないか事前に検証しておきます。

アドレスDを2of3マルチシグアドレス(連署人がアドレスAとアドレスBとダミーアドレスE)に変換します。

アドレスDへアドレスCからそれぞれのMosaicを同時に送金します。

ブロードキャストせずに交換した署名済みのトランザクションを使って、交換を完了させます。

トラストレス!

問題点と結論

NEMの標準機能だけを使ってDEXを作ろうと思うと、上記署名の交換などをmessageでやり取りすることになると思うので、正直現実的ではないレベルでfeeが必要になります。

messageをシグナリングサーバー代わりにして、WebRTCとかで接続すればP2Pでやり取り出来るのかもしれませんが、なんだか複雑になりそうです。

また、マルチシグアドレスを使うので、これまたfeeが高くなります。2of2マルチシグアドレスを二つ作成するだけで56xemかかり、なんとこれ今千円以上なんですね。。

やはりこれも現実的ではないです。

ということで結論ですが、下手にDecentralizedとか言わずに、素直にWEBサービスを立ち上げればいいんじゃないか、とw

今なんとなく想定しているのは、資金のロックすらせず、取引が成立したら送金トランザクションの署名を集めて、揃ったら残高だけ確認して問題なければブロードキャストするというような、ゆるーいサービスです。

需要あるかな?


※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD

NEM-sdkを使ってみる WebSocket編

少しバタバタしていたので間が空いてしまいましたが、引き続き以下のNEM-sdkを使ってみます。

github.com

今回はsdkを利用してWebSocketのAPIと接続してみます。

コネクターオブジェクトの作成

まずendpointaddressを渡してconnectorオブジェクトを作成します。

let endpoint = nem.model.objects.create("endpoint")
  (nem.model.nodes.defaultMainnet, nem.model.nodes.websocketPort);
let address = "NDLHY5KMQTATAR7IBRBF32MAQWDK7333VNI2MD5W";
let connector = nem.com.websockets.connector.create(endpoint, address);

NISへの接続

次にconnectorオブジェクトのconnectを呼んでNISへ接続し、返されたPromiseのコールバック関数でチャネルの購読処理や、リクエストを行います。

subscribe

チャネルの購読処理にはnem.com.websockets.subscribeを利用します。

  • errors

 文字通りなんだとは思いますが、今のところ何か流れてきたことがありません。

  • chain.height

 現在のブロック高を返します。

  • chain.blocks

 最新のブロックの情報と含まれるトランザクションの配列を返します。

  • account.data

 アカウントの残高やパブリックキーなどの情報を返します。

  • account.transactions.recent

 該当アドレスの最新25個の承認済トランザクション情報を返します。

  • account.transactions.unconfirmed

 該当アドレスに関する未承認トランザクションを返します。

  • account.transactions.confirmed

 未承認トランザクションが承認されたタイミングで、そのトランザクションを返します。

  • account.mosaics.definitions

 該当アドレスが所有するモザイクの定義を返します。xemも含まれます。配列ではなくモザイクごとに別々にオブジェクトを返します。

  • account.mosaics.owned

 該当アドレスのモザイク残高を返します。xemも含まれます。配列ではなくモザイクごとに別々にオブジェクトを返します。

  • account.namespaces.owned

 該当アドレスがオーナーのnamespaceを返します。

requests

こちらからデータを要求する場合は、nem.com.websockets.requestsを利用します。

  • account.data
  • account.transactions.recent
  • account.mosaics.definitions
  • account.mosaics.owned
  • account.namespaces.owned

それぞれ該当のチャネルを購読していれば、そこにデータが流れてきます。

なお、account.transactions.recentをリクエストすると、承認済トランザクションと共に未承認トランザクションも返ってきます。逆に、初回起動時にaccount.transactions.recentをリクエストしていない場合、account.transactions.unconfirmedを購読していても、すでにブロードキャスト済で未承認なトランザクションを把握出来ません。

コード

そのまま書くと関数名がいちいち長くて見づらいですが、コードは以下のようになります。

connector.connect().then(() => {
  console.log("Connected");

  nem.com.websockets.subscribe.errors(connector, res => console.log("errors", res));
  nem.com.websockets.subscribe.chain.blocks(connector, res => console.log("blocks", res));
  nem.com.websockets.subscribe.chain.height(connector, res => console.log("height", res));
  nem.com.websockets.subscribe.account.data(connector, res => console.log("data", res));
  nem.com.websockets.subscribe.account.transactions.recent(connector,  res => console.log("recent", res));
  nem.com.websockets.subscribe.account.transactions.unconfirmed(connector, res => console.log("unconfirmed", res));
  nem.com.websockets.subscribe.account.transactions.confirmed(connector, res => console.log("confirmed", res));
  nem.com.websockets.subscribe.account.mosaics.definitions(connector, res => console.log("definitions", res));
  nem.com.websockets.subscribe.account.mosaics.owned(connector, res => console.log("owned", res));
  nem.com.websockets.subscribe.account.namespaces.owned(connector, res => console.log("namespaces", res));

  nem.com.websockets.requests.account.data(connector);
  nem.com.websockets.requests.account.transactions.recent(connector);

  nem.com.websockets.requests.account.mosaics.definitions(connector);
  nem.com.websockets.requests.account.mosaics.owned(connector);
  nem.com.websockets.requests.account.namespaces.owned(connector);

}, err => console.log("errorMessage", err));

実行時の流れ

まず接続に成功するとConnectedが出力され、リクエストしたdatarecentdefinitionsowned、存在すればnamespacesが返り、ブロックが進むごとにheightblocksが返ります。

ちなみに接続に失敗すると10回繰り返してその後エラーが出力されます。

送金や着金があった場合、まずunconfirmedが返ります。 この時点ではnem.com.websockets.requests.account.dataで最新データを要求してもまだ残高は変わっていないので、未承認トランザクションを含めるとどうなるかはこのunconfirmedの結果を反映してあげる必要があるようです。

ブロックが進むとheightblocksが返り、未承認のトランザクションが承認に変わるのでconfirmedが返ります。

承認されると自分の残高が変わるので最新のdataが返ります。ゼロになった場合は残高ゼロで値が返ります。

モザイク

xem以外のモザイクに動きがあった場合はdefinitionsownedが所有モザイクの数だけそれぞれ返りますが、ゼロになった場合はゼロが返るのではなく、該当モザイクのオブジェクトは何もかえってきません。

手持ちのモザイクの一つを送信してゼロになった場合、モザイクに動きがあったので残された他のモザイクについてdefinitionsownedは返りますが、該当のモザイクについては何も返らないという事になります。

返ってきたオブジェクトを次々と配列に入れていく作りにした場合、ゼロになったタイミングを把握出来ないのでいまいち使いにくいです。NanoWalletの実装もそうなっていて、送金後ゼロになった場合も、残高は更新されません。(少なくとも1.2.12ではそうでした)

何故配列で返す仕様にしなかったんだろう??

とりあえず、承認されたトランザクションを元に自力で計算するか、confirmedのところで一旦配列をリセットし、definitionsownedを強制的にリクエストして再度配列を作り直すなど工夫が必要そうです。


これで残高、未承認トランザクションなど、リアルタイムでデータを更新可能なサービスが作れるようになりました。

今度こそチャーハンを作る準備は整った。かな?

※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD

NEM-sdkを使ってみる

NEMで何らかのサービスを作るにあたり、Node側からもAngular側からも同じように使えるライブラリを作ろうと思っていたのですが、NanoWalletとかにガンガンコミットしている方が作ったNEM-sdkというのが既にあるらしいので使ってみます。

github.com

インストー

普通にnpmモジュールになっているので、npm installでインストールします。

Angular側から呼んだ際にajvでエラーが出たので、ajvを最新バージョンでインストールしなおしておきます。 (Nodeから直接実行するときは特にエラー出なかったので、Angular側の問題かもしれません。)

$ npm install nem-sdk --save
$ npm install ajv --save

オブジェクト

オブジェクトの種類は主に以下で、必要に応じて組み合わせて使います。

  • common パスワード、秘密鍵
  • endpoint SNのホスト名、ポート番号
  • mosaicAttachment 送金するモザイク
  • mosaicDefinitionMetaDataPair モザイクの基本情報
  • invoice 請求書用?
  • transferTransaction 送金トランザクション
  • signatureTransaction マルチシグの署名トランザクション

空のオブジェクトを作成するには、nem.model.objects.getを利用します。

let transferTx = nem.model.objects.get("transferTransaction");

以下のようなオブジェクトが生成されます。

{
  "amount":0,
  "recipient":"",
  "recipientPublicKey":"",
  "isMultisig":false,
  "multisigAccount":"",
  "message":"",
  "isEncrypted":false,
  "mosaics":[]
}

パラメータのセットされたオブジェクトを生成するには、nem.model.objects.createを利用します。 この関数は関数を返すのでその関数に引数を渡します。

let transferTx = nem.model.objects.create("transferTransaction")("NDY4RHUZ3CZOZ53O5HNEXTEM7UF5X3MMDGH4IMAD", 1, "soon");

以下のようなオブジェクトが生成されます。このままではまだ使えません。

{
  "amount":1,
  "recipient":"NDY4RHUZ3CZOZ53O5HNEXTEM7UF5X3MMDGH4IMAD",
  "recipientPublicKey":"",
  "isMultisig":false,
  "multisigAccount":"",
  "message":"soon",
  "isEncrypted":false,
  "mosaics":[]
}

トランザクション作成

先ほどのトランザクショントランザクションタイプやタイムスタンプなどを付加するには、nem.model.transactions.prepareを利用します。 送信サイドの情報が必要なので、先にcommonオブジェクトを作成します。(privateKey部分は生の秘密鍵文字列。)

let common = nem.model.objects.create("common")("", "privateKey");
let transactionEntity = nem.model.transactions.prepare("transferTransaction")(common, transferTx, nem.model.network.data.mainnet.id);

必要情報が付加されました。

{
  "type":257,
  "version":-1744830463,
  "signer":"099132a49ed0c15936a464cf6ef43120f01fa88835803593571882feea6161db",
  "timeStamp":68485327,
  "deadline":68488927,
  "recipient":"NDY4RHUZ3CZOZ53O5HNEXTEM7UF5X3MMDGH4IMAD",
  "amount":1000000,
  "fee":2000000,
  "message":{"type":1,"payload":"736f6f6e"},
  "mosaics":null
}

送信

トランザクションの送信には、nem.model.transactions.sendを利用します。実際にネットワークへ送信するので、endpointオブジェクトを作成しておきます。

実行するとnem.model.transactions.prepareで準備したトランザクションシリアライズして署名し、ネットワークへ送信します。

let endpoint = nem.model.objects.create("endpoint")(nem.model.nodes.defaultMainnet, nem.model.nodes.defaultPort);
nem.model.transactions.send(common, transactionEntity, endpoint)
  .then(res => console.log(JSON.stringify(res)));

以下の通りSUCCESSが返りました。

{
  "innerTransactionHash":{},
  "code":1,
  "type":1,
  "message":"SUCCESS",
  "transactionHash":{
    "data":"2b1b0b95ec2f6181991e1f71c4501b56b51b9f2841f4840602590e867b480693"
  }
}

 

ということで、このライブラリを使うと本当に手軽にXEMやモザイクの送信が可能です。当然、Node側からもHTML側からも同じように呼べますし、モザイクを使ったWEBサービスやゲームなども簡単に実装出来そうです。

また、今後このライブラリがNanoWalletに統合されるようなことも書いてあるので、どうやらもう車輪から自作する必要はなく、すぐに作りたいものを作るフェーズから開始すれば良さそうです。

よし、何か作ろう。(soon)


※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD

NEM NanoWalletのServicesをnpmモジュール化する

前回に続いて、NEM NanoWalletの中身をnpmモジュール化していきます。

tadajam.hateblo.jp

前回はUtilsをnpmモジュール化したので、今回はそのモジュールを利用した各種Servicesの移植に進みます。

Services

まずはServicesの中身を整理します。

github.com

  • Alert alert.service.js

 エラーメッセージに関するサービス。

  • Connector connector.service.js

 SockJSを利用してWebSocket APIに接続するサービス。

  • DataBridge dataBridge.service.js

 NetworkRequestsやConnectorを利用してデータを取得、保持するサービス。

  • NetworkRequests networkRequests.service.js

 各種APIへ接続するサービス。

  • Transactions transactions.service.js

 各種トランザクションを作成するサービス。

  • Wallet wallet.service.js

 カレントアドレスと接続ノードなどを設定するサービス。

  • WalletBuilder walletBuilder.service.js

 アドレスを生成するサービス。

WebSocketでリアルタイムにデータを取得したいのであれば、ConnectorDataBridgeが必要になりますが、トランザクションを生成してブロードキャストするだけなら、NetworkRequestsTransactionsがあれば事足りるので、今回はこの二つのファイルを対象とします。

対象のファイル2ファイルは、上記のDataBridgeWalletを参照しています。

DataBridgeについては、NISとの時刻の同期を行っている部分を参照しているだけなので、今回は時刻同期無しでDataBridgeは無視します。

Walletについては、今回は定数としてしか使わないので、定数を保持するだけのクラスとして実装します。本来はWalletインスタンスをコンストラクタで受ける形になるかと思います。

また、以下のconfigフォルダにあるAppConstantsも参照しています。

github.com

AppConstantsはデフォルトのポート番号や、有効な言語の一覧などが入っている設定ファイルなので、スタティックな変数だけ持つクラスとして実装します。

Typescriptで実装する

せっかくなのでTypescriptで書き換えていきます。(と言っても片っ端からanyで型指定していくことに何の意味があるのかは知りませんがw)

まずは、TypeScriptのファイルと、生成されるJavaScriptのファイルをそれぞれ管理する必要があるので、少し環境を整えます。

srcがソース置き場、libが実体置き場ということにしてディレクトリを分けます。

npmには実体だけアップしたいので、.npmignoreに除外するsrcフォルダを指定します。

.npmignore

src/

また、生成されたJavaScriptのファイルをgitで管理したくないので、.gitignoreに除外するlibフォルダを指定します。

.gitignore

lib/

srcディレクトリに移動してtsc--initオプションでtsconfig.jsonを生成し、outDirで出力先を設定します。

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
    "noImplicitAny": false,
    "sourceMap": false,
    "outDir": "../lib"
  }
}

最終的なディレクトリ構成は以下のようになります。

nem-services
│  .gitignore
│  .npmignore
│  LICENSE
│  package.json
│  README.md
│
├─lib
│      appConstants.js
│      index.js
│      networkRequests.js
│      transactions.js
│      wallet.js
│
└─src
        appConstants.ts
        index.ts
        networkRequests.ts
        transactions.ts
        tsconfig.json
        wallet.ts

それぞれのサービスの実装

あとはそれぞれのファイルの、元のコードのままでは動かない部分について修正しつつ、狂ったように型指定していきます。

基本的にはそのままですが、AngularJSのhttp部分をisomorphic-fetchで書き換えました。 fetchした時に帰ってくるPromiseObservableにしていじくりまわしたのでrxjsも必要になりました。 なぜここでRxJSかというと、それってモダンじゃね?ぐらいの感覚で良く分かってませんw

package.json

"dependencies": {
  "rxjs": "^5.1.0",
  "isomorphic-fetch": "^2.2.1",
  "nem-utils": "0.0.3"
}

NetworkRequests

httpアクセス部分は以下のような感じです。 正直、Observableの使い方、使いどころが正しいのか良く分かりませんが、動いたので正しいということにしておきます。

import * as fetch from "isomorphic-fetch";
import { Observable } from 'rxjs';
import 'rxjs/add/operator/map';

getHeight(host: string): Observable<number> {
  let url: string = "http://" + host + ":" + this.getPort() + "/chain/height";

  return Observable.fromPromise(fetch(url))
    .flatMap(response => response.json())
    .map(json => json.height);
}

Transactions

DataBridgeが担っていた部分は、以下のように書き換えました。


// let d = new Date();
// let timeStamp = Math.floor(this._DataBridge.networkTime) + Math.floor(d.getSeconds() / 10);
let timeStamp: number = helpers.createNEMTimeStamp();

その他、jQueryを使っている部分をNativeなコードに直したり、型に関しては結構ゆるい作り(数値型っぽいところに突然エラーメッセージ配列を放り込む)になっていたので、その辺を微調整しました。

実行

以下のようにしてインポートして利用します。 プライベートキーは伏せてありますが、実際は正しいものを利用します。

import { Transactions } from "nem-services";

let common = {
  privateKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  password: ""
}
let dummyTransaction = {
  recipient: "NDLHY5KMQTATAR7IBRBF32MAQWDK7333VNI2MD5W",
  recipientPubKey: "099132a49ed0c15936a464cf6ef43120f01fa88835803593571882feea6161db",
  amount: 0,
  message: "sooooooooooooooooooooooooooooon",
  mosaics: null,
  fee: 12000000,
  innerFee: 0,
  due: 60,
  isMultisig: false,
  multisigAccount: ""
}

let tx = new Transactions();
let entity = tx.prepareTransfer(common, dummyTransaction, null);
tx.serializeAndAnnounceTransaction(entity, common)
  .subscribe(s => console.log(s));

以下のレスポンスが返ってきました。

{
  innerTransactionHash: Object,
  code: 1,
  type: 1,
  message: "SUCCESS",
  transactionHash: Object
}

そして無事トランザクションが取り込まれました。

http://chain.nem.ninja/#/transfer/aa530b1189fa5401101df71232fa0cf0f883a5d25364e552ea95d986a2c215c4


※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD