bbgo_origin/pkg/exchange/max/maxapi/restapi.go

442 lines
11 KiB
Go
Raw Normal View History

2020-10-02 02:10:59 +00:00
package max
import (
"bytes"
2022-04-19 05:52:10 +00:00
"context"
2020-10-02 02:10:59 +00:00
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net"
2020-10-02 02:10:59 +00:00
"net/http"
"net/http/httputil"
2020-10-02 02:10:59 +00:00
"net/url"
"regexp"
"strings"
2020-10-02 02:10:59 +00:00
"sync/atomic"
"time"
2022-04-19 04:10:15 +00:00
"github.com/c9s/requestgen"
2021-06-07 15:11:53 +00:00
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
2022-04-19 04:10:15 +00:00
"github.com/c9s/bbgo/pkg/util"
"github.com/c9s/bbgo/pkg/version"
2020-10-02 02:10:59 +00:00
)
const (
// ProductionAPIURL is the official MAX API v2 Endpoint
ProductionAPIURL = "https://max-api.maicoin.com/api/v2"
2021-02-22 07:16:12 +00:00
UserAgent = "bbgo/" + version.Version
2020-10-02 02:10:59 +00:00
2021-02-22 07:16:12 +00:00
defaultHTTPTimeout = time.Second * 30
// 2018-09-01 08:00:00 +0800 CST
TimestampSince = 1535760000
2020-10-02 02:10:59 +00:00
)
var debugRequestDump = false
var debugMaxRequestPayload = false
var addUserAgentHeader = true
var httpTransportMaxIdleConnsPerHost = http.DefaultMaxIdleConnsPerHost
var httpTransportMaxIdleConns = 100
var httpTransportIdleConnTimeout = 90 * time.Second
func init() {
debugMaxRequestPayload, _ = util.GetEnvVarBool("DEBUG_MAX_REQUEST_PAYLOAD")
debugRequestDump, _ = util.GetEnvVarBool("DEBUG_MAX_REQUEST")
addUserAgentHeader, _ = util.GetEnvVarBool("DISABLE_MAX_USER_AGENT_HEADER")
if val, ok := util.GetEnvVarInt("HTTP_TRANSPORT_MAX_IDLE_CONNS_PER_HOST"); ok {
httpTransportMaxIdleConnsPerHost = val
}
if val, ok := util.GetEnvVarInt("HTTP_TRANSPORT_MAX_IDLE_CONNS"); ok {
httpTransportMaxIdleConns = val
}
if val, ok := util.GetEnvVarDuration("HTTP_TRANSPORT_IDLE_CONN_TIMEOUT"); ok {
httpTransportIdleConnTimeout = val
}
}
2020-10-02 02:10:59 +00:00
var logger = log.WithField("exchange", "max")
var htmlTagPattern = regexp.MustCompile("<[/]?[a-zA-Z-]+.*?>")
// The following variables are used for nonce.
// timeOffset is used for nonce
var timeOffset int64 = 0
// serverTimestamp is used for storing the server timestamp, default to Now
var serverTimestamp = time.Now().Unix()
// reqCount is used for nonce, this variable counts the API request count.
var reqCount int64 = 1
2020-10-02 02:10:59 +00:00
type RestClient struct {
client *http.Client
BaseURL *url.URL
// Authentication
APIKey string
APISecret string
AccountService *AccountService
PublicService *PublicService
TradeService *TradeService
OrderService *OrderService
RewardService *RewardService
2021-05-11 14:35:31 +00:00
WithdrawalService *WithdrawalService
2020-10-02 02:10:59 +00:00
// OrderBookService *OrderBookService
// MaxTokenService *MaxTokenService
// MaxKLineService *KLineService
// CreditService *CreditService
}
func NewRestClientWithHttpClient(baseURL string, httpClient *http.Client) *RestClient {
u, err := url.Parse(baseURL)
if err != nil {
panic(err)
}
var client = &RestClient{
client: httpClient,
BaseURL: u,
}
client.AccountService = &AccountService{client}
client.TradeService = &TradeService{client}
client.PublicService = &PublicService{client}
client.OrderService = &OrderService{client}
2021-02-22 10:45:44 +00:00
client.RewardService = &RewardService{client}
2021-05-11 14:35:31 +00:00
client.WithdrawalService = &WithdrawalService{client}
2021-02-22 10:45:44 +00:00
2020-10-02 02:10:59 +00:00
// client.MaxTokenService = &MaxTokenService{client}
client.initNonce()
return client
}
func NewRestClient(baseURL string) *RestClient {
// create an isolated http transport rather than the default one
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: httpTransportMaxIdleConns,
MaxIdleConnsPerHost: httpTransportMaxIdleConnsPerHost,
IdleConnTimeout: httpTransportIdleConnTimeout,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := &http.Client{
Timeout: defaultHTTPTimeout,
Transport: transport,
}
return NewRestClientWithHttpClient(baseURL, client)
2020-10-02 02:10:59 +00:00
}
// Auth sets api key and secret for usage is requests that requires authentication.
func (c *RestClient) Auth(key string, secret string) *RestClient {
c.APIKey = key
c.APISecret = secret
return c
}
func (c *RestClient) initNonce() {
var clientTime = time.Now()
var err error
serverTimestamp, err = c.PublicService.Timestamp()
if err != nil {
logger.WithError(err).Panic("failed to sync timestamp with max")
2020-10-02 02:10:59 +00:00
}
timeOffset = serverTimestamp - clientTime.Unix()
logger.Infof("loaded max server timestamp: %d offset=%d", serverTimestamp, timeOffset)
2020-10-02 02:10:59 +00:00
}
func (c *RestClient) getNonce() int64 {
// nonce 是以正整數表示的時間戳記,代表了從 Unix epoch 到當前時間所經過的毫秒數(ms)。
// nonce 與伺服器的時間差不得超過正負30秒每個 nonce 只能使用一次。
2020-10-02 02:10:59 +00:00
var seconds = time.Now().Unix()
var rc = atomic.AddInt64(&reqCount, 1)
return (seconds+timeOffset)*1000 - 1 + int64(math.Mod(float64(rc), 1000.0))
2020-10-02 02:10:59 +00:00
}
// NewRequest create new API request. Relative url can be provided in refURL.
2022-04-19 04:10:15 +00:00
func (c *RestClient) NewRequest(ctx context.Context, method string, refURL string, params url.Values, payload interface{}) (*http.Request, error) {
2020-10-02 02:10:59 +00:00
rel, err := url.Parse(refURL)
if err != nil {
return nil, err
}
2022-04-19 05:52:10 +00:00
2020-10-02 02:10:59 +00:00
if params != nil {
rel.RawQuery = params.Encode()
}
2022-04-19 05:52:10 +00:00
2020-10-02 02:10:59 +00:00
var req *http.Request
u := c.BaseURL.ResolveReference(rel)
2022-04-19 04:10:15 +00:00
body, err := castPayload(payload)
if err != nil {
return nil, err
}
2020-10-02 02:10:59 +00:00
req, err = http.NewRequest(method, u.String(), bytes.NewReader(body))
if err != nil {
return nil, err
}
2022-04-19 05:52:10 +00:00
req = req.WithContext(ctx)
if addUserAgentHeader {
req.Header.Add("User-Agent", UserAgent)
}
2022-04-19 04:10:15 +00:00
2020-10-02 02:10:59 +00:00
return req, nil
}
2022-04-19 05:52:10 +00:00
func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, m string, refURL string, params url.Values, payload interface{}) (*http.Request, error) {
return c.newAuthenticatedRequest(ctx, m, refURL, params, payload, nil)
}
// newAuthenticatedRequest creates new http request for authenticated routes.
func (c *RestClient) newAuthenticatedRequest(ctx context.Context, m string, refURL string, params url.Values, data interface{}, rel *url.URL) (*http.Request, error) {
var err error
if rel == nil {
rel, err = url.Parse(refURL)
if err != nil {
return nil, err
}
2020-10-02 02:10:59 +00:00
}
2020-10-06 10:44:56 +00:00
var p []byte
var payload map[string]interface{}
2020-10-06 10:44:56 +00:00
switch d := data.(type) {
case nil:
payload = map[string]interface{}{
"nonce": c.getNonce(),
"path": c.BaseURL.ResolveReference(rel).Path,
}
2020-10-06 10:44:56 +00:00
case map[string]interface{}:
payload = map[string]interface{}{
2020-10-06 10:44:56 +00:00
"nonce": c.getNonce(),
"path": c.BaseURL.ResolveReference(rel).Path,
}
for k, v := range d {
payload[k] = v
}
}
2020-10-06 10:44:56 +00:00
for k, vs := range params {
k = strings.TrimSuffix(k, "[]")
if len(vs) == 1 {
payload[k] = vs[0]
} else {
payload[k] = vs
}
}
p, err = castPayload(payload)
if err != nil {
return nil, err
2020-10-02 02:10:59 +00:00
}
if debugMaxRequestPayload {
log.Infof("request payload: %s", p)
}
2020-10-02 02:10:59 +00:00
if err != nil {
return nil, err
}
2020-10-08 14:03:25 +00:00
if len(c.APIKey) == 0 {
return nil, errors.New("empty api key")
}
if len(c.APISecret) == 0 {
return nil, errors.New("empty api secret")
}
2022-04-19 05:52:10 +00:00
req, err := c.NewRequest(ctx, m, refURL, params, p)
2020-10-02 02:10:59 +00:00
if err != nil {
return nil, err
}
encoded := base64.StdEncoding.EncodeToString(p)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-MAX-ACCESSKEY", c.APIKey)
req.Header.Add("X-MAX-PAYLOAD", encoded)
req.Header.Add("X-MAX-SIGNATURE", signPayload(encoded, c.APISecret))
if debugRequestDump {
2021-06-08 18:18:37 +00:00
dump, err2 := httputil.DumpRequestOut(req, true)
if err2 != nil {
log.Errorf("dump request error: %v", err2)
} else {
fmt.Printf("REQUEST:\n%s", dump)
}
}
2020-10-02 02:10:59 +00:00
return req, nil
}
func signPayload(payload string, secret string) string {
var sig = hmac.New(sha256.New, []byte(secret))
_, err := sig.Write([]byte(payload))
if err != nil {
return ""
}
return hex.EncodeToString(sig.Sum(nil))
}
func (c *RestClient) Do(req *http.Request) (resp *http.Response, err error) {
return c.client.Do(req)
}
2022-04-19 04:10:15 +00:00
// SendRequest sends the request to the API server and handle the response
func (c *RestClient) SendRequest(req *http.Request) (*requestgen.Response, error) {
2021-06-08 18:21:32 +00:00
resp, err := c.client.Do(req)
2020-10-02 02:10:59 +00:00
if err != nil {
return nil, err
}
// newResponse reads the response body and return a new Response object
2022-04-19 04:10:15 +00:00
response, err := requestgen.NewResponse(resp)
2020-10-02 02:10:59 +00:00
if err != nil {
return response, err
}
// Check error, if there is an error, return the ErrorResponse struct type
2021-02-06 01:16:43 +00:00
if response.IsError() {
2021-05-23 04:11:27 +00:00
errorResponse, err := ToErrorResponse(response)
2020-10-02 02:10:59 +00:00
if err != nil {
return response, err
}
return response, errorResponse
}
return response, nil
}
2022-04-19 04:10:15 +00:00
func (c *RestClient) sendAuthenticatedRequest(m string, refURL string, data map[string]interface{}) (*requestgen.Response, error) {
2022-04-19 05:52:10 +00:00
req, err := c.newAuthenticatedRequest(nil, m, refURL, nil, data, nil)
2020-10-02 02:10:59 +00:00
if err != nil {
return nil, err
}
2022-04-19 04:10:15 +00:00
response, err := c.SendRequest(req)
2020-10-02 02:10:59 +00:00
if err != nil {
return nil, err
}
return response, err
}
// get sends GET http request to the api endpoint, the urlPath must start with a slash '/'
func (c *RestClient) get(urlPath string, values url.Values) ([]byte, error) {
var reqURL = c.BaseURL.String() + urlPath
// Create request
req, err := http.NewRequest("GET", reqURL, nil)
if err != nil {
return nil, fmt.Errorf("could not init request: %s", err.Error())
}
req.URL.RawQuery = values.Encode()
req.Header.Add("User-Agent", UserAgent)
// Execute request
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("could not execute request: %s", err.Error())
}
defer resp.Body.Close()
// Load request
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("could not read response: %s", err.Error())
}
return body, nil
}
// ErrorResponse is the custom error type that is returned if the API returns an
// error.
type ErrorField struct {
Code int `json:"code"`
Message string `json:"message"`
}
type ErrorResponse struct {
2022-04-19 04:10:15 +00:00
*requestgen.Response
2020-10-02 02:10:59 +00:00
Err ErrorField `json:"error"`
}
func (r *ErrorResponse) Error() string {
return fmt.Sprintf("%s %s: %d %d %s",
r.Response.Response.Request.Method,
r.Response.Response.Request.URL.String(),
r.Response.Response.StatusCode,
r.Err.Code,
r.Err.Message,
)
}
2021-05-23 04:11:27 +00:00
// ToErrorResponse tries to convert/parse the server response to the standard Error interface object
2022-04-19 04:10:15 +00:00
func ToErrorResponse(response *requestgen.Response) (errorResponse *ErrorResponse, err error) {
2020-10-02 02:10:59 +00:00
errorResponse = &ErrorResponse{Response: response}
contentType := response.Header.Get("content-type")
switch contentType {
case "text/json", "application/json", "application/json; charset=utf-8":
var err = response.DecodeJSON(errorResponse)
if err != nil {
return errorResponse, errors.Wrapf(err, "failed to decode json for response: %d %s", response.StatusCode, string(response.Body))
}
return errorResponse, nil
case "text/html":
// convert 5xx error from the HTML page to the ErrorResponse
errorResponse.Err.Message = htmlTagPattern.ReplaceAllLiteralString(string(response.Body), "")
return errorResponse, nil
case "text/plain":
errorResponse.Err.Message = string(response.Body)
return errorResponse, nil
2020-10-02 02:10:59 +00:00
}
return errorResponse, fmt.Errorf("unexpected response content type %s", contentType)
}
2022-04-19 04:10:15 +00:00
func castPayload(payload interface{}) ([]byte, error) {
if payload == nil {
return nil, nil
}
2022-04-19 04:10:15 +00:00
switch v := payload.(type) {
case string:
return []byte(v), nil
2022-04-19 04:10:15 +00:00
case []byte:
return v, nil
2022-04-19 04:10:15 +00:00
}
body, err := json.Marshal(payload)
return body, err
2022-04-19 04:10:15 +00:00
}