BitFlyer APIを触ってみた (Swift版)

##自己紹介

40代のフリーランスで主にiOSアプリのコードを書いています。
今年からAndroidも興味を持ち、現在学習中です。

##背景

最近、ニュースでも暗号化通貨とかブロックチェーンなどよく耳にしてますがどんなものなのか知りたくなったのがきっかけです。
そのものの技術も気になりますが、とりあえずbitFlyerで取引情報をAPIで公開しているのでswiftで書いてみました。
何年前にFXアプリを作った経験からすると取引の仕組みは似ている感じです。

##API情報

https://bitflyer.jp/ja-jp/api

bitFlyerから提供しているAPIは大きく`HTTP Public API`と`HTTP Private API`があります。
その中、`HTTP Public API`のサンプルを作ってみました。
当たり前の話ですが、HTTP Public APIはキーによる認証が不要なので簡単に使えます。
しかし、回数の制限があるのでトキュメントを確認してみてください。

##やってみましょう
データはHTTP ResufulAPI、リアルタイムAPIにより取得できます。
サンプルは両方書いています。

###HTTP ResufulAPI
ネットワーク通信は`Alamofire`を利用しています。
普段のRequestを発行する仕組みと変わらないですね。

```
import Foundation
import Alamofire

/*
 API制限
 HTTP API は、以下のとおり呼出回数を制限いたします。
 Private API は 1 分間に約 200 回を上限とします。
 IP アドレスごとに 1 分間に約 500 回を上限とします。
 注文数量が 0.01 以下の注文を大量に発注するユーザーは、一時的に、発注できる注文数が 1 分間に約 10 回までに制限されることがあります。
 システムに負荷をかける目的での発注を繰り返していると当社が判断した場合は、API の使用が制限されることがあります。ご了承ください。
 */
final class BFCoinAPI {
    
    // ホスト名
    private static let Host = "https://api.bitflyer.jp/v1"
    
    // 共通ヘッダー
    static let CommonHeaders:HTTPHeaders = [
        "Authorization": "",
        "Version": Bundle.main.infoDictionary!["CFBundleShortVersionString"]! as! String,
        "Accept": "application/json"
    ]
    
     //リクエスト処理の生成
    private class func createRequest(url:String, parameters: Parameters? = nil) -> Alamofire.DataRequest {
    
        return Alamofire.request("\(Host)\(url)",
                    method:.get,
                    parameters: parameters,
                    encoding: JSONEncoding.default,
                    headers: BFCoinAPI.CommonHeaders).validate()
    }
    
    //マーケットの一覧
    static func requestMarkets() -> Void {
        
        self.createRequest(url: "/markets", parameters: nil).responseJSON { response in
            
            if let JSON = response.result.value {
                print("Success with response")
                print(JSON)
            }else{
                print("Error with response")
            }
        }
    }
    
    //板情報
    static func requestBoard(_ productCode: String?) -> Void {
        
        let parameters = (productCode == nil) ? nil : ["product_code":productCode as Any]
        
        self.createRequest(url: "/board", parameters: parameters).responseJSON { response in
            
            if let JSON = response.result.value {
                print("Success with response")
                print(JSON)
            }else{
                print("Error with response")
            }
        }
        
    }
    
    //Ticker
    static func requestTicker(_ productCode: String?) -> Void {
        
        let parameters = (productCode == nil) ? nil : ["product_code":productCode as Any]
        
        self.createRequest(url: "/ticker", parameters: parameters).responseJSON { response in
            
            if let JSON = response.result.value {
                print("Success with response")
                print(JSON)
            }else{
                print("Error with response")
            }
        }
        
    }
    
...

```

###リアルタイムAPI

リアルタイムデータ取得のために`PubNub`のサービスを使っていますが、個人的にもこのサービス初めてなのでチュートリアルを読みながらコードを書きました。
ほんとはリアルタイムのみ処理するマネージャークラスを作りたかったですが、`AppDelegate`にしないとデータが受信できませんでした。（理由不明、だれか教えてください。）
結局、`AppDelegate`, `AppDeletate+RealtimeAPI`の２つのクラスに分けてコードを書いています。

`AppDelegate.swift`

```
import UIKit
import PubNub

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var client: PubNub!	//realtime api
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        //MARK: Setup realtime Client
        self.client = BFLCoinManager.sharedManager.realtimeClient
        self.client.addListener(self)
        
        return true
    }
```	

`AppDeletate+RealtimeAPI.swift`

```
import UIKit
import PubNub

extension AppDelegate : PNObjectEventListener {
    // Handle new message from one of channels on which client has been subscribed.
    func client(_ client: PubNub, didReceiveMessage message: PNMessageResult) {

        // Handle new message stored in message.data.message
        if message.data.channel != message.data.subscription {

            // Message has been received on channel group stored in message.data.subscription.
        }
        else {

            // Message has been received on channel stored in message.data.channel.
        }

        guard let dataMessage = message.data.message else {
            print("Received no message data.")
            return;
        }

        print("Received message: \(dataMessage) on channel \(message.data.channel) " + "at \(message.data.timetoken)")
        
        //更新処理
        BFLCoinManager.sharedManager.updateRealtime(message)
    }

    // New presence event handling.
    func client(_ client: PubNub, didReceivePresenceEvent event: PNPresenceEventResult) {

        // Handle presence event event.data.presenceEvent (one of: join, leave, timeout, state-change).
        if event.data.channel != event.data.subscription {

            // Presence event has been received on channel group stored in event.data.subscription.
        }
        else {

            // Presence event has been received on channel stored in event.data.channel.
        }


        guard let uuid = event.data.presence.uuid else {
            print("Received no uuid data.")
            return;
        }

        guard let state = event.data.presence.state else {
            print("Received no uuid state.")
            return;
        }

        if event.data.presenceEvent != "state-change" {

            print("\(uuid) \"\(event.data.presenceEvent)'ed\"\n" +
                "at: \(event.data.presence.timetoken) on \(event.data.channel) " +
                "(Occupancy: \(event.data.presence.occupancy))");
        }
        else {

            print("\(uuid) changed state at: " +
                "\(event.data.presence.timetoken) on \(event.data.channel) to:\n" +
                "\(state)");
        }
    }

    // Handle subscription status change.
    func client(_ client: PubNub, didReceive status: PNStatus) {

        if status.operation == .subscribeOperation {

            // Check whether received information about successful subscription or restore.
            if status.category == .PNConnectedCategory || status.category == .PNReconnectedCategory {

                let subscribeStatus: PNSubscribeStatus = status as! PNSubscribeStatus
                if subscribeStatus.category == .PNConnectedCategory {

                    // This is expected for a subscribe, this means there is no error or issue whatsoever.

                    // Select last object from list of channels and send message to it.
                    let targetChannel = client.channels().last!
                    client.publish("Hello from the PubNub Swift SDK", toChannel: targetChannel,
                                   compressed: false, withCompletion: { (publishStatus) -> Void in

                                    if !publishStatus.isError {

                                        // Message successfully published to specified channel.
                                    }
                                    else {

                                        /**
                                         Handle message publish error. Check 'category' property to find out
                                         possible reason because of which request did fail.
                                         Review 'errorData' property (which has PNErrorData data type) of status
                                         object to get additional information about issue.

                                         Request can be resent using: publishStatus.retry()
                                         */
                                    }
                    })
                }
                else {

                    /**
                     This usually occurs if subscribe temporarily fails but reconnects. This means there was
                     an error but there is no longer any issue.
                     */
                }
            }
            else if status.category == .PNUnexpectedDisconnectCategory {

                /**
                 This is usually an issue with the internet connection, this is an error, handle
                 appropriately retry will be called automatically.
                 */
            }
                // Looks like some kind of issues happened while client tried to subscribe or disconnected from
                // network.
            else {

                let errorStatus: PNErrorStatus = status as! PNErrorStatus
                if errorStatus.category == .PNAccessDeniedCategory {

                    /**
                     This means that PAM does allow this client to subscribe to this channel and channel group
                     configuration. This is another explicit error.
                     */
                }
                else {

                    /**
                     More errors can be directly specified by creating explicit cases for other error categories
                     of `PNStatusCategory` such as: `PNDecryptionErrorCategory`,
                     `PNMalformedFilterExpressionCategory`, `PNMalformedResponseCategory`, `PNTimeoutCategory`
                     or `PNNetworkIssuesCategory`
                     */
                }
            }
        }
    }
}
```

`BFCoinRealtimeAPI.swift`

```
import Foundation
import PubNub

enum PrefixChannel : String {
    case market         = "lightning_board_snapshot_"
    case board          = "lightning_board_"
    case ticker         = "lightning_ticker_"
    case executions     = "lightning_executions_"
}

final class BFCoinRealtimeAPI : NSObject {
    
    internal var client: PubNub!
    
    init(_ client: PubNub) {
        super.init()
        self.client = client
    }
    
    static func setupClient() -> PubNub {
        
        let configuration = PNConfiguration(publishKey: "BFCoinMgr", subscribeKey: "sub-c-52a9ab50-291b-11e5-baaa-0619f8945a4f")
        configuration.stripMobilePayload = false
        return PubNub.clientWithConfiguration(configuration)
    }
    
    //MARK: Channel
    func registChannelsAll(_ productCode: String) {
        
        self.registMarketChannel(productCode)
        self.registBoardChannel(productCode)
        self.registTickerChannel(productCode)
        self.registExecutionsChannel(productCode)
    }
    
    func releaseChannelsAll() {
        self.client.unsubscribeFromAll()
    }
    
    func registMarketChannel(_ productCode: String) {
        
        //let subscribeKey = "\(PrefixChannel.market.rawValue)\(productCode)"
        let subscribeKey = "lightning_ticker_BTC_JPY"
        self.client.subscribeToChannels([subscribeKey], withPresence: true)
    }
    
    func releaseMarketChannel(_ productCode: String) {
        
        let subscribeKey = "\(PrefixChannel.market.rawValue)\(productCode)"
        self.client.unsubscribeFromChannels([subscribeKey], withPresence: true)
    }
    
    func registBoardChannel(_ productCode: String) {
        
        let subscribeKey = "\(PrefixChannel.board.rawValue)\(productCode)"
        self.client.subscribeToChannels([subscribeKey], withPresence: true)
    }
    
    func releaseBoardChannel(_ productCode: String) {
        
        let subscribeKey = "\(PrefixChannel.board.rawValue)\(productCode)"
        self.client.unsubscribeFromChannels([subscribeKey], withPresence: true)
    }
    
    func registTickerChannel(_ productCode: String) {
        
        let subscribeKey = "\(PrefixChannel.ticker.rawValue)\(productCode)"
        self.client.subscribeToChannels([subscribeKey], withPresence: true)
    }
    
    func releaseTickerChannel(_ productCode: String) {
        
        let subscribeKey = "\(PrefixChannel.ticker.rawValue)\(productCode)"
        self.client.unsubscribeFromChannels([subscribeKey], withPresence: true)
    }
    ...

```

##まとめ

まだ作成中でありますが、サンプルコードはこちらです。
https://github.com/dolfalf/BFLCoinManager

何時間で簡単にデータを受信するまでできたのですが、要はAPIドキュメント読みながらコードを書けば簡単にできるレベルですね。
時間がある時にUIの方も少しコードを書いてみたいですね。