AndroidでNEMのAPIを使って送金する
NEM、面白いですね。
NEM はAPI が公開されているので、誰でも気軽にブロックチェーンを使ったアプリを作ることができそうです。
今回はNEMのAPIリファレンスなどを見ながら、Android 上で NEM の APIを叩くサンプルを作ってみました。
2017/11/21 追記
この記事の内容を元にライブラリを作成して公開しました。
NEM って何?
New Economy Movement(新しい経済運動)の頭文字で NEM です。
その名前の通り、新しい経済圏を作るために始まった仮想通貨プロジェクトです。
NEM については、こちらの記事が情報が網羅的に載ってて参考になりました。
NEM の利点として、APIが公開されているということがあります。
上記 APIのドキュメントを見ればわかるかと思いますが、RestfulなHTTPで JSONデータをリクエスト&レスポンスする、よくある Web API の形式になっています。
このドキュメントに記載のルールに沿ってHTTPリクエストを送ることで、誰でも気軽にブロックチェーンに情報を刻むことができます。
注意点
念のため。
- 今回作成したアプリは、秘密鍵を端末内に平文で保存しています。実際のアプリでは暗号化するなどにして厳重に管理しましょう。
- 今回作成したコードを使って何か損害が発生しても一切責任は取りませんので、全て自己責任でお願いします。
前知識
コードの説明に入る前に、前知識です。
NEM では、アカウントの情報を秘密鍵と公開鍵を使って管理します。
クライアントは秘密鍵を使って署名を行ったデータをサーバに送信し、サーバ(NIS)は公開鍵を使って署名を検証し、送信されてきたデータが正当であることを確認します。
公開鍵は秘密鍵を元に作成しますが、この公開鍵の作成と、署名作成・検証には Ed25519 というアルゴリズムが使われています。
図示すると↓な感じ
作ったアプリ
では今回作った Android アプリ。超簡易ウォレットって感じのものです。
このアプリを使ってやれることは以下の3点
- アプリ立ち上げると自動でアカウントを生成して保存
- NEM の API を使ってアカウントの残高を取得
- NEM の API を使って別のアカウントに送金
コード解説
例の如く、全文はGitHubを参照ください。
順を追って説明していきます。
依存ライブラリの追加
まず、必要なライブラリを揃えます。
前知識に書いたように、NEMの暗号化方式は Ed25519 ですが、Androidの標準ライブラリでは Ed25519は利用できません。
NemProject のGitHub にあるAndroidアプリ はEd25519を自前で実装しているようです。
また、NEMの coreモジュールもJavaで書かれていますが、Ed25519の実装が入っています。
Ed25519を自分で実装しようかとも思ったのですが、相当にややこしいので、下記ライブラリを使います。Ed25519のリファレンス実装をJavaにポーティングしたもののようです。
ただし、一点問題があります。
NEMはEd25519内部で使うハッシュアルゴリズムとして、”SHA3-512″ を使っているようです。
上記ライブラリはハッシュアルゴリズムとして “SHA-512” を使っているので、そのままでは使えません。
(これがなかなかわからず、大分ハマりました・・・)
というわけで、SHA3-512 を使えるようにするために、Spongy Castle という暗号化ライブラリを使います。Bouncy Castle という有名な暗号化ライブラリを Android環境で使えるようにしたものです。
以上をあわせて、build.gradle に下記のように記述を追加します。
1 2 3 4 5 6 7 8 9 |
dependencies { ...中略... implementation 'net.i2p.crypto:eddsa:0.2.0' implementation 'com.madgag.spongycastle:prov:1.51.0.0' implementation 'com.madgag.spongycastle:core:1.51.0.0' ...中略... } |
Android Studio 2系は implementation じゃなく compile です。
初期化
Spongy Castle を利用するために、初期化コードを追加します。
アプリ初期化時に下記コードを追加します。以下、コードは全て kotlinです。
1 2 |
Security.addProvider(BouncyCastleProvider()) |
本アプリではApplication の onCreate でやっています。
アカウント作成
アカウントの作成は /account/generate でもできると APIのドキュメントにありますが、平文で秘密鍵が通信路を流れるので非常に危険な API です。
そのためか、現在は NIS にこのパスを送ってもエラーで失敗するようになっていました。
というわけでローカルでキーペアを作成することにします。
さて、アカウントのキーペアを作成するのですが、上で図示したように、公開鍵は秘密鍵から作成されます。
秘密鍵はどうやって作るかというと、ただの32バイトの乱数です。
え?マジで?と思ったんですが、NemAndroidApp も SecureRandom で乱数として作っています。
というわけで、乱数として秘密鍵を作成します。
1 2 3 4 5 6 |
// 新しいキーを作成 val newKey = ByteArray(32) SecureRandom().nextBytes(newKey) val privateKeySeed = newKey |
本アプリでは作った秘密鍵を平文で Preferenceに保存してますが、実際のアプリでは暗号化するなどして保存するようにしてください。
次に、先程作成した秘密鍵のバイト列をもとに PrivateKey オブジェクトと公開鍵を作っていきます。
前述の通り ed25519-java のライブラリがハッシュアルゴリズムとしてデフォルトで SHA-512 を使う設定になっているので、SHA3-512 を使う設定に変えます。
ちょっとトリッキーな方法ですが、一旦適当な鍵ペアを ed25519-java で作らせて、そこから Ed25519 のカーブ情報などを取り出し、ハッシュアルゴリズムだけ SHA3-512 に変えて改めて本当の秘密鍵と公開鍵を作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// カーブ関係のパラメータを取得する用に、適当に一つ鍵ペアを作成する val tmpPrivateKey = KeyPairGenerator().generateKeyPair().private as EdDSAPrivateKey val param = tmpPrivateKey.params // ハッシュアルゴリズムは SHA3-512 を使う val paramSpec = EdDSAParameterSpec(param.curve, "SHA3-512", param.scalarOps, param.b) // シードとパラメータを使って秘密鍵と、公開鍵を作成 val privateKey = EdDSAPrivateKey(EdDSAPrivateKeySpec(privateKeySeed, paramSpec)) val publicKey = EdDSAPublicKey(EdDSAPublicKeySpec(privateKey.a, privateKey.params)) // キーペアを作成 val kv = KeyPair(publicKey, privateKey) |
これでやっとキーペアが作成できました。
アドレス取得
キーペアが出来たので、NEMの送金アドレスを取得します。
送金アドレスは公開鍵からローカルでも算出できるみたいでしたが、サーバに問い合わせて取得することも出来ます。
下記のような URL で問い合わせできます。ちなみにこの問い合わせは、署名必要ありません。
1 2 |
http://ホストアドレス:7890/account/get/from-public-key?publicKey=公開鍵の16進表現 |
ホストアドレスは、スーパーノードと言われるホストが公開されていますので、それを利用させてもらいます。
例えば、下記のようなリクエストを送ると
1 2 |
http://62.75.251.134:7890/account/get/from-public-key?publicKey=1e68cbd764e54655.... |
結果がJSONで返ってきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "meta": { "cosignatories": [], "cosignatoryOf": [], "status": "LOCKED", "remoteStatus": "INACTIVE" }, "account": { "address": "...NEMのアドレス...", "harvestedBlocks": 0, "balance": 749994, "importance": 0.0, "vestedBalance": 74996, "publicKey": "...公開鍵...", "label": null, "multisigInfo": {} } } |
XEMの残高は balance というフィールドですね。マイクロNEM単位なので驚かれぬよう (上記アカウントには 0.75 XEM しか入ってません)。
取得したアドレスへ送金
さて、続いて送金トランザクションを作っていきますが、送金トランザクションには手数料がかかります。
本記事執筆時は最低手数料 0.05 xem (日本円だと 1円くらい) の微々たるものですが、手数料分のXEMがアカウントにないと送金できません。
というわけで、今回取得したアドレスに送金します。
NanoWallet を使っている方はそちらからでも送金できますし、まだ XEM を持っていない方は Zaif などの取引所で購入してください。
Zaif は手数料も安いのでオススメです。
あくまでテストアプリなので、間違っても大金を送金しないようにしてくださいね。保証しませんよ。とりあえず 1xem 送っておけば 10回くらいはテストできます。
XEMを送金してから、1-2分後に再度アプリの方でアカウント情報を取得し直すと、残高が加算されているのが確認できるかと思います。
取得したアドレスから送金
手数料のXEMは用意できましたでしょうか?
では今回の山場、送金トランザクションを発行してみます。
送金トランザクションは、署名が必要なトランザクションなので、前知識の図のように先に送信するデータを用意してそれに署名を付けてまとめて送信するという手順になります。
送信データの作り方はAPIドキュメントの7.9章から書いてあります。
/transaction/prepare-announceという、署名を取得するためのAPIもありますが、秘密鍵を通信路に流してしまうのでスーパーノードに対しては使えません
よって、ここではローカルでバイト配列に情報を詰め込んでいき、署名作成を行った後に NISにデータを送ります。
エンディアンは重要ですとだけAPIのドキュメントに書いてありますが、例を見る限り数値は全てリトルエンディアンのようです。
詰め込むバイト列をざっくり書くと、下記のような感じです。
- トランザクションの種別(4バイト)
- バージョン(メインネットワークか、テストネットワークか。4バイト)
- タイムスタンプ(4バイト)
- 公開鍵の長さ(32固定、4バイト)
- 公開鍵(32バイト)
- 手数料(マイクロNEM単位、8バイト)
- デッドライン(4バイト)
ここまではトランザクション共通です。以下は、送金(転送トランザクション)用のバイト列
- 受信者のアドレス長(40固定、4バイト)
- 受信者のアドレス(UTF-8、40バイト)
- 送金量(マイクロNEM単位、8バイト)
- メッセージフィールドの長さ(下3つのバイト数の合計。ここが 0 なら下2つのフィールドは存在しない。4バイト)
- メッセージ種別(平文か、暗号文か。4バイト)
- メッセージ長(4バイト)
- メッセージペイロード(UTF-8)
- モザイク数(4バイト)
以下、送信するのがモザイク(XEM以外のトークン)の場合は送信するモザイクに関する情報が続きますが、今回はモザイクを送信しないのでここまで。
トランザクションの種別やバージョンなど、実際のバイト列については APIのドキュメントに詳しく説明がありますのでそちらを確認ください。
少しわかりにくいものについて、補足していきます。
タイムスタンプ、デッドライン
タイムスタンプは現在時刻の情報ですが、これはNEMのネメシスブロック(最初のブロック)生成時からの経過秒数です。
NEM というシステムが動き出してからのシステム時間って感じですね。
NEM のネメシスブロック生成日時は 2015/03/29 0:06:25 UTC なので、そこからの差分を Calender クラスを使って求めています。
Calender クラスの month は 0 オリジンなので注意。
1 2 3 4 5 6 7 8 9 10 11 12 |
fun currentTimeFromOrigin(): Int { // NEM のネメシスブロック生成日時は 2015/03/29 0:06:25 // この日時から現在時刻の差分を得る val origin = Calendar.getInstance(TimeZone.getTimeZone("UTC")) origin.set(2015, 2, 29, 0, 6, 25) val current = Calendar.getInstance(TimeZone.getTimeZone("UTC")) // ミリ秒 -> 秒に変換して返す return Math.floor(((current.time.time - origin.time.time) / 1000.0)).toInt() } |
本来は NTPとか使って正確な時刻を割り出した方が良さそうですが、今回は端末が持っている時刻をそのまま使っています。
デッドラインは、この時刻までに処理が終わらなければトランザクションを破棄するというリミットで、こちらもネメシスブロック生成時からの経過秒数です。
本アプリでは適当にタイムスタンプに1時間(3600秒)足した値を送っています。
手数料
手数料とか自動で計算してくれよって話かと思いきや、APIドキュメントによると手数料を上げると優先的に処理される仕組みになっているらしいです。
だから公式のドキュメントは最低手数料という書き方になっているんですね。
その最低手数料ですが、割と最近(2017年8月)に更新がありました。
本アプリでは執筆時点(v0.6.93) での手数料計算をしています。
v0.6.93では
- 10000 XEM 毎に手数料 0.05 xem
- 最低手数料は 0.05xem
- 上限は 1.25 xem
- メッセージがある場合、0.05 xem 開始で メッセージ長 32バイト毎に 0.05xem
ということのようです。手数料はマイクロNEM表記なので、下記のような式で求めています。
間違ってたら、誰か教えてください。
1 2 3 4 5 6 7 8 9 10 11 |
fun calculateMinimumTransactionFee() : Long { val xemTransferFee = Math.max(50_000L, Math.min(((ammount / 10_000_000_000L) * 50_000L), 1_250_000L)) val messageTransferFee = if (messagePayload.isNotEmpty()) { 50_000L * (1L + messagePayload.toByteArray(Charsets.UTF_8).size / 32L) } else { 0L } return xemTransferFee + messageTransferFee } |
ちなみに、手数料は NanoWalletから送金する時に自動計算結果が表示されます。試しに送信するXEMの量やメッセージ長を変えてみるとわかりやすいかと思います。
署名
送金用バイト列が出来たら、そのバイト列に対して秘密鍵で署名を行います。
これは ed25519-java の EdDSAEngineで署名のバイト列を取得できます。
1 2 3 4 5 6 7 |
fun sign(key: PrivateKey, message: ByteArray): ByteArray { val engine = EdDSAEngine() engine.initSign(key) return engine.signOneShot(message) } |
送信
最後に、送金用バイト列と署名バイト列をJSON形式にして POST します。
パスは /transaction/announce なので、URLとしては
1 2 |
http://62.75.251.134:7890/transaction/announce |
のようになり、POSTのボディとして下記のようなJSONを指定します。
1 2 3 4 5 |
{ "data":"0101000002...(送金用バイト列の16進数表現の文字列)....", "signature":"ab586463e0e...(署名バイト列の16進表現の文字列)..." } |
リクエストに成功すると、下記のようなJSONがレスポンスで返ってきます。
1 2 3 4 5 6 7 8 9 10 |
{ "innerTransactionHash": {}, "code": 1, "type": 1, "message": "SUCCESS", "transactionHash": { "data": "f15896c03...(トランザクションハッシュ)..." } } |
失敗すると、message で失敗要因が通知されるので確認しましょう。
(さらっと書いてますが、このトランザクション成功させるために何度 FAILURE_SIGNATURE_NOT_VERIFIABLE のメッセージを見たことか、、、)
さいごに
署名検証などハマるポイントはありましたが、署名付きのトランザクション送信ができるようになりました。
繰り返しになりますが、全文はGitHubを参照ください。
ここまでできれば、あとはアプリとして体裁を整えればウォレットアプリ的なものであれば作っていける状況かと思います。
NEM内の独自通貨、モザイクの送信の記事も書きましたのでよければ参考にどうぞ。
最後まで読んでいただきありがとうございます。 このブログを「いいな」と感じていただけましたら、Twiter にてフォローいただけるとうれしいです。ブログ更新情報などもお届けします。
Follow @ryuta461
この記事をシェアする
初めまして。
ブロックチェーンを使ったアプリ、興味深いです。