golangからbitflyer Lightning API呼び出して、保有資産の一覧を取得する
はじめに
記事の中では、ファイル構成やConfig.ini、config.go、bitflyer.goの詳細な説明があります。また、GetBalance関数を用いて保有資産を取得する方法も紹介しました。最後に、Trade.goファイルを使ってAPI keyとAPI Secretを読み込み、bitflyer用のAPIを操作するClientを作成し、保有資産を取得する方法を実践しました。
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)}
リクエストメッセージに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構築していければと思います。