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)
+}