From 8f4fdc2f8b6f07df4460e685a6fa942ef66a5ecb Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Sat, 2 May 2015 01:34:25 +1000 Subject: [PATCH] Added authenticated Coinbase REST support. --- README.md | 2 +- coinbase.go | 408 ++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 346 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 42ab224a..1e14ad22 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A cryptocurrency trading bot supporting multiple exchanges written in Golang. | BTCChina | Yes | Yes | No | | BTCE | Yes | NA | NA | | BTCMarkets | Yes | NA | NA | -| Coinbase | Yes (unauthenticated)| Yes | No| +| Coinbase | Yes | Yes | No| | Cryptsy | Yes | Yes | NA| | Huobi | Yes | Yes |No | ItBit | Yes | NA | NA | diff --git a/coinbase.go b/coinbase.go index 2cce34e7..c8110b8d 100644 --- a/coinbase.go +++ b/coinbase.go @@ -1,85 +1,94 @@ package main import ( - "log" - "time" + "bytes" + "errors" "fmt" - "strconv" + "log" "net/url" + "strconv" + "time" ) const ( - COINBASE_API_URL = "https://api.exchange.coinbase.com/" + COINBASE_API_URL = "https://api.exchange.coinbase.com/" COINBASE_API_VERISON = "0" - COINBASE_PRODUCTS = "products" - COINBASE_ORDERBOOK = "book" - COINBASE_TICKER = "ticker" - COINBASE_TRADES = "trades" - COINBASE_HISTORY = "candles" - COINBASE_STATS = "stats" - COINBASE_CURRENCIES = "currencies" + COINBASE_PRODUCTS = "products" + COINBASE_ORDERBOOK = "book" + COINBASE_TICKER = "ticker" + COINBASE_TRADES = "trades" + COINBASE_HISTORY = "candles" + COINBASE_STATS = "stats" + COINBASE_CURRENCIES = "currencies" + COINBASE_ACCOUNTS = "accounts" + COINBASE_LEDGER = "ledger" + COINBASE_HOLDS = "holds" + COINBASE_ORDERS = "orders" + COINBASE_FILLS = "fills" + COINBASE_TRANSFERS = "transfers" + COINBASE_REPORTS = "reports" ) type Coinbase struct { - Name string - Enabled bool - Verbose bool - Websocket bool - RESTPollingDelay time.Duration + Name string + Enabled bool + Verbose bool + Websocket bool + RESTPollingDelay time.Duration Password, APIKey, APISecret string - TakerFee, MakerFee float64 + TakerFee, MakerFee float64 } type CoinbaseTicker struct { - TradeID int64 `json:"trade_id"` - Price float64 `json:"price,string"` - Size float64 `json:"size,string"` - Time string `json:"time"` + TradeID int64 `json:"trade_id"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + Time string `json:"time"` } type CoinbaseProduct struct { - ID string `json:"id"` - BaseCurrency string `json:"base_currency"` - QuoteCurrency string `json:"quote_currency"` - BaseMinSize float64 `json:"base_min_size"` - BaseMaxSize int64 `json:"base_max_size"` + ID string `json:"id"` + BaseCurrency string `json:"base_currency"` + QuoteCurrency string `json:"quote_currency"` + BaseMinSize float64 `json:"base_min_size"` + BaseMaxSize int64 `json:"base_max_size"` QuoteIncrement float64 `json:"quote_increment"` - DisplayName string `json:"string"` + DisplayName string `json:"string"` } type CoinbaseOrderbook struct { - Asks [][]interface{} `json:"ask"` - Bids [][]interface{} `json:"bids"` - Sequence int64 `json:"sequence"` + Asks [][]interface{} `json:"ask"` + Bids [][]interface{} `json:"bids"` + Sequence int64 `json:"sequence"` } type CoinbaseTrade struct { - TradeID int64 `json:"trade_id"` - Price float64 `json:"price,string"` - Size float64 `json:"size,string"` - Time string `json:"time"` - Side string `json:"side"` + TradeID int64 `json:"trade_id"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + Time string `json:"time"` + Side string `json:"side"` } type CoinbaseStats struct { - Open float64 `json:"open,string"` - High float64 `json:"high,string"` - Low float64 `json:"low,string"` + Open float64 `json:"open,string"` + High float64 `json:"high,string"` + Low float64 `json:"low,string"` Volume float64 `json:"volume,string"` } type CoinbaseCurrency struct { - ID string - Name string + ID string + Name string MinSize float64 `json:"min_size,string"` } type CoinbaseHistory struct { - Time int64 - Low float64 - High float64 - Open float64 - Close float64 + Time int64 + Low float64 + High float64 + Open float64 + Close float64 Volume float64 } @@ -94,7 +103,7 @@ func (c *Coinbase) SetDefaults() { c.RESTPollingDelay = 10 } -func (c *Coinbase) GetName() (string) { +func (c *Coinbase) GetName() string { return c.Name } @@ -102,11 +111,11 @@ func (c *Coinbase) SetEnabled(enabled bool) { c.Enabled = enabled } -func (c *Coinbase) IsEnabled() (bool) { +func (c *Coinbase) IsEnabled() bool { return c.Enabled } -func (c *Coinbase) GetFee(maker bool) (float64) { +func (c *Coinbase) GetFee(maker bool) float64 { if maker { return c.MakerFee } else { @@ -138,12 +147,20 @@ func (c *Coinbase) Run() { func (c *Coinbase) SetAPIKeys(password, apiKey, apiSecret string) { c.Password = password c.APIKey = apiKey - c.APISecret = apiSecret + result, err := Base64Decode(apiSecret) + + if err != nil { + log.Printf("%s unable to decode secret key.", c.GetName()) + c.Enabled = false + return + } + + c.APISecret = string(result) } func (c *Coinbase) GetProducts() { products := []CoinbaseProduct{} - err := SendHTTPGetRequest(COINBASE_API_URL + COINBASE_PRODUCTS, true, &products) + err := SendHTTPGetRequest(COINBASE_API_URL+COINBASE_PRODUCTS, true, &products) if err != nil { log.Println(err) @@ -157,22 +174,22 @@ func (c *Coinbase) GetOrderbook(symbol string, level int) { path := "" if level > 0 { levelStr := strconv.Itoa(level) - path = fmt.Sprintf("%s/%s/%s?level=%s", COINBASE_API_URL + COINBASE_PRODUCTS, symbol, COINBASE_ORDERBOOK, levelStr) + path = fmt.Sprintf("%s/%s/%s?level=%s", COINBASE_API_URL+COINBASE_PRODUCTS, symbol, COINBASE_ORDERBOOK, levelStr) } else { - path = fmt.Sprintf("%s/%s/%s", COINBASE_API_URL + COINBASE_PRODUCTS, symbol, COINBASE_ORDERBOOK) + path = fmt.Sprintf("%s/%s/%s", COINBASE_API_URL+COINBASE_PRODUCTS, symbol, COINBASE_ORDERBOOK) } - + err := SendHTTPGetRequest(path, true, &orderbook) if err != nil { log.Println(err) } log.Println(orderbook) -} +} -func (c *Coinbase) GetTicker(symbol string) (CoinbaseTicker) { +func (c *Coinbase) GetTicker(symbol string) CoinbaseTicker { ticker := CoinbaseTicker{} - path := fmt.Sprintf("%s/%s/%s", COINBASE_API_URL + COINBASE_PRODUCTS, symbol, COINBASE_TICKER) + path := fmt.Sprintf("%s/%s/%s", COINBASE_API_URL+COINBASE_PRODUCTS, symbol, COINBASE_TICKER) err := SendHTTPGetRequest(path, true, &ticker) if err != nil { @@ -184,7 +201,7 @@ func (c *Coinbase) GetTicker(symbol string) (CoinbaseTicker) { func (c *Coinbase) GetTrades(symbol string) { trades := []CoinbaseTrade{} - path := fmt.Sprintf("%s/%s/%s", COINBASE_API_URL + COINBASE_PRODUCTS, symbol, COINBASE_TRADES) + path := fmt.Sprintf("%s/%s/%s", COINBASE_API_URL+COINBASE_PRODUCTS, symbol, COINBASE_TRADES) err := SendHTTPGetRequest(path, true, &trades) if err != nil { @@ -209,10 +226,10 @@ func (c *Coinbase) GetHistoricRates(symbol string, start, end, granularity int64 values.Set("granularity", strconv.FormatInt(granularity, 10)) } - path := fmt.Sprintf("%s/%s/%s", COINBASE_API_URL + COINBASE_PRODUCTS, symbol, COINBASE_HISTORY) + path := fmt.Sprintf("%s/%s/%s", COINBASE_API_URL+COINBASE_PRODUCTS, symbol, COINBASE_HISTORY) encoded := values.Encode() - if (len(encoded) > 0) { + if len(encoded) > 0 { path += encoded } @@ -224,9 +241,9 @@ func (c *Coinbase) GetHistoricRates(symbol string, start, end, granularity int64 log.Println(history) } -func (c *Coinbase) GetStats(symbol string) (CoinbaseStats) { +func (c *Coinbase) GetStats(symbol string) CoinbaseStats { stats := CoinbaseStats{} - path := fmt.Sprintf("%s/%s/%s", COINBASE_API_URL + COINBASE_PRODUCTS, symbol, COINBASE_STATS) + path := fmt.Sprintf("%s/%s/%s", COINBASE_API_URL+COINBASE_PRODUCTS, symbol, COINBASE_STATS) err := SendHTTPGetRequest(path, true, &stats) if err != nil { @@ -238,10 +255,275 @@ func (c *Coinbase) GetStats(symbol string) (CoinbaseStats) { func (c *Coinbase) GetCurrencies() { currencies := []CoinbaseCurrency{} - err := SendHTTPGetRequest(COINBASE_API_URL + COINBASE_CURRENCIES, true, ¤cies) + err := SendHTTPGetRequest(COINBASE_API_URL+COINBASE_CURRENCIES, true, ¤cies) if err != nil { log.Println(err) } log.Println(currencies) -} \ No newline at end of file +} + +type CoinbaseAccountResponse struct { + ID string `json:"id"` + Balance float64 `json:"balance,string"` + Hold float64 `json:"hold,string"` + Available float64 `json:"available,string"` + Currency string `json:"currency"` +} + +func (c *Coinbase) GetAccounts() { + resp := []CoinbaseAccountResponse{} + err := c.SendAuthenticatedHTTPRequest("GET", COINBASE_API_URL+COINBASE_ACCOUNTS, nil, &resp) + if err != nil { + log.Println(err) + } +} + +func (c *Coinbase) GetAccount(account string) { + resp := CoinbaseAccountResponse{} + path := fmt.Sprintf("%s/%s", COINBASE_ACCOUNTS, account) + err := c.SendAuthenticatedHTTPRequest("GET", COINBASE_API_URL+path, nil, &resp) + if err != nil { + log.Println(err) + } +} + +type CoinbaseAccountLedgerResponse struct { + ID string `json:"id"` + CreatedAt string `json:"created_at"` + Amount float64 `json:"amount,string"` + Balance float64 `json:"balance,string"` + Type string `json:"type"` + details interface{} `json:"details"` +} + +func (c *Coinbase) GetAccountHistory(accountID string) { + resp := []CoinbaseAccountLedgerResponse{} + path := fmt.Sprintf("%s/%s/%s", COINBASE_ACCOUNTS, accountID, COINBASE_LEDGER) + err := c.SendAuthenticatedHTTPRequest("GET", COINBASE_API_URL+path, nil, &resp) + if err != nil { + log.Println(err) + } +} + +type CoinbaseAccountHolds struct { + ID string `json:"id"` + AccountID string `json:"account_id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Amount float64 `json:"amount,string"` + Type string `json:"type"` + Reference string `json:"ref"` +} + +func (c *Coinbase) GetHolds(accountID string) { + resp := []CoinbaseAccountHolds{} + path := fmt.Sprintf("%s/%s/%s", COINBASE_ACCOUNTS, accountID, COINBASE_HOLDS) + err := c.SendAuthenticatedHTTPRequest("GET", COINBASE_API_URL+path, nil, &resp) + if err != nil { + log.Println(err) + } +} + +func (c *Coinbase) PlaceOrder(clientRef string, price, amount float64, side string, productID, stp string) (string, error) { + request := make(map[string]interface{}) + + if clientRef != "" { + request["client_oid"] = clientRef + } + + request["price"] = strconv.FormatFloat(price, 'f', 2, 64) + request["size"] = strconv.FormatFloat(amount, 'f', 8, 64) + request["side"] = side + request["product_id"] = productID + + if stp != "" { + request["stp"] = stp + } + + type OrderResponse struct { + ID string `json:"id"` + } + + resp := OrderResponse{} + err := c.SendAuthenticatedHTTPRequest("POST", COINBASE_API_URL+COINBASE_ORDERS, request, &resp) + if err != nil { + return "", err + } + + return resp.ID, nil +} + +func (c *Coinbase) CancelOrder(orderID string) { + path := fmt.Sprintf("%s/%s", COINBASE_ORDERS, orderID) + err := c.SendAuthenticatedHTTPRequest("DELETE", COINBASE_API_URL+path, nil, nil) + if err != nil { + log.Println(err) + } +} + +type CoinbaseOrdersResponse struct { + ID string `json:"id"` + Size float64 `json:"size,string"` + Price float64 `json:"price,string"` + ProductID string `json:"product_id"` + Status string `json:"status"` + FilledSize float64 `json:"filled_size,string"` + FillFees float64 `json:"fill_fees,string"` + Settled bool `json:"settled"` + Side string `json:"side"` + CreatedAt string `json:"created_at"` +} + +func (c *Coinbase) GetOrders(params url.Values) { + path := COINBASE_API_URL + COINBASE_ORDERS + + if len(params) > 0 { + path += "?" + params.Encode() + } + + resp := []CoinbaseOrdersResponse{} + err := c.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) + if err != nil { + log.Println(err) + } +} + +type CoinbaseOrderResponse struct { + ID string `json:"id"` + Size float64 `json:"size,string"` + Price float64 `json:"price,string"` + DoneReason string `json:"done_reason"` + Status string `json:"status"` + Settled bool `json:"settled"` + FilledSize float64 `json:"filled_size,string"` + ProductID string `json:"product_id"` + FillFees float64 `json:"fill_fees,string"` + Side string `json:"side"` + CreatedAt string `json:"created_at"` + DoneAt string `json:"done_at"` +} + +func (c *Coinbase) GetOrder(orderID string) { + path := fmt.Sprintf("%s/%s", COINBASE_ORDERS, orderID) + resp := CoinbaseOrderResponse{} + err := c.SendAuthenticatedHTTPRequest("GET", COINBASE_API_URL+path, nil, &resp) + if err != nil { + log.Println(err) + } +} + +type CoinbaseFillResponse struct { + TradeID int `json:"trade_id"` + ProductID string `json:"product_id"` + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + OrderID string `json:"order_id"` + CreatedAt string `json:"created_at"` + Liquidity string `json:"liquidity"` + Fee float64 `json:"fee,string"` + Settled bool `json:"settled"` + Side string `json:"side"` +} + +func (c *Coinbase) GetFills(params url.Values) { + path := COINBASE_API_URL + COINBASE_FILLS + + if len(params) > 0 { + path += "?" + params.Encode() + } + + resp := []CoinbaseFillResponse{} + err := c.SendAuthenticatedHTTPRequest("GET", path, nil, &resp) + if err != nil { + log.Println(err) + } +} + +func (c *Coinbase) Transfer(transferType string, amount float64, accountID string) { + request := make(map[string]interface{}) + request["type"] = transferType + request["amount"] = strconv.FormatFloat(amount, 'f', 8, 64) + request["coinbase_account_id"] = accountID + + err := c.SendAuthenticatedHTTPRequest("POST", COINBASE_API_URL+COINBASE_TRANSFERS, request, nil) + if err != nil { + log.Println(err) + } +} + +type CoinbaseReportResponse struct { + ID string `json:"id"` + Type string `json:"type"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + CompletedAt string `json:"completed_at"` + ExpiresAt string `json:"expires_at"` + FileURL string `json:"file_url"` + Params struct { + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + } `json:params"` +} + +func (c *Coinbase) GetReport(reportType, startDate, endDate string) { + request := make(map[string]interface{}) + request["type"] = reportType + request["start_date"] = startDate + request["end_date"] = endDate + + resp := CoinbaseReportResponse{} + err := c.SendAuthenticatedHTTPRequest("POST", COINBASE_API_URL+COINBASE_REPORTS, request, &resp) + if err != nil { + log.Println(err) + } +} + +func (c *Coinbase) GetReportStatus(reportID string) { + path := fmt.Sprintf("%s/%s", COINBASE_REPORTS, reportID) + resp := CoinbaseReportResponse{} + err := c.SendAuthenticatedHTTPRequest("POST", COINBASE_API_URL+path, nil, &resp) + if err != nil { + log.Println(err) + } +} + +func (c *Coinbase) SendAuthenticatedHTTPRequest(method, path string, params map[string]interface{}, result interface{}) (err error) { + timestamp := strconv.FormatInt(time.Now().UnixNano(), 10)[0:13] + payload := []byte("") + + if params != nil { + payload, err = JSONEncode(params) + + if err != nil { + return errors.New("SendAuthenticatedHTTPRequest: Unable to JSON request") + } + + if c.Verbose { + log.Printf("Request JSON: %s\n", payload) + } + } + + message := timestamp + method + path + string(payload) + hmac := GetHMAC(HASH_SHA256, []byte(message), []byte(c.APISecret)) + headers := make(map[string]string) + headers["CB-ACCESS-SIGN"] = Base64Encode([]byte(hmac)) + headers["CB-ACCESS-TIMESTAMP"] = timestamp + headers["CB-ACCESS-KEY"] = c.APIKey + headers["CB-ACCESS-PASSPHRASE"] = c.Password + headers["Content-Type"] = "application/json" + + resp, err := SendHTTPRequest(method, COINBASE_API_URL+path, headers, bytes.NewBuffer(payload)) + + if c.Verbose { + log.Printf("Recieved raw: \n%s\n", resp) + } + + err = JSONDecode([]byte(resp), &result) + + if err != nil { + return errors.New("Unable to JSON Unmarshal response.") + } + + return nil +}