diff --git a/README.md b/README.md index 2c63864..8544a64 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,13 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors-) +[![Go Reference](https://pkg.go.dev/badge/github.com/nao1215/coincheck.svg)](https://pkg.go.dev/github.com/nao1215/coincheck) +![Coverage](https://raw.githubusercontent.com/nao1215/octocovs-central-repo/main/badges/nao1215/coincheck/coverage.svg) +[![MultiPlatformUnitTest](https://github.com/nao1215/coincheck/actions/workflows/unit_test.yml/badge.svg)](https://github.com/nao1215/coincheck/actions/workflows/unit_test.yml) +[![reviewdog](https://github.com/nao1215/coincheck/actions/workflows/reviewdog.yml/badge.svg)](https://github.com/nao1215/coincheck/actions/workflows/reviewdog.yml) +[![gitleaks](https://github.com/nao1215/coincheck/actions/workflows/gitleak.yml/badge.svg)](https://github.com/nao1215/coincheck/actions/workflows/gitleak.yml) + + >[!IMPORTANT] > This library is under development and is not yet ready for production use. @@ -12,8 +19,8 @@ The coincheck package is a client for the API provided by Coincheck, Inc., which - Private API: Requires authentication using the API Key and API Secret issued by the Coincheck service. The coincheck package supports both Public and Private APIs. -- [Official API documentation](https://coincheck.com/documents/exchange/api) -- [Official API client](https://github.com/coincheckjp/coincheck-go) +- [Coincheck official API documentation](https://coincheck.com/documents/exchange/api) +- [Coincheck official API client](https://github.com/coincheckjp/coincheck-go) ## Supported OS and go version @@ -78,7 +85,8 @@ If you want to execute the Private API, you need to create a client with the API | API | Method Name |Description | | :--- | :--- | :--- | -| GET /api/ticker | GetTicker() | Check latest ticker information | +| GET /api/ticker | GetTicker() | Check latest ticker information. | +| GET /api/trades | GetTrades() | You can get current order transactions. | ### Private API diff --git a/coincheck.go b/coincheck.go index 48b1f59..94691d3 100644 --- a/coincheck.go +++ b/coincheck.go @@ -9,6 +9,8 @@ package coincheck import ( "context" + "encoding/json" + "fmt" "io" "net/http" "net/url" @@ -108,3 +110,21 @@ func (c *Client) createRequest(ctx context.Context, input createRequestInput) (* req.Header.Add("cache-control", "no-cache") return req, nil } + +// Do sends an HTTP request and returns an HTTP response. +func (c *Client) do(req *http.Request, output any) error { + resp, err := c.client.Do(req) + if err != nil { + return withPrefixError(err) + } + defer resp.Body.Close() //nolint: errcheck // ignore error + + if resp.StatusCode != http.StatusOK { + return withPrefixError(fmt.Errorf("unexpected status code=%d", resp.StatusCode)) + } + + if err := json.NewDecoder(resp.Body).Decode(output); err != nil { + return withPrefixError(err) + } + return nil +} diff --git a/order.go b/order.go new file mode 100644 index 0000000..304ac6d --- /dev/null +++ b/order.go @@ -0,0 +1,11 @@ +package coincheck + +// OrderType represents the order type. +type OrderType string + +const ( + // OrderTypeBuy is the order type of buy. + OrderTypeBuy OrderType = "buy" + // OrderTypeSell is the order type of sell. + OrderTypeSell OrderType = "sell" +) diff --git a/pagenation.go b/pagenation.go new file mode 100644 index 0000000..183cc59 --- /dev/null +++ b/pagenation.go @@ -0,0 +1,26 @@ +package coincheck + +// Pagination represents the pagination of coincheck API. +// It is possible to get by dividing the data. +type Pagination struct { + // Limit is the number of data to get. + Limit int `json:"limit"` + // PaginationOrder is the order of the data. You can specify "desc" or "asc". + PaginationOrder PaginationOrder `json:"order"` + // StartingAfter is the ID of the data to start getting. + // Greater than the specified ID. For example, if you specify 3, you will get data from ID 4. + StartingAfter int `json:"starting_after,omitempty"` + // EndingBefore is the ID of the data to end getting. + // Less than the specified ID. For example, if you specify 3, you will get data up to ID 2. + EndingBefore int `json:"ending_before,omitempty"` +} + +// PaginationOrder represents the order of the pagination. +type PaginationOrder string + +const ( + // PaginationOrderDesc is the order of the pagination in descending order. + PaginationOrderDesc PaginationOrder = "desc" + // PaginationOrderAsc is the order of the pagination in ascending order. + PaginationOrderAsc PaginationOrder = "asc" +) diff --git a/ticker.go b/ticker.go index ce13608..db343e4 100644 --- a/ticker.go +++ b/ticker.go @@ -2,8 +2,6 @@ package coincheck import ( "context" - "encoding/json" - "fmt" "net/http" ) @@ -76,20 +74,9 @@ func (c *Client) GetTicker(ctx context.Context, input GetTickerInput) (*GetTicke return nil, err } - resp, err := c.client.Do(req) - if err != nil { - return nil, withPrefixError(err) - } - defer resp.Body.Close() //nolint: errcheck // ignore error - - if resp.StatusCode != http.StatusOK { - fmt.Println(req.URL) - return nil, withPrefixError(fmt.Errorf("unexpected status code=%d", resp.StatusCode)) - } - var output GetTickerResponse - if err := json.NewDecoder(resp.Body).Decode(&output); err != nil { - return nil, withPrefixError(err) + if err := c.do(req, &output); err != nil { + return nil, err } return &output, nil } diff --git a/trade.go b/trade.go new file mode 100644 index 0000000..bf694be --- /dev/null +++ b/trade.go @@ -0,0 +1,60 @@ +package coincheck + +import ( + "context" + "net/http" +) + +// GetTradesInput represents the input parameter for GetTrades +type GetTradesInput struct { + // Pair is the pair of the currency. e.g. btc_jpy. + Pair Pair +} + +// GetTradesResponse represents the output from GetTrades +type GetTradesResponse struct { + // Success is a boolean value that indicates the success of the API call. + Success bool `json:"success"` + // Pagination is the pagination of the data. + Pagination Pagination `json:"pagination"` + // Data is a list of trades. + Data []Trade `json:"data"` +} + +// Trade represents a trade. +type Trade struct { + // ID is the trade ID. + ID int `json:"id"` + // Amount is the amount of the trade. + Amount float64 `json:"amount"` + // Rate is the rate of the trade. + Rate float64 `json:"rate"` + // Pair is the pair of the currency. + Pair Pair `json:"pair"` + // OrderType is the order type. + OrderType OrderType `json:"order_type"` + // CreatedAt is the creation time of the trade. + CreatedAt string `json:"created_at"` +} + +// GetTrades returns a list of trades (order transactions). +// API: GET /api/trades +// Visibility: Public +func (c *Client) GetTrades(ctx context.Context, input GetTradesInput) (*GetTradesResponse, error) { + req, err := c.createRequest(ctx, createRequestInput{ + method: http.MethodGet, + path: "/api/trades", + queryParam: map[string]string{ + "pair": string(input.Pair), + }, + }) + if err != nil { + return nil, err + } + + var output GetTradesResponse + if err := c.do(req, &output); err != nil { + return nil, err + } + return &output, nil +} diff --git a/trade_test.go b/trade_test.go new file mode 100644 index 0000000..8cb50e3 --- /dev/null +++ b/trade_test.go @@ -0,0 +1,125 @@ +package coincheck + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestClient_GetTrades(t *testing.T) { + t.Run("In the case of a successful GET /api/trades request", func(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + wantMethod := http.MethodGet + if got := r.Method; got != wantMethod { + t.Errorf("Method: got %v, want %v", got, wantMethod) + } + + wantEndpoint := "/api/trades" + if got := r.URL.Path; got != wantEndpoint { + t.Errorf("Endpoint: got %v, want %v", got, wantEndpoint) + } + + wantPair := PairETCJPY + if got := r.URL.Query().Get("pair"); got != wantPair.String() { + t.Errorf("Pair: got %v, want %v", got, wantPair) + } + + result := GetTradesResponse{ + Success: true, + Pagination: Pagination{ + Limit: 1, + PaginationOrder: "desc", + StartingAfter: 0, + EndingBefore: 0, + }, + Data: []Trade{ + { + ID: 1, + Amount: 1, + Rate: 1000000, + Pair: PairETCJPY, + OrderType: OrderTypeBuy, + CreatedAt: "2021-01-01T00:00:00Z", + }, + { + ID: 2, + Amount: 2, + Rate: 2000000, + Pair: PairETCJPY, + OrderType: OrderTypeSell, + CreatedAt: "2021-01-02T00:00:00Z", + }, + }, + } + if err := json.NewEncoder(w).Encode(result); err != nil { + t.Fatal(err) + } + })) + + client, err := NewClient(WithBaseURL(testServer.URL)) + if err != nil { + t.Fatal(err) + } + + input := GetTradesInput{ + Pair: PairETCJPY, + } + got, err := client.GetTrades(context.Background(), input) + if err != nil { + t.Fatal(err) + } + + want := &GetTradesResponse{ + Success: true, + Pagination: Pagination{ + Limit: 1, + PaginationOrder: PaginationOrderDesc, + StartingAfter: 0, + EndingBefore: 0, + }, + Data: []Trade{ + { + ID: 1, + Amount: 1, + Rate: 1000000, + Pair: PairETCJPY, + OrderType: OrderTypeBuy, + CreatedAt: "2021-01-01T00:00:00Z", + }, + { + ID: 2, + Amount: 2, + Rate: 2000000, + Pair: PairETCJPY, + OrderType: OrderTypeSell, + CreatedAt: "2021-01-02T00:00:00Z", + }, + }, + } + if diff := cmp.Diff(want, got); diff != "" { + printDiff(t, diff) + } + }) + + t.Run("In the case of a failed GET /api/trades request", func(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + + client, err := NewClient(WithBaseURL(testServer.URL)) + if err != nil { + t.Fatal(err) + } + + input := GetTradesInput{ + Pair: PairETCJPY, + } + if _, err = client.GetTrades(context.Background(), input); err == nil { + t.Fatal("err must not be nil") + } + }) +} diff --git a/withdraw.go b/withdraw.go index ade37dd..2234077 100644 --- a/withdraw.go +++ b/withdraw.go @@ -2,8 +2,6 @@ package coincheck import ( "context" - "encoding/json" - "fmt" "net/http" ) @@ -46,19 +44,9 @@ func (c *Client) GetBankAccounts(ctx context.Context) (*GetBankAccountsResponse, return nil, err } - resp, err := c.client.Do(req) - if err != nil { - return nil, withPrefixError(err) - } - defer resp.Body.Close() //nolint: errcheck // ignore error - - if resp.StatusCode != http.StatusOK { - return nil, withPrefixError(fmt.Errorf("unexpected status code=%d", resp.StatusCode)) - } - var output GetBankAccountsResponse - if err := json.NewDecoder(resp.Body).Decode(&output); err != nil { - return nil, withPrefixError(err) + if err := c.do(req, &output); err != nil { + return nil, err } return &output, nil }