mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
Added authenticated Coinbase REST support.
This commit is contained in:
@@ -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 |
|
||||
|
||||
408
coinbase.go
408
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user