bbgo_origin/pkg/exchange/ftx/rest.go

275 lines
5.7 KiB
Go
Raw Normal View History

2021-02-08 10:58:47 +00:00
package ftx
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/util"
)
2021-08-07 06:50:12 +00:00
type transferRequest struct {
*restRequest
}
type TransferPayload struct {
Coin string
Size float64
Source string
Destination string
}
func (r *restRequest) Transfer(ctx context.Context, p TransferPayload) (transferResponse, error) {
resp, err := r.
Method("POST").
ReferenceURL("api/subaccounts/transfer").
Payloads(map[string]interface{}{
"coin": p.Coin,
"size": p.Size,
"source": p.Source,
"destination": p.Destination,
}).
DoAuthenticatedRequest(ctx)
if err != nil {
return transferResponse{}, err
}
var t transferResponse
if err := json.Unmarshal(resp.Body, &t); err != nil {
return transferResponse{}, fmt.Errorf("failed to unmarshal transfer response body to json: %w", err)
}
return t, nil
}
2021-02-08 10:58:47 +00:00
type restRequest struct {
2021-03-21 12:17:41 +00:00
*walletRequest
2021-03-07 04:47:11 +00:00
*orderRequest
2021-03-18 00:33:14 +00:00
*accountRequest
2021-03-21 02:51:17 +00:00
*marketRequest
2021-03-25 08:57:54 +00:00
*fillsRequest
2021-08-07 06:50:12 +00:00
*transferRequest
2021-02-08 10:58:47 +00:00
key, secret string
// Optional sub-account name
sub string
c *http.Client
baseURL *url.URL
refURL string
// http method, e.g., GET or POST
m string
2021-03-23 14:25:57 +00:00
// query string
q map[string]string
2021-02-08 10:58:47 +00:00
// payload
p map[string]interface{}
// object id
id string
2021-02-08 10:58:47 +00:00
}
func newRestRequest(c *http.Client, baseURL *url.URL) *restRequest {
r := &restRequest{
c: c,
baseURL: baseURL,
2021-03-23 14:25:57 +00:00
q: make(map[string]string),
p: make(map[string]interface{}),
2021-02-08 10:58:47 +00:00
}
2021-03-25 08:57:54 +00:00
r.fillsRequest = &fillsRequest{restRequest: r}
2021-03-21 02:51:17 +00:00
r.marketRequest = &marketRequest{restRequest: r}
2021-03-18 00:33:14 +00:00
r.accountRequest = &accountRequest{restRequest: r}
2021-03-21 12:17:41 +00:00
r.walletRequest = &walletRequest{restRequest: r}
2021-03-07 04:47:11 +00:00
r.orderRequest = &orderRequest{restRequest: r}
2021-02-08 10:58:47 +00:00
return r
}
func (r *restRequest) Auth(key, secret string) *restRequest {
r.key = key
r.secret = secret
return r
}
func (r *restRequest) SubAccount(subAccount string) *restRequest {
r.sub = subAccount
return r
}
func (r *restRequest) Method(method string) *restRequest {
r.m = method
return r
}
func (r *restRequest) ReferenceURL(refURL string) *restRequest {
r.refURL = refURL
return r
}
func (r *restRequest) buildURL() (*url.URL, error) {
u := r.refURL
if len(r.id) > 0 {
u = u + "/" + r.id
}
refURL, err := url.Parse(u)
2021-02-08 10:58:47 +00:00
if err != nil {
return nil, err
}
2021-02-08 10:58:47 +00:00
return r.baseURL.ResolveReference(refURL), nil
}
2021-05-24 06:18:40 +00:00
func (r *restRequest) ID(id string) *restRequest {
r.id = id
return r
}
2021-02-08 10:58:47 +00:00
func (r *restRequest) Payloads(payloads map[string]interface{}) *restRequest {
for k, v := range payloads {
r.p[k] = v
}
return r
}
2021-03-23 14:25:57 +00:00
func (r *restRequest) Query(query map[string]string) *restRequest {
for k, v := range query {
r.q[k] = v
}
return r
}
2021-02-08 10:58:47 +00:00
func (r *restRequest) DoAuthenticatedRequest(ctx context.Context) (*util.Response, error) {
req, err := r.newAuthenticatedRequest(ctx)
if err != nil {
return nil, err
}
return r.sendRequest(req)
}
func (r *restRequest) newAuthenticatedRequest(ctx context.Context) (*http.Request, error) {
u, err := r.buildURL()
if err != nil {
return nil, err
}
var jsonPayload []byte
if len(r.p) > 0 {
var err2 error
jsonPayload, err2 = json.Marshal(r.p)
if err2 != nil {
return nil, fmt.Errorf("can't marshal payload map to json: %w", err2)
}
}
req, err := http.NewRequestWithContext(ctx, r.m, u.String(), bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, err
}
2021-03-23 14:25:57 +00:00
ts := strconv.FormatInt(timestamp(), 10)
p := fmt.Sprintf("%s%s%s", ts, r.m, u.Path)
if len(r.q) > 0 {
rq := u.Query()
for k, v := range r.q {
rq.Add(k, v)
}
req.URL.RawQuery = rq.Encode()
p += "?" + req.URL.RawQuery
}
if len(jsonPayload) > 0 {
p += string(jsonPayload)
}
signature := sign(r.secret, p)
2021-02-08 10:58:47 +00:00
req.Header.Set("Content-Type", "application/json")
req.Header.Set("FTX-KEY", r.key)
req.Header.Set("FTX-SIGN", signature)
req.Header.Set("FTX-TS", ts)
if r.sub != "" {
req.Header.Set("FTX-SUBACCOUNT", r.sub)
}
return req, nil
}
func sign(secret, body string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(body))
return hex.EncodeToString(mac.Sum(nil))
}
func timestamp() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}
func (r *restRequest) sendRequest(req *http.Request) (*util.Response, error) {
resp, err := r.c.Do(req)
if err != nil {
return nil, err
}
// newResponse reads the response body and return a new Response object
response, err := util.NewResponse(resp)
if err != nil {
return response, err
}
// Check error, if there is an error, return the ErrorResponse struct type
if response.IsError() {
errorResponse, err := toErrorResponse(response)
if err != nil {
return response, err
}
return response, errorResponse
}
return response, nil
}
type ErrorResponse struct {
*util.Response
IsSuccess bool `json:"Success"`
ErrorString string `json:"error,omitempty"`
}
func (r *ErrorResponse) Error() string {
return fmt.Sprintf("%s %s %d, Success: %t, err: %s",
r.Response.Request.Method,
r.Response.Request.URL.String(),
r.Response.StatusCode,
r.IsSuccess,
r.ErrorString,
)
}
func toErrorResponse(response *util.Response) (*ErrorResponse, error) {
errorResponse := &ErrorResponse{Response: response}
if response.IsJSON() {
var err = response.DecodeJSON(errorResponse)
if err != nil {
return nil, errors.Wrapf(err, "failed to decode json for response: %d %s", response.StatusCode, string(response.Body))
}
if errorResponse.IsSuccess {
return nil, fmt.Errorf("response.Success should be false")
}
return errorResponse, nil
}
return errorResponse, fmt.Errorf("unexpected response content type %s", response.Header.Get("content-type"))
}