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

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

自分はNEMという暗号通貨が好きで、Javascriptからアドレスを生成したり、トランザクションを発行したりなど色々触って遊んでいました。

tadajam.katari.be

NEMはもともとAPIが提供されているので、たいしたスキルを必要とせずにAPIにアクセスすることで簡単に送金が出来たり、素人が何か開発しようとするには非常に敷居の低い暗号通貨です。

その流れでAngular2アプリの開発に興味を持つようになり、Node.jsを調べる内にバックエンドも含めてJavaScriptでなんでも出来てしまうということを知り、今はその辺の勉強も兼ねてNEMのモザイクの交換所でも作ろうかと思っています。

そこで、初めの一歩として、NEMのアドレスを生成したり、トランザクションを発行したり、署名を検証したりする機能を、NanoWalletから移植していきます。

NanoWallet

まずはNanoWalletの構成について整理します。

NanoWalletはNEMのAPIにアクセスすることで、自らサーバを立てることなくNEMのほとんどの機能を使えてしまう優れたアプリケーションです。 AngularJSで作成されていて、実際にトランザクションを発行したり、APIに問い合わせたりする機能の実装は、以下のservices配下にあります。

github.com

今回はその一歩手前で、各種サービスから利用されている以下utils配下にある機能を、今後自前で開発するサービスなどから簡単に呼び出せるようにしていきます。

github.com

今まで.angular-cli.jsonscriptsでそれぞれ読み込んで使っていたのですが、手順をバックエンド側と共通化したいのでnpmモジュール化してみます。

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

nem-utils
│  index.js
│  LICENSE
│  package.json
│  README.md
│
└─lib
        Address.js
        bip32.js
        convert.js
        CryptoHelpers.js
        helpers.js
        KeyPair.js
        nacl-fast.js
        Network.js
        nodes.js
        nty.js
        Serialization.js
        sinks.js
        TransactionTypes.js

リポジトリの作成

せっかくなのでGitHubリポジトリを作成しておきます。

git initで初期化し、git remote add originで事前に作成しておいたリモートリポジトリのアドレスを登録。 git pullで自動生成されたLICENSEとREADME.mdを拾ってきます。

$ mkdir nem-utils
$ cd nem-utils
$ git init
$ git remote add origin https://github.com/tadajamdev/nem-utils.git
$ git pull origin master

LICENSEとREADME.mdの中身は適当に作成します。

モジュール作成

ユーティリティ側は極力もとのコードを触らずに、また呼び出し方も同じように呼べるようにという考え方で進めます。

初めに、モジュールの設定をpackage.jsonに登録します。

package.json

{
  "name": "nem-utils",
  "version": "0.0.1",
  "description": "NEM's NanoWallet Utils",
  "main": "./index.js",
  "author": {
    "name": "tadajam",
    "email": "tadajam.dev@gmail.com"
  },
  "homepage": "https://github.com/tadajamdev/nem-utils",
  "license": "MIT",
  "keywords": [
    "nem"
  ],
  "dependencies": {
    "crypto-js": "^3.1.9-1"
  }
}

CryptoJSはNanoWalletのソースから拾うのではなく、普通にnpmでインストールします。

npm install --save crypto-js

lib配下に各jsファイルを格納して、index.jsですべてのファイルを読み込みます。 exprtsやらmodule.exportsやらイマイチ理解していないのですが、以下のようにすれば一応動きます。

index.js

exports.Address = require('./lib/Address');
exports.bip32 = require('./lib/bip32');
exports.convert = require('./lib/convert');
exports.CryptoHelpers = require('./lib/CryptoHelpers');
exports.helpers = require('./lib/helpers');
exports.KeyPair = require('./lib/KeyPair');
exports.nacl = require('./lib/nacl-fast');
exports.Network = require('./lib/Network');
exports.nodes = require('./lib/nodes');
exports.nty = require('./lib/nty');
exports.Serialization = require('./lib/Serialization');
exports.sinks = require('./lib/sinks');
exports.TransactionTypes = require('./lib/TransactionTypes');

また、このままだとエラーが出たので、各ユーティリティファイル側を少し修正します。

各ファイルのimportのところで怒られるので、requireで読み込みます。 また、併せてCryptoJSが必要なファイルでは、同じようにCryptoJSも読み込みます。

/** @module utils/Address */

// import convert from './convert';
// import Network from './Network';

let CryptoJS = require("crypto-js");
let Network = require("./Network");
let convert = require("./convert");

GitHubに公開

git addでインデックスに追加、git commitでインデックスに追加されたファイルをコミットし、git push origin masterでリモートリポジトリに反映させます。

$ git add .
$ git commit -m "[add]init"
$ git push origin master

公開されました。 github.com

npmモジュール公開

npmの公式ページから開発者として登録をします。 www.npmjs.com

以下項目を入力し、登録しました。

  • Name
  • Public Email
  • Username
  • Password

npm adduserでログインし、npm publishで公開します。

$ npm adduser
Username: tadajam
Password:
Email: (this IS public) tadajam.dev@gmail.com
Logged in as tadajam on https://registry.npmjs.org/.
$ npm publish

公開されました。 www.npmjs.com

モジュールの利用

Angularのコンポーネントから使用してみます。

まずはインストールします。

$ npm install --save nem-utils

インポートして使用します。

app.component.ts

import { Address, KeyPair, Network } from "nem-utils";

console.log(Address.toAddress(KeyPair.create("").publicKey.toString(), Network.char2Id("N")));

秘密鍵を空文字列で生成した場合のアドレス、NBONKWCOWBZYZB2I5JD3LSDBQVBYHB757WJ2KDEBが出力されました。

バックエンド側でも同じようにインポートすれば、同じように使えます。  

 

 


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

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

MEANスタック入門(6) MongoDBとの連携

今回はMongoDBのシェルからデータベースを操作する方法と、Node.jsからアクセスする方法についてまとめます。

mongoの使い方とデータベースの作成

ここからはmongoコマンドを使って、MongoDBのシェルで作業していきます。 mongoコマンドを実行すると、MongoDBのシェルに入ります。

$ mongo
データベースの準備

初めにデータベースを作成します。

useコマンドでデータベース名を指定すると、カレントが指定したデータベースに変更になります。 この段階ではsample_dbはまだ追加されず、コレクションやドキュメントを追加した段階で追加されます。

> use sample_db
switched to db sample_db

次に通常のRDBのテーブルにあたる、コレクションを作成します。

> db.createCollection("users")
{ "ok" : 1 }

show collectionsコマンドで実際に作成出来たか確認します。

> show collections
users

最後に、通常のRDBのレコードにあたる、ドキュメントを追加します。

usersコレクションに対して作業するので、dbに続けてdb.usersのようにコレクション名を指定します。 コレクションが存在しなければ、コレクションを新規で作成した上でドキュメントが追加されます。

> db.users.save({id:0, address: "address0"})
WriteResult({ "nInserted" : 1 })

findで実際にドキュメントが追加されたことを確認します。

> db.users.find()
{ "_id" : ObjectId("590dbde126111dfac549518e"), "id" : 0, "address" : "address0" }
(参考)データベースの削除

removeでドキュメントを全削除します。

> db.users.remove({})
WriteResult({ "nRemoved" : 1 })

dropでコレクションごと削除します。

> db.users.drop()
true

dropDatabaseでデータベースごと削除します。

> db.dropDatabase()
{ "dropped" : "sample_db", "ok" : 1 }

show dbsで確認すると、データベース一覧から消えています。

> show dbs
admin      0.000GB
local      0.000GB
ダミーデータの準備

MongoDBのシェルではJavaScriptの構文が使えるので、以下のようなコードを作成してシェルに貼り付けると、まとめてダミーデータが作成出来ます。

use sample_db;
for(let i = 0;i < 20; i++) {
  db.users.save({id:i, address: "address" + i});
}

findで確認します。

> db.users.find()
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf271"), "id" : 0, "address" : "address0" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf272"), "id" : 1, "address" : "address1" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf273"), "id" : 2, "address" : "address2" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf274"), "id" : 3, "address" : "address3" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf275"), "id" : 4, "address" : "address4" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf276"), "id" : 5, "address" : "address5" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf277"), "id" : 6, "address" : "address6" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf278"), "id" : 7, "address" : "address7" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf279"), "id" : 8, "address" : "address8" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf27a"), "id" : 9, "address" : "address9" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf27b"), "id" : 10, "address" : "address10" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf27c"), "id" : 11, "address" : "address11" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf27d"), "id" : 12, "address" : "address12" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf27e"), "id" : 13, "address" : "address13" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf27f"), "id" : 14, "address" : "address14" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf280"), "id" : 15, "address" : "address15" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf281"), "id" : 16, "address" : "address16" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf282"), "id" : 17, "address" : "address17" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf283"), "id" : 18, "address" : "address18" }
{ "_id" : ObjectId("590dddf0f62e1b1cfe7bf284"), "id" : 19, "address" : "address19" }

Node.jsとMongoDBの連携

今回はMongooseという、Node.jsからMongoDBを操作するためのライブラリを利用します。

まずはMongooseをインストールします。

$ npm install --save mongoose

ここからはこのMongoose経由でMongoDBにアクセスしていきます。

初めにserver直下にmodelsディレクトリを作成し、usersコレクション用のusers.model.tsを作成します。 ここに以下のようにスキーマを定義し、スキーマからモデルをコンパイルします。

users.model.ts

import * as mongoose from "mongoose";

const usersModel: mongoose.Model<mongoose.Document> = mongoose.model(
  "users",
  new mongoose.Schema({
      id : {
        type: Number
      },
      address : {
        type : String
      }
  })
);

export { usersModel };

次にMongoDBへの接続について、アプリケーションのエントリポイントであるwww.tsに追記します。

listenに成功したコールバックの中で、mongoose.connectを利用してMongoDBに接続します。 接続先はサーバのポート設定を記述したconfig.tsに外部化しておきます。

www.ts

import * as http from "http";
import { app } from "../app";
import { serverPort, mongoUri } from "../config";
import * as mongoose from "mongoose";

const port = process.env.PORT || serverPort;
app.set("port", port);

const server = http.createServer(app);

server.listen(port, () => {
  mongoose.connect(mongoUri);
});

config.ts

export const serverPort: number = 4300;
export const mongoUri: string = "mongodb://localhost/sample_db";

apiの実体であるusers.tsから実際にMongoDBへアクセスします。

先ほど作成したモデルからfindし、コレクションの全ドキュメントを取得して返します。

users.ts

import { Request, Response, Router } from "express";
import { usersModel } from "../models/users.model";

const usersRouter: Router = Router();

usersRouter.get("/", (request: Request, response: Response) => {
  usersModel.find({}, function(err, users) {
    if (err) throw err;
    response.json({users: users});
  });
});

export { usersRouter };

動作確認

これで以下のような一連の流れが完成しました。

  • Angularで構築したHTMLからExpressで構築したAPIにアクセス。
  • API側ではMongoDBのデータを取得して返答する。
  • レスポンスをAngular側で整形してテーブルを表示する。

f:id:tadajam:20170506235823p:plain

おしまい

これで全6回に渡るMEANスタック入門は完了です。 ここからさらにAngular側を掘っていくとか、Express側でRESTfulなAPIを構築していくだとか、色々とやりたいことはありますので、気が向いたらまた書きたいと思います。

最後に、記事に誤っている部分を発見した方や、もっと良い方法をご存じの方はぜひお声がけ下さい。

MEANスタック入門(5) タスクランナーの代わりにnpm-scriptsを使う

ビルドから実行までを自動化するのに、gulpやgruntなどのタスクランナー代わりにnpm-scriptsを使います。

npm-scriptsの設定

開発時

開発時はそれぞれコードの変更を監視して、自動でトランスパイルを走らせます。

Angularをng serverで実行し、Express側のトランスパイルをtscで実行します。

また、変更を検知して自動でコンパイルするために別途tsc-wオプション付きで実行し、それによりソースが書き換わった際に自動で再起動するnodemonを実行します。

コマンドは順を追うと以下の通りです。

$ ng server --proxy-config proxy.conf.json
$ tsc -p ./server
$ tsc -w -p ./server
$ nodemon dist/server/bin/www.js

上記をpackage.jsonscriptsに設定し、npm-run-allを使って並列実行します。

必要なnpm-run-allnodemonをインストールしておきます。

$ npm install --save-dev npm-run-all
$ npm install --save-dev nodemon

サーバ開始前にTypeScriptのトランスパイルを実行させたいので、run-sを使って直列につなぎます。

次にそれぞれのサーバ開始と監視開始をrun-pを使って並列に実行します。 Angular側の作業順序はng serverコマンドに任せます。

また、ついでにMongoDBの起動もここでやっておきます。

package.json

"scripts": {
    "start": "run-s tsc start:p",
    "tsc": "tsc -p ./server",
    "start:p": "run-p start:mongo watch:*",
    "start:mongo": "mongod --config \"C:/MongoDB/Server/3.4/bin/mongodb.config\"",
    "watch:ng": "ng server --proxy-config proxy.conf.json",
    "watch:tsc": "tsc -w -p ./server",
    "watch:nodemon": "nodemon dist/server/bin/www.js"
}

npm runで実行すると、上記手順が一度に実行されます。

$ npm run start
公開用

それぞれ単純にビルドします。

package.json

"scripts": {
    "build": "run-p tsc build:ng",
    "build:ng": "ng build --prod"
}

同じくnpm runで実行します。

$ npm run build

これだけでもかなり効率化されました。