中安拓也のブログ

プログラミングについて書くブログ

仮想通貨取引所のAPIを使ってみる #3 - Private API にアクセスする

前回記事はこちら

仮想通貨取引所のbitFlyerのHTTP Private APIにアクセスして、自分の資産残高を取得する。

バージョン情報

JavaScriptのフレームワークであるAngularを使用する

  • Angular: 5.2.9

  • crypto-js: 3.1.9-1

今回アクセスするAPIは認証が必要

第1回目第2回目でアクセスした、認証が不要なPublic APIではなく、認証が必要なPrivate APIにアクセスする

Private APIの認証

https://lightning.bitflyer.jp/docs

Private APIの認証では、下記の情報をHTTPリクエストヘッダーに含める必要がある。

  • ACCESS-KEY: 開発者ページで発行した API key

  • ACCESS-TIMESTAMP: リクエスト時の Unix Timestamp

  • ACCESS-SIGN: ACCESS-TIMESTAMP, HTTP メソッド, リクエストのパス, リクエストボディ を文字列として連結したものを、 API secret で HMAC-SHA256 署名を行った結果

下準備

HMAC-SHA256 署名を実施するのに必要なライブラリ、crypto-jsをインストールする。

$ npm i crypto-js

コード

Private API の一つにアクセスして、自分の資産残高を取得してみる

src/app/services/bitflyer.service.ts

// ...省略
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { PrivateAPIKeySet } from '../private/private.component';
import * as crypto from 'crypto-js';

const URLS = {
  BASE: 'https://api.bitflyer.jp',
  GET_BALANCE: '/v1/me/getbalance'
};

@Injectable()
export class BitflyerService {
  constructor(
    private http: HttpClient,
    private action: BitflyerTickerActions
  ) {}

  getBalance = (keySet: PrivateAPIKeySet): void => {
    const timestamp = Date.now().toString();
    const method = 'GET';
    // ACCESS-TIMESTAMP, HTTP メソッド, リクエストのパス,
    // リクエストボディ を文字列として連結したもの
    const text = timestamp + method + URLS.GET_BALANCE;

    // HMAC-SHA-256で署名を作成する
    // keySet.secretはAPI secret
    const hmac = crypto.HmacSHA256(text, keySet.secret);
    // 署名をHEXでエンコード
    const sign = crypto.enc.Hex.stringify(hmac);

    const httpOptions = {
      headers: new HttpHeaders({
        // keySet.keyはAPIキー
        'ACCESS-KEY': keySet.key,
        'ACCESS-TIMESTAMP': timestamp,
        'ACCESS-SIGN': sign
      })
    };
    this.http
      .get(`${URLS.BASE}${URLS.GET_BALANCE}`, httpOptions)
      .subscribe(console.log);
  };
}

動作確認

上記のコードを実行すると、下記の結果(自分の資産残高)が帰ってくる。

[{
    "currency_code": "JPY",
    "amount": 531.0,
    "available": 531.000000000000
}, {
    "currency_code": "BTC",
    "amount": 0.0000648,
    "available": 0.000064800000
}, {
    "currency_code": "BCH",
    "amount": 0.0,
    "available": 0.0
}, {
    "currency_code": "ETH",
    "amount": 0.0,
    "available": 0.0
}, {
    "currency_code": "ETC",
    "amount": 0.0,
    "available": 0.0
}, {
    "currency_code": "LTC",
    "amount": 0.0,
    "available": 0.0
}, {
    "currency_code": "MONA",
    "amount": 0.0,
    "available": 0.0
}, {
    "currency_code": "LSK",
    "amount": 0.0,
    "available": 0.0
}]

参考サイト

HMAC-SHA256 署名 プログラム・ソース・ソフトウェアの配布 PJC

base64ってなんぞ??理解のために実装してみた - Qiita

GolangでHMAC-SHA256署名する - 逆さまにした

HMAC - Wikipedia

Angular Materialでログインフォームを作る

f:id:l08084:20180420160959g:plain

Angular Materialを使って、ログイン用のフォームを作ります

Angular Materialのインストールから解説している記事はこちら

バージョン情報

  • Angular: 5.2.9

  • Node: 8.1.4

  • @angular/material: 5.2.4

テンプレート駆動型とモデル駆動型

Angularのフォームには、テンプレート駆動型とモデル駆動型があります

  • テンプレート駆動型のフォーム

    • 検証ルールをテンプレート側に記述する
    • モデル駆動型よりも手軽に実装できる
    • 複雑な要件には向いていない
    • NgModuleFormsModuleのインポートが必要
  • モデル駆動型のフォーム

    • 検証ルールをコンポーネント側に記述する
    • コードが冗長になりやすい
    • 複雑な要件でも表現できる
    • NgModuleReactiveFormsModuleのインポートが必要

今回は、簡単に書けそうなテンプレート駆動型のフォームを採用します。ただ、Angular Materialで作られたフォームのサンプルを見ると、モデル駆動型のフォームで記述されているものが多い気がするので、後々問題が発生したら、モデル駆動型に切り替えます。

NgModuleの設定

ログインフォーム作成のために、下記のAngular Materialコンポーネントを使ったので、NgModuleに設定していきます。

  • MatButtonModule

  • MatCardModule

  • MatInputModule

  • MatFormFieldModule

  • MatIconModule

また、今回はAngularのテンプレート駆動型のフォームを使うので、FormsModuleの設定も必要です。

src/app/app.module.ts:

import { FormsModule } from '@angular/forms';

import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';


@NgModule({
  declarations: [AppComponent, ExchangeListComponent, PrivateComponent],
  imports: [
    // add this!
    MatButtonModule,
    MatCardModule,
    MatInputModule,
    MatFormFieldModule,
    MatIconModule,
    FormsModule,
    // ...省略
  ],
// ...省略

下記の手順を実行してください

  1. 上記のモジュール(MatFormFieldModule他6つ)をインポート

  2. @NgModuleデコレータで定義されているimports:[]リストに上記の6つのモジュールを追加する

index.htmlにアイコンのCDNを追加

マテリアルアイコンをパスワード欄で使うので、下記のCDNをindex.htmlに追加する必要があります。

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

src/app/index.html:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>CriptocurrencyApiCall</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>

<body>
  <app-root></app-root>
</body>

</html>

ログインフォームを作る

f:id:l08084:20180420161351p:plain

下記の要件に従って、ログインフォームを作ります。

  • ログインフォームの要件
    • API Key欄とAPI Secret欄がある
    • API Secret欄は、目のアイコンをクリックすると、type=passwordが解除される
    • バリデーションは、API Key欄とAPI Secret欄の必須チェックのみ
    • バリデーションのチェックが全部OKにならないと、Loginボタンはdisable
    • 必須チェックを満たしていない欄には、<mat-error>によるエラーメッセージが表示される

src/app/private/private.component.html:

<mat-card class="login-card">
  <mat-card-header>
    <mat-card-title class="login-title">bitFlyer API</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <form (ngSubmit)="onSubmit()" class="login-form" #loginForm="ngForm">
      <mat-form-field>
        <input matInput placeholder="API Key" id="key" [(ngModel)]="keySet.key" name="key" #key="ngModel" required>
        <mat-error *ngIf="key.errors?.required">You must enter a value</mat-error>
      </mat-form-field>

      <mat-form-field>
        <input [type]="hide ? 'password' : 'text'" matInput placeholder="API Secret" id="secret" [(ngModel)]="keySet.secret" name="secret"
          #secret="ngModel" required>
        <mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility' : 'visibility_off'}}</mat-icon>
        <mat-error *ngIf="secret.errors?.required">You must enter a value</mat-error>
      </mat-form-field>
      <button type="submit" mat-raised-button color="primary" [disabled]="loginForm.invalid">Login</button>
    </form>
  </mat-card-content>
</mat-card>

src/app/private/private.component.ts:

import { Component, OnInit } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';

export class PrivateAPIKeySet {
  constructor(public key: string, public secret: string) {}
}

@Component({
  selector: 'app-private',
  templateUrl: './private.component.html',
  styleUrls: ['./private.component.scss']
})
export class PrivateComponent implements OnInit {
  keySet = new PrivateAPIKeySet('', '');
  hide = true;
  constructor() {}

  ngOnInit() {}

  onSubmit() {
    console.log(this.keySet);
  }
}

src/app/private/private.component.scss:

.login-card {
  max-width: 400px;
}

.login-form {
  display: flex;
  flex-direction: column;
}

.login-title {
  font-weight: bold;
  font-size: 22px;
  color: #3f51b5;
}

参考サイト

https://material.angular.io/components/form-field/overview

【Angular】エラー: Can't bind to 'ngModel' since it isn't a known property of 'input'.

Angularでフォームを書いていたら、Can't bind to 'ngModel' since it isn't a known property of 'input'.というエラーに遭遇した

バージョン情報

  • Angular: 5.2.9

  • Node: 8.1.4

エラー原因

formControlを使っているのに、NgModuleReactiveFormsModuleをインポートしていないのが原因だった。

src/app/app.module.ts:

import { FormsModule, ReactiveFormsModule } from '@angular/forms';

// ...省略

@NgModule({
  declarations: [AppComponent, ExchangeListComponent, PrivateComponent],
  imports: [
    MatFormFieldModule,
    MatIconModule,
    FormsModule,
    // add this!
    ReactiveFormsModule,
    RouterModule.forRoot(myRoutes)
  ],
// ...省略

下記の手順を実行するとエラーが消える

  1. import { ReactiveFormsModule } from '@angular/forms';ReactiveFormsModuleをインポート

  2. @NgModuleデコレータで定義されているimports:[]リストにReactiveFormsModuleを追加する

エラーメッセージ全文

compiler.js:485 Uncaught Error: Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'. (" #loginForm="ngForm">
      <mat-form-field>
        <input matInput placeholder="API Key" id="key" [ERROR ->][(ngModel)]="keySet.key" name="key" [formControl]="key" required>
        <mat-error *ngIf="key.inval"): ng:///AppModule/PrivateComponent.html@7:55
Can't bind to 'formControl' since it isn't a known property of 'input'. ("m-field>
        <input matInput placeholder="API Key" id="key" [(ngModel)]="keySet.key" name="key" [ERROR ->][formControl]="key" required>
        <mat-error *ngIf="key.invalid">{{getErrorMessage()}}</mat-error"): ng:///AppModule/PrivateComponent.html@7:91
No provider for NgControl ("form (ngSubmit)="onSubmit()" class="login-form" #loginForm="ngForm">
      <mat-form-field>
        [ERROR ->]<input matInput placeholder="API Key" id="key" [(ngModel)]="keySet.key" name="key" [formControl]="key"): ng:///AppModule/PrivateComponent.html@7:8
    at syntaxError (webpack-internal:///./node_modules/@angular/compiler/esm5/compiler.js:706:34)
    at TemplateParser.parse (webpack-internal:///./node_modules/@angular/compiler/esm5/compiler.js:24889:19)
    at JitCompiler._parseTemplate (webpack-internal:///./node_modules/@angular/compiler/esm5/compiler.js:34842:37)
    at JitCompiler._compileTemplate (webpack-internal:///./node_modules/@angular/compiler/esm5/compiler.js:34817:23)
    at eval (webpack-internal:///./node_modules/@angular/compiler/esm5/compiler.js:34718:62)
    at Set.forEach (native)
    at JitCompiler._compileComponents (webpack-internal:///./node_modules/@angular/compiler/esm5/compiler.js:34718:19)
    at eval (webpack-internal:///./node_modules/@angular/compiler/esm5/compiler.js:34588:19)
    at Object.then (webpack-internal:///./node_modules/@angular/compiler/esm5/compiler.js:695:77)
    at JitCompiler._compileModuleAndComponents (webpack-internal:///./node_modules/@angular/compiler/esm5/compiler.js:34587:26)

参考サイト

Angular

Angular Material でヘッダーを作る

f:id:l08084:20180417211036g:plain

Angular Materialを使ってナビゲーション付きのヘッダーを作る

Angular Materialのインストールから解説している記事は、こちら

バージョン情報

  • Angular: 5.2.9

  • Node: 8.1.4

  • @angular/material: 5.2.4

NgModuleの設定

ヘッダーの作成には、Angular MaterialのToolbar<mat-toolbar>を使う。

まず事前準備として、他のAngular Material コンポーネントと同様に、ツールバーのモジュールをNgModuleのimports: []に追加してあげる必要がある。

src/app/app.module.ts:

// ...省略
// add this!
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';

@NgModule({
  declarations: [AppComponent, ExchangeListComponent, PrivateComponent],
  imports: [
     // ...省略
    // add this!
    MatButtonModule,
    MatToolbarModule
  ],
// ...省略

今回、ヘッダーに他画面へのリンク(ボタン)を付けたいので、MatToolbarModuleだけでなく、MatButtonModuleも追加している。

テンプレートを編集する

Angularのテンプレートに、ツールバーのタグを追加して、ヘッダーが表示されるようにする。

<mat-toolbar-row>から、</mat-toolbar-row>までがヘッダーのコードになる。

今回、ヘッダーは1行でいいので<mat-toolbar-row>はワンセットにしている(2セットあると、ヘッダーが2行になる)。

他ページへのリンクがアンカーリンク<a>になっているが、Angular Materialでは、mat-buttonプロパティをつけることで、<button>でも<a>でもボタンのスタイルが適用される。

src/app/app.component.html

<mat-toolbar color="primary">
  <mat-toolbar-row>
    <span>Cryptocurrency</span>
    <span>
      <a mat-button routerLink="" class="header-button">Exchanges</a>
    </span>
    <span>
      <a mat-button routerLink="private" class="header-button">Login</a>
    </span>
  </mat-toolbar-row>
</mat-toolbar>
<router-outlet></router-outlet>

npmパッケージのバージョンを固定する

npmパッケージのバージョンを固定して、動作に変化がないようにしたい。

バージョン情報

  • Node: 8.1.4

  • npm: 5.6.0

  • macOS High Sierra: 10.13.4

バージョン固定の手順

npmのver5以降からpackage-lock.jsonが導入されたため、下記の手順でバージョンのロックができる

  1. package.jsonのバージョン指定をパッチバージョンまで記述(5.0.x)。互換記号(~,^など)は消す

  2. package-lock.jsonを一度削除し、$ npm install

  3. package.jsonから呼び出している子のpackageはlock.json側で管理される

初めからキャレット^記号がつかないようにしたい

package.jsonのバージョン指定にキャレット^記号がつくと、範囲指定になるためバージョンが固定されない。そもそも初めからキャレットがつかないようにしたい。

$ npm config set save-exact true

👆のコマンドを実行することにより、npmの設定を変更して、npm install時にキャレット^が付かないようにすることができる。

また、npmの設定は~/.npmrcに保存されているので、.npmrcファイルをプロジェクト直下に配置することによって、プロジェクト単位でnpm install時のキャレットを無効にすることができる

.npmrc:

save-exact=true

参考サイト

package.jsonのパッケージバージョンに記載される ^ (キャレット) とは?どうしてつくのか? - dackdive's blog

パッケージマネージャがパッケージをインストールする仕組み – ymyzk’s blog

IonicをAndroid/iOSでビルド・パッケージ化

毎回やり方を忘れるので書きました...PCはMacです。Ionic以外のハイブリットモバイルアプリフレームワークでも概ね同じ手順になると思います。

バージョン情報

今回ビルドするアプリでは、Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っています

  • Android端末: Nexus6(Android 7.1.1)

  • iOS端末: iPhone 8 plus(iOS: 11.1.2)

  • Android Studio: 3.1.1

  • ionic info コマンドの結果

cli packages: (/usr/local/lib/node_modules)

    @ionic/cli-utils  : 1.19.2
    ionic (Ionic CLI) : 3.20.0

global packages:

    cordova (Cordova CLI) : 8.0.0

local packages:

    @ionic/app-scripts : 3.1.8
    Cordova Platforms  : android 7.0.0 ios 4.5.4
    Ionic Framework    : ionic-angular 3.9.2

System:

    Android SDK Tools : 26.0.2
    ios-deploy        : 1.9.2
    ios-sim           : 6.1.2
    Node              : v8.1.4
    npm               : 5.6.0
    OS                : macOS High Sierra
    Xcode             : Xcode 9.3 Build version 9E145

Environment Variables:

    ANDROID_HOME : /Users/user-name/Applications/android-sdk-macosx

Misc:

    backend : pro

ビルド手順

プラットフォーム作成(Android/iOS共通)

  1. node_modulesplatformsplugins配下を全て削除する

  2. ターミナルを開いて、プロジェクト直下に移動

  3. $ npm iコマンドでnpmライブラリをインストールする

  4. $ cordova platform add androidコマンドを実行して、プラットフォームとプラグインをインストール&作成

  5. $ cordova platform add iosコマンドを実行して、プラットフォームとプラグインをインストール&作成

$ npm i
$ cordova platform add android
$ cordova platform add ios

Android

実機ビルド

Android端末でのビルド方法について

f:id:l08084:20180416170818p:plain

  1. Android Studioを立ち上げる

  2. メニューから「Inport project (Gradle, Eclipse ADT, etc.)」を選択する

  3. ビルドしたいプロジェクトのplatforms/androidを選択する

f:id:l08084:20180416171125p:plain

上のポップアップが出たら「OK」を選択

f:id:l08084:20180416171922p:plain

  1. Android端末をPCにつないで実行ボタンを選択

  2. Android端末上でアプリが起動する

パッケージ化

apkファイルの作成方法について

Android Studioのメニューから [Build] -> [Build APK(s)] を選択

f:id:l08084:20180416172415p:plain

ビルドが完了すると、👆のポップアップが出るので、クリックすると、作成されたapkファイルがFinderで表示されます

f:id:l08084:20180416172821p:plain
作成されたapkファイル

iOS

実機ビルド

iOS端末でのビルド方法について

f:id:l08084:20180406202651p:plain

Xcodeで対象アプリのplatforms/ios配下の.xcodeprojファイルを開く

f:id:l08084:20180416175755p:plain

Signingをした後、PCにiOS端末をつなぐ

f:id:l08084:20180416181720p:plain

接続したiOS端末を選択してから、実行ボタンをクリック

パッケージ化

ipaファイルの作成方法について

f:id:l08084:20180406203135p:plain

シミュレーターや実機ではなく、Generic iOS Deviceを選択

f:id:l08084:20180406203417p:plain

Xcodeのメニューから[Product]->[Archive]を選択(Archiveが選択できない場合は、Generic iOS Deviceが選択されているかをチェック)

f:id:l08084:20180406204224p:plain

Exportを選択した後、設定に応じて選択肢をクリックしていくと、ipaファイルが作成されます

関連サイト

【Ionic, Cordova】カスタムプラグインを導入した時のiOSビルド手順 - 中安拓也のブログ

【Xcode】iOSアプリをネットワーク経由で配布する - In Houseビルドの方法 - 中安拓也のブログ

【Xcode】iOSシミュレータをブラウザでデバックする

iOSシミュレーターについても、iPhone、iPadなどの実機同様にSafariのWebインスペクタでデバックできるって知ってましたか?私は知りませんでしたが....

バージョン情報

  • macOS High Sierra: 10.13.4

  • Safari: 11.1

  • Xcode: 9.3

  • Simulator: 10.0

デバック手順

デバック手順についても、実機を使用する場合とそう変わりません。

f:id:l08084:20180415142308p:plain

  1. シミュレーターを立ち上げて、デバックしたいアプリを起動する。

  2. Safariを起動して、[開発] メニューを選択した後、シミュレーターとデバックしたいアプリを選択する

  3. Webインスペクタが開かれて、デバックできる状態になる

f:id:l08084:20180415142533p:plain

シミュレーターが認識されない場合

どうも実機をブラウザでデバックする時よりも、端末(シミュレーター)が認識されないケースが多い気がする...もし、Safariの[開発]メニューに起動しているシミュレーターが認識されない場合は、Safariを再起動すると治る事があります。