golangからbitflyer Lightning API呼び出して、保有資産の一覧を取得する

2021-02-27

golangからbitflyer Lightning API呼び出して、保有資産を取得する

golangで、bitflyer APIを用いて保有資産を取得する方法を記載します。 公式にはgolangのサンプルが無いので参考になれば嬉しいです。

実行するには、bitflyer口座を開設し個人認証済みである必要があります。 保有している口座のアカウントを用いて、下記サイトにてAPIのkeyとsecretを取得します。

bitflyer lightning bitflyer lightning API Document

ファイル構成

E:.
│  Config.ini
│  go.mod
│  go.sum
│  Trade.go
│─bitflyer
|   bitflyer.go
│─utils
|   logging.go
└─Config
    Config.go

Config.ini

ファイルの中身にはこのように記載します。

[gotraing]
log_file = trading.log

[bitflyer]
api_key = XXXXXX
api_secret = XXXXX

出力するファイル名をSettingファイルと bitflyerで使用するAPIKeyとsecretを記載します。

config.go

前回の記事で作成したものを流用。 詳細はコチラ:golangでLogファイルを作成する方法

コード

package config

import (
    "log"
    "os"

    "gopkg.in/ini.v1"
)

type ConfigList struct {
    LogFile   string
}

var Config ConfigList

func init() {
    cfg, err := ini.Load("config.ini")
    if err != nil {
        log.Printf("Failed to read file: %v", err)
        os.Exit(1)
    }

    Config = ConfigList{
        LogFile:   cfg.Section("LogSettings").Key("log_file").String(),
    }
}

bitflyer.go

全体のコードはこのようになりました。

package bitflyer

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
    "strconv"
    "time"
)

const baseURL = "https://api.bitflyer.com/v1/"

type APIClient struct {
    key        string
    secret     string
    httpClient *http.Client
}

type Balance struct {
    Currency_code string  `json:"currency_code"`
    Amount        float64 `json:"amount"`
    Available     float64 `json:"available"`
}

func New(key, secret string) *APIClient {
    apiClient := &APIClient{key, secret, &http.Client{}}
    return apiClient
}

//https://lightning.bitflyer.com/docs#api制限
func (api APIClient) getHeader(method, path string, body []byte) map[string]string {
    timestamp := strconv.FormatInt(time.Now().Unix(), 10)
    text := timestamp + method + path + string(body)
    secret := []byte(api.secret)

    hashmac := hmac.New(sha256.New, secret)
    hashmac.Write([]byte(text))
    expectedMAC:=hashmac.Sum(nil)
    sign := hex.EncodeToString(expectedMAC)

    header := map[string]string{
        "ACCESS-KEY":       api.key,
        "ACCESS-TIMESTAMP": timestamp,
        "ACCESS-SIGN":      sign,
        "Content-Type":     "application/json",
    }
    return header
}

func (api *APIClient) reqMessage(method, urlPath string, query map[string]string, data []byte) (body []byte, err error) {
    baseURL, err := url.Parse(baseURL)
    if err != nil {
        log.Printf("action=getBalance err=%s", err.Error())
        return nil, err
    }
    apiURL, err := url.Parse(urlPath)
    if err != nil {
        log.Printf("action=getBalance err=%s", err.Error())
        return nil, err
    }

    absURL := baseURL.ResolveReference(apiURL).String()
    req, err := http.NewRequest(method, absURL, bytes.NewBuffer(data))
    if err != nil {
        log.Printf("action=getBalance err=%s", err.Error())
        return nil, err
    }

    q := req.URL.Query()
    for key, value := range query {
        q.Add(key, value)
    }
    req.URL.RawQuery = q.Encode()

    headers := api.getHeader(method, req.URL.RequestURI(), data)
    for key, value := range headers {
        req.Header.Add(key, value)
    }

    resp, err := api.httpClient.Do(req)
    if err != nil {
        log.Printf("action=getBalance err=%s", err.Error())
        return nil, err
    }
    defer resp.Body.Close()
    body, err = ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Printf("action=getBalance err=%s", err.Error())
        return nil, err
    }
    return body, nil
}

//https://lightning.bitflyer.com/docs#資産残高を取得
func (api *APIClient) GetBalance() ([]Balance, error) {
    url := "me/getbalance"
    resp, err := api.reqMessage("GET", url, map[string]string{}, nil)
    if err != nil {
        log.Printf("action=getBalance err=%s", err.Error())
        return nil, err
    }
    var balance []Balance
    err = json.Unmarshal(resp, &balance)
    if err != nil {
        log.Printf("action=getBalance err=%s", err.Error())
        return nil, err
    }
    return balance, nil
}

getHeader

API Documentation #api制限 を参考に作成していきます。

公式ドキュメントによると、 HTTP リクエストヘッダを作っている箇所がこちらに記載されています。 javascriptで書かれているため、この部分を書き直していきます。

var text = timestamp + method + path + body;
var sign = crypto.createHmac('sha256', secret).update(text).digest('hex');

var options = {
    url: 'https://api.bitflyer.com' + path,
    method: method,
    body: body,
    headers: {
        'ACCESS-KEY': key,
        'ACCESS-TIMESTAMP': timestamp,
        'ACCESS-SIGN': sign,
        'Content-Type': 'application/json'
    }
};

Go言語で書き直したのがこの部分です。 javascriptで記載しているheadersと同じものを返す構成になっています。

headersの中身は公式ドキュメントからの記載そのままですが…

  • ACCESS-KEY: 開発者ページで発行した API key を記載しています。
  • ACCESS-TIMESTAMP: リクエスト時の Unix Timestampを記載。time.Now().Unix()で取得します。
  • ACCESS-SIGN: 以下の方法でリクエストごとに生成した署名 ※ACCESS-TIMESTAMP, HTTP メソッド, リクエストのパス, リクエストボディ を文字列として連結したものを、 API secret で HMAC-SHA256 署名
func (api APIClient) getHeader(method, path string, body []byte) map[string]string {
    timestamp := strconv.FormatInt(time.Now().Unix(), 10)
    text := timestamp + method + path + string(body)
    secret := []byte(api.secret)

    hashmac := hmac.New(sha256.New, secret)
    hashmac.Write([]byte(text))
    sign := hex.EncodeToString(hashmac.Sum(nil))

    header := map[string]string{
        "ACCESS-KEY":       api.key,
        "ACCESS-TIMESTAMP": timestamp,
        "ACCESS-SIGN":      sign,
        "Content-Type":     "application/json",
    }
    return header
}

HMAC-SHA256署名に関しては、golangで用意されているhmacというPackageを利用しました。 参照:Package hmac

HMACで署名しているのがこの部分です。hmacの公式にあるOverviewそのままですね。

    hashmac := hmac.New(sha256.New, secret)
    hashmac.Write([]byte(text))
    expectedMAC:=hashmac.Sum(nil)
    sign := hex.EncodeToString(expectedMAC)

reqMessage

HTTP リクエスト処理について説明します。 もとコードからError処理を割愛して記載します。

リクエストメッセージをつなげる処理。

baseURLに、今回であれば「me/getbalance」というようなAPI固有のアドレスを繋げる処理がコチラ。 繋げたurlを、ResolveReference()を叩いて相対アドレスから絶対アドレスに変換しています。 http.NewRequest()を用いて要求を出します。(今回あれば、methodにはGETが入ります。

const baseURL = "https://api.bitflyer.com/v1/"
~~~中略
func (api *APIClient) reqMessage(method, urlPath string, query map[string]string, data []byte) (body []byte, err error) {
    baseURL, err := url.Parse(baseURL)
    apiURL, err := url.Parse(urlPath)
    absURL := baseURL.ResolveReference(apiURL).String()
    req, err := http.NewRequest(method, absURL, bytes.NewBuffer(data))

要求するとこのような応答が取得できます。

GET https://api.bitflyer.com/v1/me/getbalance HTTP/1.1 %!s(int=1) %!s(int=1) map[] {} %!s(func() (io.ReadCloser, error)=0x122cfa0) %!s(int64=0) [] %!s(bool=false) api.bitflyer.com map[] map[] %!s(*multipart.Form=<nil>) map[]   %!s(*tls.ConnectionState=<nil>) %!s(<-chan struct {}=<nil>) %!s(*http.Response=<nil>) %!s(*context.emptyCtx=0xc0000140b0)}

参考 URL.ResolveReference

リクエストメッセージにQueryやヘッダ情報を追加する

先程までに取得したreqからURL(https://api.bitflyer.com/v1/me/getbalance)を取り出しを取り出し) Queryが必要なメッセージの場合には、こちらで追加します。 ※今後の拡張性のため追加しましたが、資産情報取得する際には不要です。

    q := req.URL.Query()

    for key, value := range query {
        q.Add(key, value)
    }
    req.URL.RawQuery = q.Encode()

ヘッダ情報を追加していきます。

    headers := api.getHeader(method, req.URL.RequestURI(), data)
    for key, value := range headers {
        req.Header.Add(key, value)
    }
リクエストを送信し、Bodyを取得する

http.Clientを使ってリクエストを送信する箇所と、 リクエスト結果からBody部分を読み出す部分。

    resp, err := api.httpClient.Do(req)
    defer resp.Body.Close()

    body, err = ioutil.ReadAll(resp.Body)
    return body, nil
}

GetBalance

頭文字を大文字にするとPublicで読み出せる関数 小文字にするとPrivateで読み出せる関数です。(私は最近知りました)

API Documentを参考に資産残高取得処理を記載していきます。

レスポンスを格納するための構造体を作ります。

type Balance struct {
    Currency_code string  `json:"currency_code"`
    Amount        float64 `json:"amount"`
    Available     float64 `json:"available"`
}

リクエストURLと、メッセージの種類(GET)を指定します。

GET /v1/me/getbalance

リクエスト結果はJsonで取得されるので、 UnmarshalでJson ー>構造体 として変換します。 Balance構造体に変換結果を格納していきます。

//https://lightning.bitflyer.com/docs#資産残高を取得
func (api *APIClient) GetBalance() ([]Balance, error) {
    url := "me/getbalance"
    resp, err := api.reqMessage("GET", url, map[string]string{}, nil)
    var balance []Balance
    err = json.Unmarshal(resp, &balance)
    return balance, nil
}

参考までに、 JSONで取得される情報の中身はこのようになります。

[{"currency_code":"JPY","amount":0.0,"available":0.0},{"currency_code":"BTC","amount":0.0,"available":0.0},{"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},{"currency_code":"XRP","amount":0.0,"available":0.0},{"currency_code":"BAT","amount":0.0,"available":0.0},{"currency_code":"XLM","amount":0.0,"available":0.0},{"currency_code":"XEM","amount":0.0,"available":0.0},{"currency_code":"XTZ","amount":0.0,"available":0.0}]

Trade.go

先程までに実装してきたものを読み出します。

package main

import (
    "config/bitflyer"
    "config/config"
    "config/utils"
    "fmt"
)

func main() {
    utils.LoggingSetting(config.Config.LogFile)

    apikey := config.Config.ApiKey
    secretKey := config.Config.ApiSecret
    apiClient := bitflyer.New(apikey, secretKey)

    balances, err := apiClient.GetBalance()
    if err == nil {
        fmt.Println(balances)
    }

}

iniファイルに書き込んでいるAPIkeyとAPISecretを読み込みます。 bitflyer用のAPIを操作するClientを作成しています。 最後に、Clientに実装している保有資産を取得関数を呼び出す作りです。

    apikey := config.Config.ApiKey
    secretKey := config.Config.ApiSecret
    apiClient := bitflyer.New(apikey, secretKey)

    balances, err := apiClient.GetBalance()

出力例:

> go run .\Trade.go
[{JPY 0 0} {BTC 0 0} {BCH 0 0} {ETH 0 0} {ETC 0 0} {LTC 0 0} {MONA 0 0} {LSK 0 0} {XRP 0 0} {BAT 0 0} {XLM 0 0} {XEM 0 0} {XTZ 0 0}]

保有残高によって記載は変わりますがこのような感じで取得することができました。

まとめ

BitflyerのPrivate APIを活用する方法まで紹介しました。 引き続き、読み出し用のAPI構築していければと思います。