diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db1f454 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/config_dev.json +/config_prod.json +/.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 4ba0f78..53365e4 100644 --- a/README.md +++ b/README.md @@ -1 +1,173 @@ -# kucoin-api-demo +# Test Tool for KuCoin API + +## Choose environment + +| Environment | BaseUri | +| ------------ | ------------------------------------------------------------ | +| *Production* | `https://api.kucoin.com(DEFAULT)` `https://openapi-v2.kucoin.com` `https://api.kcs.top` | +| *Sandbox* | `https://openapi-sandbox.kucoin.com` | + +## Example + +* First,you should wirte your api information into config.json. + + ```json + { + "ApiBaseURI":"", + "ApiKey": "", + "ApiSecret": "", + "ApiPassphrase": "", + "ApiSkipVerifyTls": false + } + ``` + +* If not, you can run this script and follow the guide,then input your api information, such as: + + ```shell + # please input api base URI,such as:https://api.kucoin.com + % https://api.kucoin.com + # please input your api key... + % input your api key + # please input your api secret... + % input your api secret + # please input your api passphrase... + % input your api passphrase + ``` + +* input the api name and parameter information to choose api which you want to test. + + ```shell + # please input the api name which you want to test + % Accounts + # please input the api parameters,use ',' to spilt parameters. + # if it is no parameter api,just enter to skip... + % BTC,main + ``` + +* Then,it will show the request and response body to you. + +## Api map list + +
+Account + +| API Name | Request | Parameter | Parameter Example | Description | +| -------- | -------- | -------- | -------- | -------- | +| CreateAccount | POST /api/v1/accounts | type, currency | main,BTC | https://docs.kucoin.com/#create-an-account | +| Accounts | GET /api/v1/accounts | currency,type | BTC,main | https://docs.kucoin.com/#list-accounts | +| Account | GET /api/v1/accounts/{accountId} | accountId | {account ID} | https://docs.kucoin.com/#get-an-account | +| SubAccountUsers | GET /api/v1/sub/user | | | https://docs.kucoin.com/#get-user-info-of-all-sub-accounts | +| SubAccounts | GET /api/v1/sub-accounts | | | https://docs.kucoin.com/#get-the-aggregated-balance-of-all-sub-accounts | +| SubAccount | GET /api/v1/sub-accounts/{subUserId} | subUserId |{sub-account ID} | https://docs.kucoin.com/#get-account-balance-of-a-sub-account | +| AccountLedgers | GET /api/v1/accounts/{accountId}/ledgers | accountId |{account ID} | https://docs.kucoin.com/#get-account-ledgers | +| AccountHolds | GET /api/v1/accounts/{accountId}/holds | accountId | {account ID} | https://docs.kucoin.com/#get-holds | +| InnerTransferV2 | POST /api/v2/accounts/inner-transfer | Currency,from,to,amount | KCS,main,trade,2 | https://docs.kucoin.com/#inner-transfer | + +
+ +
+Deposit + +| API Name | Request | Parameter | Parameter Example | Description | +| -------- | -------- | -------- | -------- | -------- | +| CreateDepositAddress | POST /api/v1/deposit-addresses | currency | BTC | https://docs.kucoin.com/#create-deposit-address | +| DepositAddresses | GET /api/v1/deposit-addresses | currency | BTC | https://docs.kucoin.com/#get-deposit-address | +| V1Deposits | GET /api/v1/hist-deposits | currency | KCS | https://docs.kucoin.com/#get-v1-historical-deposits-list | +| Deposits | GET /api/v1/deposits | currency | KCS | https://docs.kucoin.com/#get-deposit-list | + +
+ +
+Fill + +| API Name | Request | Parameter | Parameter Example | Description | +| -------- | -------- | -------- | -------- | -------- | +| Fills | GET /api/v1/fills | tradeType | TRADE |https://docs.kucoin.com/#list-fills| +| RecentFills | GET /api/v1/limit/fills | | |https://docs.kucoin.com/#recent-fills| + +
+ +
+Order + +| API Name | Request | Parameter | Parameter Example | Description | +| -------- | -------- | -------- | -------- | -------- | +| CreateOrder | POST /api/v1/orders | side,symbol,price,size | sell,BTC-USDT,7330,0.1 |https://docs.kucoin.com/#place-a-new-order| +| CreateMultiOrder | POST /api/v1/orders/multi | symbol,side,price,size | BTC-USDT,sell,7350,0.01 |https://docs.kucoin.com/#place-bulk-orders| +| CancelOrder | DELETE /api/v1/orders/{orderId} | orderId | {order ID} |https://docs.kucoin.com/#cancel-an-order| +| CancelOrders | DELETE /api/v1/orders | symbol | BTC-USDT |https://docs.kucoin.com/#cancel-all-orders| +| Orders | GET /api/v1/orders | symbol | BTC-USDT,sell |https://docs.kucoin.com/#list-orders| +| Order | GET /api/v1/orders/{order-id} | orderId | {order ID} |https://docs.kucoin.com/#get-an-order| +| RecentOrders | GET /api/v1/limit/orders | | |https://docs.kucoin.com/#recent-orders| + +
+ +
+WebSocket Feed + +| API Name | Request | Parameter | Parameter Example | Description | +| -------- | -------- | -------- | -------- | -------- | +| WebSocketPublicToken | POST /api/v1/bullet-public | | | https://docs.kucoin.com/#apply-connect-token | +| WebSocketPrivateToken | POST /api/v1/bullet-private | | | https://docs.kucoin.com/#apply-connect-token | +| NewWebSocketClient | | | | https://docs.kucoin.com/#websocket-feed | + +
+ +
+Withdrawal + +| API Name | Request | Parameter | Parameter Example | Description | +| -------- | -------- | -------- | -------- | -------- | +| WithdrawalQuotas | GET /api/v1/withdrawals/quotas | currency | BTC | https://docs.kucoin.com/#get-withdrawal-quotas | +| Withdrawals | GET /api/v1/withdrawals | | | https://docs.kucoin.com/#get-withdrawals-list | +| ApplyWithdrawal | POST /api/v1/withdrawals | currency, address,amount | KCS,{address},0.1 | https://docs.kucoin.com/#apply-withdraw-2 | +| CancelWithdrawal | DELETE /api/v1/withdrawals/{withdrawalId} | withdrawalId | {withdrawal ID} | https://docs.kucoin.com/#cancel-withdrawal | + +
+ +
+Currency + +| API Name | Request | Parameter | Parameter Example | Description | +| -------- | -------- | -------- | -------- | -------- | +| Currencies | GET /api/v1/currencies | | | https://docs.kucoin.com/#get-currencies | +| Currency | GET /api/v1/currencies/BTC | currency | BTC | https://docs.kucoin.com/#get-currency-detail | +| Prices | GET /api/v1/prices | base, currencies | USD,KCS | https://docs.kucoin.com/#get-fiat-price | + +
+ +
+Symbol + +| API Name | Request | Parameter | Parameter Example | Description | +| -------- | -------- | -------- | -------- | -------- | +| Symbols | GET /api/v1/symbols | | | https://docs.kucoin.com/#get-symbols-list | +| TickerLevel1 | GET /api/v1/market/orderbook/level1 | symbol | BTC-USDT | https://docs.kucoin.com/#get-ticker | +| Tickers | GET /api/v1/market/allTickers | | | https://docs.kucoin.com/#get-all-tickers | +| AggregatedPartOrderBook | GET /api/v1/market/orderbook/level2_{depth} | symbol, depth | BTC-USDT,20 | https://docs.kucoin.com/#get-part-order-book-aggregated | +| AggregatedFullOrderBook | GET /api/v2/market/orderbook/level2 | symbol | BTC-USDT |https://docs.kucoin.com/#get-full-order-book-aggregated| +| AtomicFullOrderBook | GET GET /api/v1/market/orderbook/level3 | symbol | BTC-USDT | https://docs.kucoin.com/#get-full-order-book-atomic | +| TradeHistories | GET /api/v1/market/histories | symbol | BTC-USDT | https://docs.kucoin.com/#get-trade-histories | +| KLines | GET /api/v1/market/candles | symbol, type | BTC-USDT,30min | https://docs.kucoin.com/#get-klines | +| Stats24hr | GET /api/v1/market/stats | symbol | BTC-USDT | https://docs.kucoin.com/#get-24hr-stats | +| Markets | GET /api/v1/markets | | | https://docs.kucoin.com/#get-market-list | + +
+ +
+Time + +| API Name | Request | Parameter | Parameter Example | Description | +| -------- | -------- | -------- | -------- | -------- | +| ServerTime | GET /api/v1/timestamp | | | https://docs.kucoin.com/#server-time | + +
+ +
+Service Status + +| API Name | Request | Parameter | Parameter Example | Description | +| -------- | -------- | -------- | -------- | -------- | +| ServiceStatus | GET /api/v1/status | | | https://docs.kucoin.com/#service-status | + +
\ No newline at end of file diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..60617df --- /dev/null +++ b/config.json.example @@ -0,0 +1,7 @@ +{ + "ApiBaseURI":"", + "ApiKey": "", + "ApiSecret": "", + "ApiPassphrase": "", + "ApiSkipVerifyTls": false +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bce7672 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module kucoin-api-test + +go 1.14 + +require ( + github.com/Kucoin/kucoin-go-sdk v1.2.6 + github.com/gorilla/websocket v1.4.0 + github.com/pkg/errors v0.8.1 + github.com/sirupsen/logrus v1.4.1 +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..310668d --- /dev/null +++ b/main.go @@ -0,0 +1,195 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io/ioutil" + "kucoin-api-test/sdk" + "log" + "os" + "reflect" + "strings" +) + +var ( + kucoin *sdk.ApiService + PATH = "./config.json.example" +) + +func main() { + //1. get api authentication info from user input or configuration file. + var apiURI, apiKey, apiSecret, apiPassphrase string + var apiSkipVerifyTls bool + + // read config from json file + file, err := ioutil.ReadFile(PATH) + if err != nil { + log.Fatal("error when read configuration from json file:", err) + return + } + var config ApiConfig + // unmarshal config data + err = json.Unmarshal(file, &config) + if err != nil { + log.Fatal("error when unmarshal config from json file:", err) + return + } + // if we have configuration from json file + if len(config.ApiBaseURI) > 0 { + apiURI = config.ApiBaseURI + } else { + log.Println("please input api base URI,such as:https://api.kumex.com") + fmt.Scanf("%s", &apiURI) + } + + if len(config.ApiKey) > 0 { + apiURI = config.ApiKey + } else { + log.Println("please input your api key...") + fmt.Scanf("%s", &apiKey) + } + + if len(config.ApiSecret) > 0 { + apiURI = config.ApiSecret + } else { + log.Println("please input your api secret...") + fmt.Scanf("%s", &apiSecret) + } + + if len(config.ApiPassphrase) > 0 { + apiURI = config.ApiPassphrase + } else { + log.Println("please input your api passphrase...") + fmt.Scanf("%s", &apiPassphrase) + } + + apiSkipVerifyTls = config.ApiSkipVerifyTls + + initApiService(apiURI, apiKey, apiSecret, apiPassphrase, apiSkipVerifyTls) + + // 2. api function map to user input. + apiMap := initApiMap() + // 3. get api info from user input. + var apiName string + log.Println("please input the api name which you want to test...") + fmt.Scanf("%s", &apiName) + log.Println("please input the api parameters,use ',' to spilt parameters.") + log.Println("if it is no parameter api,just enter to skip...") + + reader := bufio.NewReader(os.Stdin) + inputs, err := reader.ReadString('\n') + if err != nil { + log.Fatal("input parameter error:", err) + } + + var params []string + if len(strings.TrimSpace(inputs)) > 0 { + params = strings.Split(strings.TrimSpace(inputs), ",") + } + // 4. call api function which user choose to test. + call(apiMap, apiName, params) +} + +type ApiConfig struct { + ApiBaseURI string `json:ApiBaseURI` + ApiKey string `json:ApiKey` + ApiSecret string `json:ApiSecret` + ApiPassphrase string `json:ApiPassphrase` + ApiSkipVerifyTls bool `json:ApiSkipVerifyTls` +} + +// initApiService init an api service +func initApiService(apiURI, apiKey, apiSecret, apiPassphrase string, apiSkipVerifyTls bool) { + kucoin = sdk.NewApiService( + sdk.ApiBaseURIOption(apiURI), + sdk.ApiKeyOption(apiKey), + sdk.ApiSecretOption(apiSecret), + sdk.ApiPassPhraseOption(apiPassphrase), + sdk.ApiSkipVerifyTlsOption(apiSkipVerifyTls), + ) + sdk.DebugMode = true +} + +// call according to string input to call function which named the input. +func call(m map[string]interface{}, name string, params []string) { + // if string input is a function + f := reflect.ValueOf(m[name]) + if f.Kind() != reflect.Func { + log.Fatal("input error,your input is not a name of api.") + return + } + + in := make([]reflect.Value, len(params)) + // if the function has no parameters + if len(params) > 0 { + for k, v := range params { + fmt.Println(v) + in[k] = reflect.ValueOf(v) + } + } else { + in = nil + } + + f.Call(in) +} + +// initApiMap init api function mapping +func initApiMap() map[string]interface{} { + return map[string]interface{}{ + "CreateAccount": kucoin.CreateAccount, + "Accounts": kucoin.Accounts, + "Account": kucoin.Account, + "SubAccountUsers": kucoin.SubAccountUsers, + "SubAccounts": kucoin.SubAccounts, + "SubAccount": kucoin.SubAccount, + "AccountLedgers": kucoin.AccountLedgers, + "AccountHolds": kucoin.AccountHolds, + + "InnerTransferV2": kucoin.InnerTransferV2, + "SubTransfer": kucoin.SubTransfer, + "CreateDepositAddress": kucoin.CreateDepositAddress, + "DepositAddresses": kucoin.DepositAddresses, + "V1Deposits": kucoin.V1Deposits, + "Deposits": kucoin.Deposits, + + "Fills": kucoin.Fills, + "RecentFills": kucoin.RecentFills, + + "CreateOrder": kucoin.CreateOrder, + "CreateMultiOrder": kucoin.CreateMultiOrder, + "CancelOrder": kucoin.CancelOrder, + "CancelOrders": kucoin.CancelOrders, + "Orders": kucoin.Orders, + "Order": kucoin.Order, + "RecentOrders": kucoin.RecentOrders, + + "WebSocketPublicToken": kucoin.WebSocketPublicToken, + "WebSocketPrivateToken": kucoin.WebSocketPrivateToken, + "NewWebSocketClient": kucoin.NewWebSocketClient, + + "WithdrawalQuotas": kucoin.WithdrawalQuotas, + "Withdrawals": kucoin.Withdrawals, + "ApplyWithdrawal": kucoin.ApplyWithdrawal, + "CancelWithdrawal": kucoin.CancelWithdrawal, + + "Currencies": kucoin.Currencies, + "Currency": kucoin.Currency, + "Prices": kucoin.Prices, + + "Symbols": kucoin.Symbols, + "TickerLevel1": kucoin.TickerLevel1, + "Tickers": kucoin.Tickers, + "AggregatedPartOrderBook": kucoin.AggregatedPartOrderBook, + "AggregatedFullOrderBook": kucoin.AggregatedFullOrderBook, + "AtomicFullOrderBook": kucoin.AtomicFullOrderBook, + "TradeHistories": kucoin.TradeHistories, + "KLines": kucoin.KLines, + "Stats24hr": kucoin.Stats24hr, + "Markets": kucoin.Markets, + + "ServerTime": kucoin.ServerTime, + + "ServiceStatus": kucoin.ServiceStatus, + } +} diff --git a/sdk/account.go b/sdk/account.go new file mode 100644 index 0000000..4c6301f --- /dev/null +++ b/sdk/account.go @@ -0,0 +1,209 @@ +package sdk + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +// An AccountModel represents an account. +type AccountModel struct { + Id string `json:"id"` + Currency string `json:"currency"` + Type string `json:"type"` + Balance string `json:"balance"` + Available string `json:"available"` + Holds string `json:"holds"` +} + +// An AccountsModel is the set of *AccountModel. +type AccountsModel []*AccountModel + +// Accounts returns a list of accounts. +// See the Deposits section for documentation on how to deposit funds to begin trading. +func (as *ApiService) Accounts(currency, typo string) (*ApiResponse, error) { + p := map[string]string{} + if currency != "" { + p["currency"] = currency + } + if typo != "" { + p["type"] = typo + } + req := NewRequest(http.MethodGet, "/api/v1/accounts", p) + return as.Call(req) +} + +// Account returns an account when you know the accountId. +func (as *ApiService) Account(accountId string) (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/accounts/"+accountId, nil) + return as.Call(req) +} + +// A SubAccountUserModel represents a sub-account user. +type SubAccountUserModel struct { + UserId string `json:"userId"` + SubName string `json:"subName"` + Remarks string `json:"remarks"` +} + +// A SubAccountUsersModel is the set of *SubAccountUserModel. +type SubAccountUsersModel []*SubAccountUserModel + +// SubAccountUsers returns a list of sub-account user. +func (as *ApiService) SubAccountUsers() (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/sub/user", nil) + return as.Call(req) +} + +// A SubAccountsModel is the set of *SubAccountModel. +type SubAccountsModel []*SubAccountModel + +// SubAccounts returns the aggregated balance of all sub-accounts of the current user. +func (as *ApiService) SubAccounts() (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/sub-accounts", nil) + return as.Call(req) +} + +// A SubAccountModel represents the balance of a sub-account user. +type SubAccountModel struct { + SubUserId string `json:"subUserId"` + SubName string `json:"subName"` + MainAccounts []struct { + Currency string `json:"currency"` + Balance string `json:"balance"` + Available string `json:"available"` + Holds string `json:"holds"` + } `json:"mainAccounts"` + TradeAccounts []struct { + Currency string `json:"currency"` + Balance string `json:"balance"` + Available string `json:"available"` + Holds string `json:"holds"` + } `json:"tradeAccounts"` +} + +// SubAccount returns the detail of a sub-account. +func (as *ApiService) SubAccount(subUserId string) (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/sub-accounts/"+subUserId, nil) + return as.Call(req) +} + +// CreateAccount creates an account according to type(main|trade) and currency +// Parameter #1 typo is type of account. +func (as *ApiService) CreateAccount(typo, currency string) (*ApiResponse, error) { + req := NewRequest(http.MethodPost, "/api/v1/accounts", map[string]string{"currency": currency, "type": typo}) + return as.Call(req) +} + +// An AccountLedgerModel represents account activity either increases or decreases your account balance. +type AccountLedgerModel struct { + Currency string `json:"currency"` + Amount string `json:"amount"` + Fee string `json:"fee"` + Balance string `json:"balance"` + BizType string `json:"bizType"` + Direction string `json:"direction"` + CreatedAt int64 `json:"createdAt"` + Context json.RawMessage `json:"context"` +} + +// An AccountLedgersModel the set of *AccountLedgerModel. +type AccountLedgersModel []*AccountLedgerModel + +// AccountLedgers returns a list of ledgers. +// Account activity either increases or decreases your account balance. +// Items are paginated and sorted latest first. +func (as *ApiService) AccountLedgers(accountId string) (*ApiResponse, error) { + p := map[string]string{} + // default + pagination := &PaginationParam{CurrentPage: 1, PageSize: 10} + + pagination.ReadParam(p) + req := NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/ledgers", accountId), p) + return as.Call(req) +} + +// An AccountHoldModel represents the holds on an account for any active orders or pending withdraw requests. +// As an order is filled, the hold amount is updated. +// If an order is canceled, any remaining hold is removed. +// For a withdraw, once it is completed, the hold is removed. +type AccountHoldModel struct { + Currency string `json:"currency"` + HoldAmount string `json:"holdAmount"` + BizType string `json:"bizType"` + OrderId string `json:"orderId"` + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` +} + +// An AccountHoldsModel is the set of *AccountHoldModel. +type AccountHoldsModel []*AccountHoldModel + +// AccountHolds returns a list of currency hold. +// Holds are placed on an account for any active orders or pending withdraw requests. +// As an order is filled, the hold amount is updated. +// If an order is canceled, any remaining hold is removed. +// For a withdraw, once it is completed, the hold is removed. +func (as *ApiService) AccountHolds(accountId string) (*ApiResponse, error) { + p := map[string]string{} + // default + pagination := &PaginationParam{CurrentPage: 1, PageSize: 10} + pagination.ReadParam(p) + req := NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/holds", accountId), p) + return as.Call(req) +} + +// An InnerTransferResultModel represents the result of a inner-transfer operation. +type InnerTransferResultModel struct { + OrderId string `json:"orderId"` +} + +// InnerTransfer makes a currency transfer internally. +// Deprecated: This interface was discontinued on August 29, 2019. Please use InnerTransferV2. +// The inner transfer interface is used for transferring assets between the accounts of a user and is free of charges. +// For example, a user could transfer assets from their main account to their trading account on the platform. +func (as *ApiService) InnerTransfer(clientOid, payAccountId, recAccountId, amount string) (*ApiResponse, error) { + p := map[string]string{ + "clientOid": clientOid, + "payAccountId": payAccountId, + "recAccountId": recAccountId, + "amount": amount, + } + req := NewRequest(http.MethodPost, "/api/v1/accounts/inner-transfer", p) + return as.Call(req) +} + +// InnerTransferV2 makes a currency transfer internally. +// Recommended for use on June 5, 2019. +// The inner transfer interface is used for transferring assets between the accounts of a user and is free of charges. +// For example, a user could transfer assets from their main account to their trading account on the platform. +func (as *ApiService) InnerTransferV2(currency, from, to, amount string) (*ApiResponse, error) { + p := map[string]string{ + "clientOid": IntToString(time.Now().Unix()), + "currency": currency, + "from": from, + "to": to, + "amount": amount, + } + req := NewRequest(http.MethodPost, "/api/v2/accounts/inner-transfer", p) + return as.Call(req) +} + +// A SubTransferResultModel represents the result of a sub-transfer operation. +type SubTransferResultModel InnerTransferResultModel + +// SubTransfer transfers between master account and sub-account. +func (as *ApiService) SubTransfer(currency, amount, direction, accountType, subAccountType, subUserId string) (*ApiResponse, error) { + p := map[string]string{ + "clientOid": IntToString(time.Now().Unix()), + "currency": currency, + "amount": amount, + "direction": direction, + "accountType": accountType, + "subAccountType": subAccountType, + "subUserId": subUserId, + } + req := NewRequest(http.MethodPost, "/api/v1/accounts/sub-transfer", p) + return as.Call(req) +} diff --git a/sdk/api.go b/sdk/api.go new file mode 100644 index 0000000..c0c26e9 --- /dev/null +++ b/sdk/api.go @@ -0,0 +1,171 @@ +/* +Package sdk provides two kinds of APIs: `RESTful API` and `WebSocket feed`. +The official document: https://docs.kucoin.com +*/ +package sdk + +import ( + "bytes" + "errors" + "fmt" + "log" + "os" + "runtime" + "time" + + "github.com/sirupsen/logrus" +) + +var ( + // Version is SDK version. + Version = "1.2.6" + // DebugMode will record the logs of API and WebSocket to files in the directory "kucoin.LogDirectory" according to the minimum log level "kucoin.LogLevel". + DebugMode = os.Getenv("API_DEBUG_MODE") == "1" +) + +func init() { + // Initialize the logging component by default + logrus.SetLevel(logrus.DebugLevel) + if runtime.GOOS == "windows" { + SetLoggerDirectory("tmp") + } else { + SetLoggerDirectory("/tmp") + } +} + +// SetLoggerDirectory sets the directory for logrus output. +func SetLoggerDirectory(directory string) { + var logFile string + if !DebugMode { + logFile = os.DevNull + } else { + logFile = fmt.Sprintf("%s/kucoin-sdk-%s.log", directory, time.Now().Format("2006-01-02")) + } + logWriter, err := os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0664) + if err != nil { + log.Panicf("Open file failed: %s", err.Error()) + } + logrus.SetOutput(logWriter) +} + +// An ApiService provides a HTTP client and a signer to make a HTTP request with the signature to KuCoin API. +type ApiService struct { + apiBaseURI string + apiKey string + apiSecret string + apiPassphrase string + apiSkipVerifyTls bool + requester Requester + signer Signer +} + +// ProductionApiBaseURI is api base uri for production. +const ProductionApiBaseURI = "https://api.kucoin.com" + +// An ApiServiceOption is a option parameter to create the instance of ApiService. +type ApiServiceOption func(service *ApiService) + +// ApiBaseURIOption creates a instance of ApiServiceOption about apiBaseURI. +func ApiBaseURIOption(uri string) ApiServiceOption { + return func(service *ApiService) { + service.apiBaseURI = uri + } +} + +// ApiKeyOption creates a instance of ApiServiceOption about apiKey. +func ApiKeyOption(key string) ApiServiceOption { + return func(service *ApiService) { + service.apiKey = key + } +} + +// ApiSecretOption creates a instance of ApiServiceOption about apiSecret. +func ApiSecretOption(secret string) ApiServiceOption { + return func(service *ApiService) { + service.apiSecret = secret + } +} + +// ApiPassPhraseOption creates a instance of ApiServiceOption about apiPassPhrase. +func ApiPassPhraseOption(passPhrase string) ApiServiceOption { + return func(service *ApiService) { + service.apiPassphrase = passPhrase + } +} + +// ApiSkipVerifyTlsOption creates a instance of ApiServiceOption about apiSkipVerifyTls. +func ApiSkipVerifyTlsOption(skipVerifyTls bool) ApiServiceOption { + return func(service *ApiService) { + service.apiSkipVerifyTls = skipVerifyTls + } +} + +// NewApiService creates a instance of ApiService by passing ApiServiceOptions, then you can call methods. +func NewApiService(opts ...ApiServiceOption) *ApiService { + as := &ApiService{requester: &BasicRequester{}} + for _, opt := range opts { + opt(as) + } + if as.apiBaseURI == "" { + as.apiBaseURI = ProductionApiBaseURI + } + if as.apiKey != "" { + as.signer = NewKcSigner(as.apiKey, as.apiSecret, as.apiPassphrase) + } + return as +} + +// NewApiServiceFromEnv creates a instance of ApiService by environmental variables such as `API_BASE_URI` `API_KEY` `API_SECRET` `API_PASSPHRASE`, then you can call the methods of ApiService. +func NewApiServiceFromEnv() *ApiService { + return NewApiService( + ApiBaseURIOption(os.Getenv("API_BASE_URI")), + ApiKeyOption(os.Getenv("API_KEY")), + ApiSecretOption(os.Getenv("API_SECRET")), + ApiPassPhraseOption(os.Getenv("API_PASSPHRASE")), + ApiSkipVerifyTlsOption(os.Getenv("API_SKIP_VERIFY_TLS") == "1"), + ) +} + +// Call calls the API by passing *Request and returns *ApiResponse. +func (as *ApiService) Call(request *Request) (*ApiResponse, error) { + defer func() { + if err := recover(); err != nil { + log.Println("[[Recovery] panic recovered:", err) + } + }() + + request.BaseURI = as.apiBaseURI + request.SkipVerifyTls = as.apiSkipVerifyTls + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "KuCoin-Go-SDK/"+Version) + if as.signer != nil { + var b bytes.Buffer + b.WriteString(request.Method) + b.WriteString(request.RequestURI()) + b.Write(request.Body) + h := as.signer.(*KcSigner).Headers(b.String()) + for k, v := range h { + request.Header.Set(k, v) + } + } + + rsp, err := as.requester.Request(request, request.Timeout) + if err != nil { + return nil, err + } + + ar := &ApiResponse{response: rsp} + if err := rsp.ReadJsonBody(ar); err != nil { + rb, _ := rsp.ReadBody() + m := fmt.Sprintf("[Parse]Failure: parse JSON body failed because %s, %s %s with body=%s, respond code=%d body=%s", + err.Error(), + rsp.request.Method, + rsp.request.RequestURI(), + string(rsp.request.Body), + rsp.StatusCode, + string(rb), + ) + return ar, errors.New(m) + } + return ar, nil +} diff --git a/sdk/currency.go b/sdk/currency.go new file mode 100644 index 0000000..6cb2879 --- /dev/null +++ b/sdk/currency.go @@ -0,0 +1,46 @@ +package sdk + +import ( + "net/http" +) + +// A CurrencyModel represents a model of known currency. +type CurrencyModel struct { + Name string `json:"name"` + Currency string `json:"currency"` + FullName string `json:"fullName"` + Precision uint8 `json:"precision"` + WithdrawalMinSize string `json:"withdrawalMinSize"` + WithdrawalMinFee string `json:"withdrawalMinFee"` + IsWithdrawEnabled bool `json:"isWithdrawEnabled"` + IsDepositEnabled bool `json:"isDepositEnabled"` +} + +// A CurrenciesModel is the set of *CurrencyModel. +type CurrenciesModel []*CurrencyModel + +// Currencies returns a list of known currencies. +func (as *ApiService) Currencies() (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/currencies", nil) + return as.Call(req) +} + +// Currency returns the details of the currency. +func (as *ApiService) Currency(currency string) (*ApiResponse, error) { + params := map[string]string{} + req := NewRequest(http.MethodGet, "/api/v1/currencies/"+currency, params) + return as.Call(req) +} + +// Prices returns the fiat prices for currency. +func (as *ApiService) Prices(base, currencies string) (*ApiResponse, error) { + params := map[string]string{} + if base != "" { + params["base"] = base + } + if currencies != "" { + params["currencies"] = currencies + } + req := NewRequest(http.MethodGet, "/api/v1/prices", params) + return as.Call(req) +} diff --git a/sdk/deposit.go b/sdk/deposit.go new file mode 100644 index 0000000..3672f78 --- /dev/null +++ b/sdk/deposit.go @@ -0,0 +1,80 @@ +package sdk + +import ( + "net/http" +) + +// A DepositAddressModel represents a deposit address of currency for deposit. +type DepositAddressModel struct { + Address string `json:"address"` + Memo string `json:"memo"` +} + +// A DepositAddressesModel is the set of *DepositAddressModel. +type DepositAddressesModel []*DepositAddressModel + +// A DepositModel represents a deposit record. +type DepositModel struct { + Address string `json:"address"` + Memo string `json:"memo"` + Amount string `json:"amount"` + Fee string `json:"fee"` + Currency string `json:"currency"` + IsInner bool `json:"isInner"` + WalletTxId string `json:"walletTxId"` + Status string `json:"status"` + Remark string `json:"remark"` + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` +} + +// A DepositsModel is the set of *DepositModel. +type DepositsModel []*DepositModel + +// CreateDepositAddress creates a deposit address. +func (as *ApiService) CreateDepositAddress(currency string) (*ApiResponse, error) { + params := map[string]string{"currency": currency} + req := NewRequest(http.MethodPost, "/api/v1/deposit-addresses", params) + return as.Call(req) +} + +// DepositAddresses returns the deposit address of currency for deposit. +// If return data is empty, you may need create a deposit address first. +func (as *ApiService) DepositAddresses(currency string) (*ApiResponse, error) { + params := map[string]string{"currency": currency} + req := NewRequest(http.MethodGet, "/api/v1/deposit-addresses", params) + return as.Call(req) +} + +// Deposits returns a list of deposit. +func (as *ApiService) Deposits(currency string) (*ApiResponse, error) { + params := map[string]string{"currency": currency} + pagination := &PaginationParam{CurrentPage: 1, PageSize: 10} + pagination.ReadParam(params) + req := NewRequest(http.MethodGet, "/api/v1/deposits", params) + return as.Call(req) +} + +// A V1DepositModel represents a v1 deposit record. +type V1DepositModel struct { + Amount string `json:"amount"` + Currency string `json:"currency"` + IsInner bool `json:"isInner"` + WalletTxId string `json:"walletTxId"` + Status string `json:"status"` + CreateAt int64 `json:"createAt"` +} + +// A V1DepositsModel is the set of *V1DepositModel. +type V1DepositsModel []*V1DepositModel + +// V1Deposits returns a list of v1 historical deposits. +func (as *ApiService) V1Deposits(currency string) (*ApiResponse, error) { + params := map[string]string{ + "currency": currency, + } + pagination := &PaginationParam{CurrentPage: 1, PageSize: 10} + pagination.ReadParam(params) + req := NewRequest(http.MethodGet, "/api/v1/hist-deposits", params) + return as.Call(req) +} diff --git a/sdk/fill.go b/sdk/fill.go new file mode 100644 index 0000000..81ef166 --- /dev/null +++ b/sdk/fill.go @@ -0,0 +1,41 @@ +package sdk + +import "net/http" + +// A FillModel represents the structure of fill. +type FillModel struct { + Symbol string `json:"symbol"` + TradeId string `json:"tradeId"` + OrderId string `json:"orderId"` + CounterOrderId string `json:"counterOrderId"` + Side string `json:"side"` + Liquidity string `json:"liquidity"` + ForceTaker bool `json:"forceTaker"` + Price string `json:"price"` + Size string `json:"size"` + Funds string `json:"funds"` + Fee string `json:"fee"` + FeeRate string `json:"feeRate"` + FeeCurrency string `json:"feeCurrency"` + Stop string `json:"stop"` + Type string `json:"type"` + CreatedAt int64 `json:"createdAt"` +} + +// A FillsModel is the set of *FillModel. +type FillsModel []*FillModel + +// Fills returns a list of recent fills. +func (as *ApiService) Fills(tradeType string) (*ApiResponse, error) { + params := map[string]string{"tradeType": tradeType} + pagination := &PaginationParam{CurrentPage: 1, PageSize: 10} + pagination.ReadParam(params) + req := NewRequest(http.MethodGet, "/api/v1/fills", params) + return as.Call(req) +} + +// RecentFills returns the recent fills of the latest transactions within 24 hours. +func (as *ApiService) RecentFills() (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/limit/fills", nil) + return as.Call(req) +} diff --git a/sdk/helper.go b/sdk/helper.go new file mode 100644 index 0000000..90cfd31 --- /dev/null +++ b/sdk/helper.go @@ -0,0 +1,20 @@ +package sdk + +import ( + "encoding/json" + "strconv" +) + +// IntToString converts int64 to string. +func IntToString(i int64) string { + return strconv.FormatInt(i, 10) +} + +// ToJsonString converts any value to JSON string. +func ToJsonString(v interface{}) string { + b, err := json.Marshal(v) + if err != nil { + return "" + } + return string(b) +} diff --git a/sdk/http.go b/sdk/http.go new file mode 100644 index 0000000..a435ffe --- /dev/null +++ b/sdk/http.go @@ -0,0 +1,286 @@ +package sdk + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "time" + + "github.com/sirupsen/logrus" +) + +// A Request represents a HTTP request. +type Request struct { + fullURL string + requestURI string + BaseURI string + Method string + Path string + Query url.Values + Body []byte + Header http.Header + Timeout time.Duration + SkipVerifyTls bool +} + +// NewRequest creates a instance of Request. +func NewRequest(method, path string, params interface{}) *Request { + r := &Request{ + Method: method, + Path: path, + Query: make(url.Values), + Header: make(http.Header), + Body: []byte{}, + Timeout: 30 * time.Second, + } + if r.Path == "" { + r.Path = "/" + } + if r.Method == "" { + r.Method = http.MethodGet + } + r.addParams(params) + return r +} + +func (r *Request) addParams(p interface{}) { + if p == nil { + return + } + switch r.Method { + case http.MethodGet, http.MethodDelete: + for key, value := range p.(map[string]string) { + r.Query.Add(key, value) + } + default: + b, err := json.Marshal(p) + if err != nil { + log.Panic("Cannot marshal params to JSON string:", err.Error()) + } + r.Body = b + } +} + +// RequestURI returns the request uri. +func (r *Request) RequestURI() string { + if r.requestURI != "" { + return r.requestURI + } + + fu := r.FullURL() + u, err := url.Parse(fu) + if err != nil { + r.requestURI = "/" + } else { + r.requestURI = u.RequestURI() + } + return r.requestURI +} + +// FullURL returns the full url. +func (r *Request) FullURL() string { + if r.fullURL != "" { + return r.fullURL + } + r.fullURL = fmt.Sprintf("%s%s", r.BaseURI, r.Path) + if len(r.Query) > 0 { + if strings.Contains(r.fullURL, "?") { + r.fullURL += "&" + r.Query.Encode() + } else { + r.fullURL += "?" + r.Query.Encode() + } + } + return r.fullURL +} + +// HttpRequest creates a instance of *http.Request. +func (r *Request) HttpRequest() (*http.Request, error) { + req, err := http.NewRequest(r.Method, r.FullURL(), bytes.NewBuffer(r.Body)) + if err != nil { + return nil, err + } + + for key, values := range r.Header { + for _, value := range values { + req.Header.Add(key, value) + } + } + + return req, nil +} + +// Requester contains Request() method, can launch a http request. +type Requester interface { + Request(request *Request, timeout time.Duration) (*Response, error) +} + +// A BasicRequester represents a basic implement of Requester by http.Client. +type BasicRequester struct { +} + +// Request makes a http request. +func (br *BasicRequester) Request(request *Request, timeout time.Duration) (*Response, error) { + tr := http.DefaultTransport + tc := tr.(*http.Transport).TLSClientConfig + if tc == nil { + tc = &tls.Config{InsecureSkipVerify: request.SkipVerifyTls} + } else { + tc.InsecureSkipVerify = request.SkipVerifyTls + } + + cli := http.DefaultClient + cli.Transport, cli.Timeout = tr, timeout + + req, err := request.HttpRequest() + if err != nil { + return nil, err + } + // Prevent re-use of TCP connections + // req.Close = true + + rid := time.Now().UnixNano() + + if DebugMode { + dump, _ := httputil.DumpRequest(req, true) + logrus.Debugf("Sent a HTTP request#%d: %s", rid, string(dump)) + log.Printf("Sent a HTTP request#%d: %s", rid, string(dump)) + } + + rsp, err := cli.Do(req) + if err != nil { + return nil, err + } + + if DebugMode { + dump, _ := httputil.DumpResponse(rsp, true) + logrus.Debugf("Received a HTTP response#%d: %s", rid, string(dump)) + log.Printf("Received a HTTP response#%d: %s", rid, string(dump)) + } + + return &Response{ + request: request, + Response: rsp, + body: nil, + }, nil +} + +// A Response represents a HTTP response. +type Response struct { + request *Request + *http.Response + body []byte +} + +// ReadBody read the response data, then return it. +func (r *Response) ReadBody() ([]byte, error) { + if r.body != nil { + return r.body, nil + } + + r.body = make([]byte, 0) + defer r.Body.Close() + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } + r.body = b + return r.body, nil +} + +// ReadJsonBody read the response data as JSON into v. +func (r *Response) ReadJsonBody(v interface{}) error { + b, err := r.ReadBody() + if err != nil { + return err + } + return json.Unmarshal(b, v) +} + +// The predefined API codes +const ( + ApiSuccess = "200000" +) + +// An ApiResponse represents a API response wrapped Response. +type ApiResponse struct { + response *Response + Code string `json:"code"` + RawData json.RawMessage `json:"data"` // delay parsing + Message string `json:"msg"` +} + +// HttpSuccessful judges the success of http. +func (ar *ApiResponse) HttpSuccessful() bool { + return ar.response.StatusCode == http.StatusOK +} + +// ApiSuccessful judges the success of API. +func (ar *ApiResponse) ApiSuccessful() bool { + return ar.Code == ApiSuccess +} + +// ReadData read the api response `data` as JSON into v. +func (ar *ApiResponse) ReadData(v interface{}) error { + if !ar.HttpSuccessful() { + rsb, _ := ar.response.ReadBody() + m := fmt.Sprintf("[HTTP]Failure: status code is NOT 200, %s %s with body=%s, respond code=%d body=%s", + ar.response.request.Method, + ar.response.request.RequestURI(), + string(ar.response.request.Body), + ar.response.StatusCode, + string(rsb), + ) + return errors.New(m) + } + + if !ar.ApiSuccessful() { + m := fmt.Sprintf("[API]Failure: api code is NOT %s, %s %s with body=%s, respond code=%s message=\"%s\" data=%s", + ApiSuccess, + ar.response.request.Method, + ar.response.request.RequestURI(), + string(ar.response.request.Body), + ar.Code, + ar.Message, + string(ar.RawData), + ) + return errors.New(m) + } + // when input parameter v is nil, read nothing and return nil + if v == nil { + return nil + } + + if len(ar.RawData) == 0 { + m := fmt.Sprintf("[API]Failure: try to read empty data, %s %s with body=%s, respond code=%s message=\"%s\" data=%s", + ar.response.request.Method, + ar.response.request.RequestURI(), + string(ar.response.request.Body), + ar.Code, + ar.Message, + string(ar.RawData), + ) + return errors.New(m) + } + + return json.Unmarshal(ar.RawData, v) +} + +// ReadPaginationData read the data `items` as JSON into v, and returns *PaginationModel. +func (ar *ApiResponse) ReadPaginationData(v interface{}) (*PaginationModel, error) { + p := &PaginationModel{} + if err := ar.ReadData(p); err != nil { + return nil, err + } + if err := p.ReadItems(v); err != nil { + return p, err + } + return p, nil +} diff --git a/sdk/order.go b/sdk/order.go new file mode 100644 index 0000000..14000b8 --- /dev/null +++ b/sdk/order.go @@ -0,0 +1,190 @@ +package sdk + +import ( + "net/http" + "strconv" + "time" +) + +// A CreateOrderModel is the input parameter of CreateOrder(). +type CreateOrderModel struct { + // BASE PARAMETERS + ClientOid string `json:"clientOid"` + Side string `json:"side"` + Symbol string `json:"symbol,omitempty"` + Type string `json:"type,omitempty"` + Remark string `json:"remark,omitempty"` + Stop string `json:"stop,omitempty"` + StopPrice string `json:"stopPrice,omitempty"` + STP string `json:"stp,omitempty"` + TradeType string `json:"tradeType,omitempty"` + + // LIMIT ORDER PARAMETERS + Price string `json:"price,omitempty"` + Size string `json:"size,omitempty"` + TimeInForce string `json:"timeInForce,omitempty"` + CancelAfter uint64 `json:"cancelAfter,omitempty"` + PostOnly bool `json:"postOnly,omitempty"` + Hidden bool `json:"hidden,omitempty"` + IceBerg bool `json:"iceberg,omitempty"` + VisibleSize string `json:"visibleSize,omitempty"` + + // MARKET ORDER PARAMETERS + // Size string `json:"size"` + Funds string `json:"funds,omitempty"` +} + +// A CreateOrderResultModel represents the result of CreateOrder(). +type CreateOrderResultModel struct { + OrderId string `json:"orderId"` +} + +// CreateOrder places a new order. +func (as *ApiService) CreateOrder(side, symbol, price, size string) (*ApiResponse, error) { + o := &CreateOrderModel{ + ClientOid: IntToString(time.Now().UnixNano()), + Side: side, + Symbol: symbol, + Price: price, + Size: size, + } + req := NewRequest(http.MethodPost, "/api/v1/orders", o) + return as.Call(req) +} + +// A CreateMultiOrderResultModel represents the result of CreateMultiOrder(). +type CreateMultiOrderResultModel struct { + Data OrdersModel `json:"data"` +} + +// CreateMultiOrder places bulk orders. +func (as *ApiService) CreateMultiOrder(symbol, side, price, size string) (*ApiResponse, error) { + orders := make([]*CreateOrderModel, 0, 3) + for i := 0; i < 2; i++ { + p := &CreateOrderModel{ + ClientOid: IntToString(time.Now().UnixNano() + int64(i)), + Side: side, + Price: price, + Size: size, + Remark: "Multi " + strconv.Itoa(i), + } + orders = append(orders, p) + } + params := map[string]interface{}{ + "symbol": symbol, + "orderList": orders, + } + req := NewRequest(http.MethodPost, "/api/v1/orders/multi", params) + return as.Call(req) +} + +// A CancelOrderResultModel represents the result of CancelOrder(). +type CancelOrderResultModel struct { + CancelledOrderIds []string `json:"cancelledOrderIds"` +} + +// CancelOrder cancels a previously placed order. +func (as *ApiService) CancelOrder(orderId string) (*ApiResponse, error) { + req := NewRequest(http.MethodDelete, "/api/v1/orders/"+orderId, nil) + return as.Call(req) +} + +// CancelOrders cancels all orders of the symbol. +// With best effort, cancel all open orders. The response is a list of ids of the canceled orders. +func (as *ApiService) CancelOrders(symbol string) (*ApiResponse, error) { + p := map[string]string{} + if symbol != "" { + p["symbol"] = symbol + } + req := NewRequest(http.MethodDelete, "/api/v1/orders", p) + return as.Call(req) +} + +// An OrderModel represents an order. +type OrderModel struct { + Id string `json:"id"` + Symbol string `json:"symbol"` + OpType string `json:"opType"` + Type string `json:"type"` + Side string `json:"side"` + Price string `json:"price"` + Size string `json:"size"` + Funds string `json:"funds"` + DealFunds string `json:"dealFunds"` + DealSize string `json:"dealSize"` + Fee string `json:"fee"` + FeeCurrency string `json:"feeCurrency"` + Stp string `json:"stp"` + Stop string `json:"stop"` + StopTriggered bool `json:"stopTriggered"` + StopPrice string `json:"stopPrice"` + TimeInForce string `json:"timeInForce"` + PostOnly bool `json:"postOnly"` + Hidden bool `json:"hidden"` + IceBerg bool `json:"iceberg"` + VisibleSize string `json:"visibleSize"` + CancelAfter uint64 `json:"cancelAfter"` + Channel string `json:"channel"` + ClientOid string `json:"clientOid"` + Remark string `json:"remark"` + Tags string `json:"tags"` + IsActive bool `json:"isActive"` + CancelExist bool `json:"cancelExist"` + CreatedAt int64 `json:"createdAt"` + TradeType string `json:"tradeType"` + Status string `json:"status"` + FailMsg string `json:"failMsg"` +} + +// A OrdersModel is the set of *OrderModel. +type OrdersModel []*OrderModel + +// Orders returns a list your current orders. +func (as *ApiService) Orders(symbol, side string) (*ApiResponse, error) { + params := map[string]string{ + "symbol": symbol, + "side": side, + } + pagination := &PaginationParam{CurrentPage: 1, PageSize: 10} + pagination.ReadParam(params) + req := NewRequest(http.MethodGet, "/api/v1/orders", params) + return as.Call(req) +} + +// A V1OrderModel represents a v1 order. +type V1OrderModel struct { + Symbol string `json:"symbol"` + DealPrice string `json:"dealPrice"` + DealValue string `json:"dealValue"` + Amount string `json:"amount"` + Fee string `json:"fee"` + Side string `json:"side"` + CreatedAt int64 `json:"createdAt"` +} + +// A V1OrdersModel is the set of *V1OrderModel. +type V1OrdersModel []*V1OrderModel + +// V1Orders returns a list of v1 historical orders. +func (as *ApiService) V1Orders() (*ApiResponse, error) { + params := map[string]string{ + //"symbol": symbol, + //"side": side, + } + pagination := &PaginationParam{CurrentPage: 1, PageSize: 10} + pagination.ReadParam(params) + req := NewRequest(http.MethodGet, "/api/v1/hist-orders", params) + return as.Call(req) +} + +// Order returns a single order by order id. +func (as *ApiService) Order(orderId string) (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/orders/"+orderId, nil) + return as.Call(req) +} + +// RecentOrders returns the recent orders of the latest transactions within 24 hours. +func (as *ApiService) RecentOrders() (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/limit/orders", nil) + return as.Call(req) +} diff --git a/sdk/pagination.go b/sdk/pagination.go new file mode 100644 index 0000000..c8c6ba8 --- /dev/null +++ b/sdk/pagination.go @@ -0,0 +1,28 @@ +package sdk + +import "encoding/json" + +// A PaginationParam represents the pagination parameters `currentPage` `pageSize` in a request . +type PaginationParam struct { + CurrentPage int64 + PageSize int64 +} + +// ReadParam read pagination parameters into params. +func (p *PaginationParam) ReadParam(params map[string]string) { + params["currentPage"], params["pageSize"] = IntToString(p.CurrentPage), IntToString(p.PageSize) +} + +// A PaginationModel represents the pagination in a response. +type PaginationModel struct { + CurrentPage int64 `json:"currentPage"` + PageSize int64 `json:"pageSize"` + TotalNum int64 `json:"totalNum"` + TotalPage int64 `json:"totalPage"` + RawItems json.RawMessage `json:"items"` // delay parsing +} + +// ReadItems read the `items` into v. +func (p *PaginationModel) ReadItems(v interface{}) error { + return json.Unmarshal(p.RawItems, v) +} diff --git a/sdk/service_status.go b/sdk/service_status.go new file mode 100644 index 0000000..b8da19f --- /dev/null +++ b/sdk/service_status.go @@ -0,0 +1,17 @@ +package sdk + +import ( + "net/http" +) + +// A ServiceStatusModel represents the structure of service status. +type ServiceStatusModel struct { + Status string `json:"status"` + Msg string `json:"msg"` +} + +// ServiceStatus returns the service status. +func (as *ApiService) ServiceStatus() (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/status", nil) + return as.Call(req) +} diff --git a/sdk/signer.go b/sdk/signer.go new file mode 100644 index 0000000..b40d085 --- /dev/null +++ b/sdk/signer.go @@ -0,0 +1,63 @@ +package sdk + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "time" +) + +// Signer interface contains Sign() method. +type Signer interface { + Sign(plain []byte) []byte +} + +// Sha256Signer is the sha256 Signer. +type Sha256Signer struct { + key []byte +} + +// Sign makes a signature by sha256. +func (ss *Sha256Signer) Sign(plain []byte) []byte { + hm := hmac.New(sha256.New, ss.key) + hm.Write(plain) + return hm.Sum(nil) +} + +// KcSigner is the implement of Signer for KuCoin. +type KcSigner struct { + Sha256Signer + apiKey string + apiSecret string + apiPassPhrase string +} + +// Sign makes a signature by sha256 with `apiKey` `apiSecret` `apiPassPhrase`. +func (ks *KcSigner) Sign(plain []byte) []byte { + s := ks.Sha256Signer.Sign(plain) + return []byte(base64.StdEncoding.EncodeToString(s)) +} + +// Headers returns a map of signature header. +func (ks *KcSigner) Headers(plain string) map[string]string { + t := IntToString(time.Now().UnixNano() / 1000000) + p := []byte(t + plain) + s := string(ks.Sign(p)) + return map[string]string{ + "KC-API-KEY": ks.apiKey, + "KC-API-PASSPHRASE": ks.apiPassPhrase, + "KC-API-TIMESTAMP": t, + "KC-API-SIGN": s, + } +} + +// NewKcSigner creates a instance of KcSigner. +func NewKcSigner(key, secret, passPhrase string) *KcSigner { + ks := &KcSigner{ + apiKey: key, + apiSecret: secret, + apiPassPhrase: passPhrase, + } + ks.key = []byte(secret) + return ks +} diff --git a/sdk/symbol.go b/sdk/symbol.go new file mode 100644 index 0000000..3245eec --- /dev/null +++ b/sdk/symbol.go @@ -0,0 +1,183 @@ +package sdk + +import ( + "net/http" + "time" +) + +// A SymbolModel represents an available currency pairs for trading. +type SymbolModel struct { + Symbol string `json:"symbol"` + Name string `json:"name"` + BaseCurrency string `json:"baseCurrency"` + QuoteCurrency string `json:"quoteCurrency"` + BaseMinSize string `json:"baseMinSize"` + QuoteMinSize string `json:"quoteMinSize"` + BaseMaxSize string `json:"baseMaxSize"` + QuoteMaxSize string `json:"quoteMaxSize"` + BaseIncrement string `json:"baseIncrement"` + QuoteIncrement string `json:"quoteIncrement"` + PriceIncrement string `json:"priceIncrement"` + FeeCurrency string `json:"feeCurrency"` + EnableTrading bool `json:"enableTrading"` + IsMarginEnabled bool `json:"isMarginEnabled"` + PriceLimitRate string `json:"priceLimitRate"` +} + +// A SymbolsModel is the set of *SymbolModel. +type SymbolsModel []*SymbolModel + +// Symbols returns a list of available currency pairs for trading. +func (as *ApiService) Symbols() (*ApiResponse, error) { + p := map[string]string{} + req := NewRequest(http.MethodGet, "/api/v1/symbols", p) + return as.Call(req) +} + +// A TickerLevel1Model represents ticker include only the inside (i.e. best) bid and ask data, last price and last trade size. +type TickerLevel1Model struct { + Sequence string `json:"sequence"` + Price string `json:"price"` + Size string `json:"size"` + BestBid string `json:"bestBid"` + BestBidSize string `json:"bestBidSize"` + BestAsk string `json:"bestAsk"` + BestAskSize string `json:"bestAskSize"` + Time int64 `json:"time"` +} + +// TickerLevel1 returns the ticker include only the inside (i.e. best) bid and ask data, last price and last trade size. +func (as *ApiService) TickerLevel1(symbol string) (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/market/orderbook/level1", map[string]string{"symbol": symbol}) + return as.Call(req) +} + +// A TickerModel represents a market ticker for all trading pairs in the market (including 24h volume). +type TickerModel struct { + Symbol string `json:"symbol"` + SymbolName string `json:"symbolName"` + Buy string `json:"buy"` + Sell string `json:"sell"` + ChangeRate string `json:"changeRate"` + ChangePrice string `json:"changePrice"` + High string `json:"high"` + Low string `json:"low"` + Vol string `json:"vol"` + VolValue string `json:"volValue"` + Last string `json:"last"` +} + +// A TickersModel is the set of *MarketTickerModel. +type TickersModel []*TickerModel + +// TickersResponseModel represents the response model of MarketTickers(). +type TickersResponseModel struct { + Time int64 `json:"time"` + Tickers TickersModel `json:"ticker"` +} + +// Tickers returns all tickers as TickersResponseModel for all trading pairs in the market (including 24h volume). +func (as *ApiService) Tickers() (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/market/allTickers", nil) + return as.Call(req) +} + +// A Stats24hrModel represents 24 hr stats for the symbol. +// Volume is in base currency units. +// Open, high, low are in quote currency units. +type Stats24hrModel struct { + Symbol string `json:"symbol"` + ChangeRate string `json:"changeRate"` + ChangePrice string `json:"changePrice"` + Open string `json:"open"` + Close string `json:"close"` + High string `json:"high"` + Low string `json:"low"` + Vol string `json:"vol"` + VolValue string `json:"volValue"` +} + +// Stats24hr returns 24 hr stats for the symbol. volume is in base currency units. open, high, low are in quote currency units. +func (as *ApiService) Stats24hr(symbol string) (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/market/stats", map[string]string{"symbol": symbol}) + return as.Call(req) +} + +// Markets returns the transaction currencies for the entire trading market. +func (as *ApiService) Markets() (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/markets", nil) + return as.Call(req) +} + +// A PartOrderBookModel represents a list of open orders for a symbol, a part of Order Book within 100 depth for each side(ask or bid). +type PartOrderBookModel struct { + Sequence string `json:"sequence"` + Time int64 `json:"time"` + Bids [][]string `json:"bids"` + Asks [][]string `json:"asks"` +} + +// AggregatedPartOrderBook returns a list of open orders(aggregated) for a symbol. +func (as *ApiService) AggregatedPartOrderBook(symbol string, depth string) (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/market/orderbook/level2_"+depth, map[string]string{"symbol": symbol}) + return as.Call(req) +} + +// A FullOrderBookModel represents a list of open orders for a symbol, with full depth. +type FullOrderBookModel struct { + Sequence string `json:"sequence"` + Time int64 `json:"time"` + Bids [][]string `json:"bids"` + Asks [][]string `json:"asks"` +} + +// AggregatedFullOrderBook returns a list of open orders(aggregated) for a symbol. +func (as *ApiService) AggregatedFullOrderBook(symbol string) (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v2/market/orderbook/level2", map[string]string{"symbol": symbol}) + return as.Call(req) +} + +// AtomicFullOrderBook returns a list of open orders for a symbol. +// Level-3 order book includes all bids and asks (non-aggregated, each item in Level-3 means a single order). +func (as *ApiService) AtomicFullOrderBook(symbol string) (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/market/orderbook/level3", map[string]string{"symbol": symbol}) + return as.Call(req) +} + +// A TradeHistoryModel represents a the latest trades for a symbol. +type TradeHistoryModel struct { + Sequence string `json:"sequence"` + Price string `json:"price"` + Size string `json:"size"` + Side string `json:"side"` + Time int64 `json:"time"` +} + +// A TradeHistoriesModel is the set of *TradeHistoryModel. +type TradeHistoriesModel []*TradeHistoryModel + +// TradeHistories returns a list the latest trades for a symbol. +func (as *ApiService) TradeHistories(symbol string) (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/market/histories", map[string]string{"symbol": symbol}) + return as.Call(req) +} + +// KLineModel represents the k lines for a symbol. +// Rates are returned in grouped buckets based on requested type. +type KLineModel []string + +// A KLinesModel is the set of *KLineModel. +type KLinesModel []*KLineModel + +// KLines returns the k lines for a symbol. +// Data are returned in grouped buckets based on requested type. +// Parameter #2 typo is the type of candlestick patterns. +func (as *ApiService) KLines(symbol, typo string) (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/market/candles", map[string]string{ + "symbol": symbol, + "type": typo, + "startAt": IntToString(time.Now().Unix() - 7*24*3600), + "endAt": IntToString(time.Now().Unix()), + }) + return as.Call(req) +} diff --git a/sdk/time.go b/sdk/time.go new file mode 100644 index 0000000..66a351e --- /dev/null +++ b/sdk/time.go @@ -0,0 +1,11 @@ +package sdk + +import ( + "net/http" +) + +// ServerTime returns the API server time. +func (as *ApiService) ServerTime() (*ApiResponse, error) { + req := NewRequest(http.MethodGet, "/api/v1/timestamp", nil) + return as.Call(req) +} diff --git a/sdk/websocket.go b/sdk/websocket.go new file mode 100644 index 0000000..7b2caf9 --- /dev/null +++ b/sdk/websocket.go @@ -0,0 +1,395 @@ +package sdk + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "log" + "math/rand" + "net/http" + "net/url" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// A WebSocketTokenModel contains a token and some servers for WebSocket feed. +type WebSocketTokenModel struct { + Token string `json:"token"` + Servers WebSocketServersModel `json:"instanceServers"` + AcceptUserMessage bool `json:"accept_user_message"` +} + +// A WebSocketServerModel contains some servers for WebSocket feed. +type WebSocketServerModel struct { + PingInterval int64 `json:"pingInterval"` + Endpoint string `json:"endpoint"` + Protocol string `json:"protocol"` + Encrypt bool `json:"encrypt"` + PingTimeout int64 `json:"pingTimeout"` +} + +// A WebSocketServersModel is the set of *WebSocketServerModel. +type WebSocketServersModel []*WebSocketServerModel + +// RandomServer returns a server randomly. +func (s WebSocketServersModel) RandomServer() (*WebSocketServerModel, error) { + l := len(s) + if l == 0 { + return nil, errors.New("No available server ") + } + return s[rand.Intn(l)], nil +} + +// WebSocketPublicToken returns the token for public channel. +func (as *ApiService) WebSocketPublicToken() (*ApiResponse, error) { + req := NewRequest(http.MethodPost, "/api/v1/bullet-public", map[string]string{}) + return as.Call(req) +} + +// WebSocketPrivateToken returns the token for private channel. +func (as *ApiService) WebSocketPrivateToken() (*ApiResponse, error) { + req := NewRequest(http.MethodPost, "/api/v1/bullet-private", map[string]string{}) + return as.Call(req) +} + +// All message types of WebSocket. +const ( + WelcomeMessage = "welcome" + PingMessage = "ping" + PongMessage = "pong" + SubscribeMessage = "subscribe" + AckMessage = "ack" + UnsubscribeMessage = "unsubscribe" + ErrorMessage = "error" + Message = "message" + Notice = "notice" + Command = "command" +) + +// A WebSocketMessage represents a message between the WebSocket client and server. +type WebSocketMessage struct { + Id string `json:"id"` + Type string `json:"type"` +} + +// A WebSocketSubscribeMessage represents a message to subscribe the public/private channel. +type WebSocketSubscribeMessage struct { + *WebSocketMessage + Topic string `json:"topic"` + PrivateChannel bool `json:"privateChannel"` + Response bool `json:"response"` +} + +// NewPingMessage creates a ping message instance. +func NewPingMessage() *WebSocketMessage { + return &WebSocketMessage{ + Id: IntToString(time.Now().UnixNano()), + Type: PingMessage, + } +} + +// NewSubscribeMessage creates a subscribe message instance. +func NewSubscribeMessage(topic string, privateChannel bool) *WebSocketSubscribeMessage { + return &WebSocketSubscribeMessage{ + WebSocketMessage: &WebSocketMessage{ + Id: IntToString(time.Now().UnixNano()), + Type: SubscribeMessage, + }, + Topic: topic, + PrivateChannel: privateChannel, + Response: true, + } +} + +// A WebSocketUnsubscribeMessage represents a message to unsubscribe the public/private channel. +type WebSocketUnsubscribeMessage WebSocketSubscribeMessage + +// NewUnsubscribeMessage creates a unsubscribe message instance. +func NewUnsubscribeMessage(topic string, privateChannel bool) *WebSocketUnsubscribeMessage { + return &WebSocketUnsubscribeMessage{ + WebSocketMessage: &WebSocketMessage{ + Id: IntToString(time.Now().UnixNano()), + Type: UnsubscribeMessage, + }, + Topic: topic, + PrivateChannel: privateChannel, + Response: true, + } +} + +// A WebSocketDownstreamMessage represents a message from the WebSocket server to client. +type WebSocketDownstreamMessage struct { + *WebSocketMessage + Sn string `json:"sn"` + Topic string `json:"topic"` + Subject string `json:"subject"` + RawData json.RawMessage `json:"data"` +} + +// ReadData read the data in channel. +func (m *WebSocketDownstreamMessage) ReadData(v interface{}) error { + return json.Unmarshal(m.RawData, v) +} + +// A WebSocketClient represents a connection to WebSocket server. +type WebSocketClient struct { + // Wait all goroutines quit + wg *sync.WaitGroup + // Stop subscribing channel + done chan struct{} + // Pong channel to check pong message + pongs chan string + // ACK channel to check pong message + acks chan string + // Error channel + errors chan error + // Downstream message channel + messages chan *WebSocketDownstreamMessage + conn *websocket.Conn + token *WebSocketTokenModel + server *WebSocketServerModel + enableHeartbeat bool + skipVerifyTls bool + timeout time.Duration +} + +var defaultTimeout = time.Second * 5 + +// WebSocketClientOpts defines the options for the client +// during the websocket connection. +type WebSocketClientOpts struct { + Token *WebSocketTokenModel + TLSSkipVerify bool + Timeout time.Duration +} + +// NewWebSocketClient creates an instance of WebSocketClient. +func (as *ApiService) NewWebSocketClient() *WebSocketClient { + // default to public token to create an instance of WebSocketClient. + rsp, err := as.WebSocketPublicToken() + if err != nil { + log.Fatalln("error when getting websocket public token:", err) + return nil + } + tk := &WebSocketTokenModel{} + if err := rsp.ReadData(tk); err != nil { + log.Fatalln("error when reading websocket public token:", err) + return nil + } + + return as.NewWebSocketClientOpts(WebSocketClientOpts{ + Token: tk, + TLSSkipVerify: as.apiSkipVerifyTls, + Timeout: defaultTimeout, + }) +} + +// NewWebSocketClientOpts creates an instance of WebSocketClient with the parsed options. +func (as *ApiService) NewWebSocketClientOpts(opts WebSocketClientOpts) *WebSocketClient { + wc := &WebSocketClient{ + wg: &sync.WaitGroup{}, + done: make(chan struct{}), + errors: make(chan error, 1), + pongs: make(chan string, 1), + acks: make(chan string, 1), + token: opts.Token, + messages: make(chan *WebSocketDownstreamMessage, 2048), + skipVerifyTls: opts.TLSSkipVerify, + timeout: opts.Timeout, + } + return wc +} + +// Connect connects the WebSocket server. +func (wc *WebSocketClient) Connect() (<-chan *WebSocketDownstreamMessage, <-chan error, error) { + // Find out a server + s, err := wc.token.Servers.RandomServer() + if err != nil { + return wc.messages, wc.errors, err + } + wc.server = s + + // Concat ws url + q := url.Values{} + q.Add("connectId", IntToString(time.Now().UnixNano())) + q.Add("token", wc.token.Token) + if wc.token.AcceptUserMessage == true { + q.Add("acceptUserMessage", "true") + } + u := fmt.Sprintf("%s?%s", s.Endpoint, q.Encode()) + + // Ignore verify tls + websocket.DefaultDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: wc.skipVerifyTls} + + // Connect ws server + websocket.DefaultDialer.ReadBufferSize = 2048000 //2000 kb + wc.conn, _, err = websocket.DefaultDialer.Dial(u, nil) + if err != nil { + return wc.messages, wc.errors, err + } + + // Must read the first welcome message + for { + m := &WebSocketDownstreamMessage{} + if err := wc.conn.ReadJSON(m); err != nil { + return wc.messages, wc.errors, err + } + if DebugMode { + logrus.Debugf("Received a WebSocket message: %s", ToJsonString(m)) + } + if m.Type == ErrorMessage { + return wc.messages, wc.errors, errors.Errorf("Error message: %s", ToJsonString(m)) + } + if m.Type == WelcomeMessage { + break + } + } + + wc.wg.Add(2) + go wc.read() + go wc.keepHeartbeat() + + return wc.messages, wc.errors, nil +} + +func (wc *WebSocketClient) read() { + defer func() { + close(wc.pongs) + close(wc.messages) + wc.wg.Done() + }() + + for { + select { + case <-wc.done: + return + default: + m := &WebSocketDownstreamMessage{} + if err := wc.conn.ReadJSON(m); err != nil { + wc.errors <- err + return + } + if DebugMode { + logrus.Debugf("Received a WebSocket message: %s", ToJsonString(m)) + } + // log.Printf("ReadJSON: %s", ToJsonString(m)) + switch m.Type { + case WelcomeMessage: + case PongMessage: + if wc.enableHeartbeat { + wc.pongs <- m.Id + } + case AckMessage: + // log.Printf("Subscribed: %s==%s? %s", channel.Id, m.Id, channel.Topic) + wc.acks <- m.Id + case ErrorMessage: + wc.errors <- errors.Errorf("Error message: %s", ToJsonString(m)) + return + case Message, Notice, Command: + wc.messages <- m + default: + wc.errors <- errors.Errorf("Unknown message type: %s", m.Type) + } + } + } +} + +func (wc *WebSocketClient) keepHeartbeat() { + wc.enableHeartbeat = true + // New ticker to send ping message + pt := time.NewTicker(time.Duration(wc.server.PingInterval)*time.Millisecond - time.Millisecond*200) + defer wc.wg.Done() + defer pt.Stop() + + for { + select { + case <-wc.done: + return + case <-pt.C: + p := NewPingMessage() + m := ToJsonString(p) + if DebugMode { + logrus.Debugf("Sent a WebSocket message: %s", m) + } + if err := wc.conn.WriteMessage(websocket.TextMessage, []byte(m)); err != nil { + wc.errors <- err + return + } + + // log.Printf("Ping: %s", ToJsonString(p)) + // Waiting (with timeout) for the server to response pong message + // If timeout, close this connection + select { + case pid := <-wc.pongs: + if pid != p.Id { + wc.errors <- errors.Errorf("Invalid pong id %s, expect %s", pid, p.Id) + return + } + case <-time.After(time.Duration(wc.server.PingTimeout) * time.Millisecond): + wc.errors <- errors.Errorf("Wait pong message timeout in %d ms", wc.server.PingTimeout) + return + } + } + } +} + +// Subscribe subscribes the specified channel. +func (wc *WebSocketClient) Subscribe(channels ...*WebSocketSubscribeMessage) error { + for _, c := range channels { + m := ToJsonString(c) + if DebugMode { + logrus.Debugf("Sent a WebSocket message: %s", m) + } + if err := wc.conn.WriteMessage(websocket.TextMessage, []byte(m)); err != nil { + return err + } + //log.Printf("Subscribing: %s, %s", c.Id, c.Topic) + select { + case id := <-wc.acks: + //log.Printf("ack: %s=>%s", id, c.Id) + if id != c.Id { + return errors.Errorf("Invalid ack id %s, expect %s", id, c.Id) + } + case err := <-wc.errors: + return errors.Errorf("Subscribe failed, %s", err.Error()) + case <-time.After(wc.timeout): + return errors.Errorf("Wait ack message timeout in %v", wc.timeout) + } + } + return nil +} + +// Unsubscribe unsubscribes the specified channel. +func (wc *WebSocketClient) Unsubscribe(channels ...*WebSocketUnsubscribeMessage) error { + for _, c := range channels { + m := ToJsonString(c) + if DebugMode { + logrus.Debugf("Sent a WebSocket message: %s", m) + } + if err := wc.conn.WriteMessage(websocket.TextMessage, []byte(m)); err != nil { + return err + } + //log.Printf("Unsubscribing: %s, %s", c.Id, c.Topic) + select { + case id := <-wc.acks: + //log.Printf("ack: %s=>%s", id, c.Id) + if id != c.Id { + return errors.Errorf("Invalid ack id %s, expect %s", id, c.Id) + } + case <-time.After(wc.timeout): + return errors.Errorf("Wait ack message timeout in %v", wc.timeout) + } + } + return nil +} + +// Stop stops subscribing the specified channel, all goroutines quit. +func (wc *WebSocketClient) Stop() { + close(wc.done) + _ = wc.conn.Close() + wc.wg.Wait() +} diff --git a/sdk/withdrawal.go b/sdk/withdrawal.go new file mode 100644 index 0000000..368bddb --- /dev/null +++ b/sdk/withdrawal.go @@ -0,0 +1,85 @@ +package sdk + +import ( + "net/http" +) + +// A WithdrawalModel represents a withdrawal. +type WithdrawalModel struct { + Id string `json:"id"` + Address string `json:"address"` + Memo string `json:"memo"` + Currency string `json:"currency"` + Amount string `json:"amount"` + Fee string `json:"fee"` + WalletTxId string `json:"walletTxId"` + IsInner bool `json:"isInner"` + Status string `json:"status"` + Remark string `json:"remark"` + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` +} + +// A WithdrawalsModel is the set of *WithdrawalModel. +type WithdrawalsModel []*WithdrawalModel + +// Withdrawals returns a list of withdrawals. +func (as *ApiService) Withdrawals() (*ApiResponse, error) { + params := map[string]string{} + pagination := &PaginationParam{CurrentPage: 1, PageSize: 10} + pagination.ReadParam(params) + req := NewRequest(http.MethodGet, "/api/v1/withdrawals", params) + return as.Call(req) +} + +// A WithdrawalQuotasModel represents the quotas for a currency. +type WithdrawalQuotasModel struct { + Currency string `json:"currency"` + AvailableAmount string `json:"availableAmount"` + RemainAmount string `json:"remainAmount"` + WithdrawMinSize string `json:"withdrawMinSize"` + LimitBTCAmount string `json:"limitBTCAmount"` + InnerWithdrawMinFee string `json:"innerWithdrawMinFee"` + UsedBTCAmount string `json:"usedBTCAmount"` + IsWithdrawEnabled bool `json:"isWithdrawEnabled"` + WithdrawMinFee string `json:"withdrawMinFee"` + Precision uint8 `json:"precision"` +} + +// WithdrawalQuotas returns the quotas of withdrawal. +func (as *ApiService) WithdrawalQuotas(currency string) (*ApiResponse, error) { + params := map[string]string{"currency": currency} + req := NewRequest(http.MethodGet, "/api/v1/withdrawals/quotas", params) + return as.Call(req) +} + +// ApplyWithdrawalResultModel represents the result of ApplyWithdrawal(). +type ApplyWithdrawalResultModel struct { + WithdrawalId string `json:"withdrawalId"` +} + +// ApplyWithdrawal applies a withdrawal. +func (as *ApiService) ApplyWithdrawal(currency, address, amount string) (*ApiResponse, error) { + p := map[string]string{ + "currency": currency, + "address": address, + "amount": amount, + } + options := map[string]string{} + for k, v := range options { + p[k] = v + } + req := NewRequest(http.MethodPost, "/api/v1/withdrawals", p) + return as.Call(req) +} + +// CancelWithdrawalResultModel represents the result of CancelWithdrawal(). +type CancelWithdrawalResultModel struct { + CancelledWithdrawIds []string `json:"cancelledWithdrawIds"` +} + +// CancelWithdrawal cancels a withdrawal by withdrawalId. +func (as *ApiService) CancelWithdrawal(withdrawalId string) (*ApiResponse, error) { + req := NewRequest(http.MethodDelete, "/api/v1/withdrawals/"+withdrawalId, nil) + return as.Call(req) +}