1// Copyright (C) 2019 Storj Labs, Inc.
2// See LICENSE for copying information.
3
4package coinpayments
5
6import (
7	"bytes"
8	"context"
9	"crypto/hmac"
10	"crypto/sha512"
11	"encoding/hex"
12	"encoding/json"
13	"net/http"
14	"net/url"
15
16	"github.com/zeebo/errs"
17)
18
19// Error is error class API errors.
20var Error = errs.Class("coinpayments client")
21
22// ErrMissingPublicKey is returned when Coinpayments client is missing public key.
23var ErrMissingPublicKey = errs.Class("missing public key")
24
25// Credentials contains public and private API keys for client.
26type Credentials struct {
27	PublicKey  string
28	PrivateKey string
29}
30
31type httpClient interface {
32	Do(*http.Request) (*http.Response, error)
33}
34
35// Client handles base API processing.
36type Client struct {
37	creds Credentials
38	http  httpClient
39}
40
41// NewClient creates new instance of client with provided credentials.
42func NewClient(creds Credentials) *Client {
43	client := &Client{
44		creds: creds,
45		http: &http.Client{
46			Timeout: 0,
47		},
48	}
49	return client
50}
51
52// Transactions returns transactions API.
53func (c *Client) Transactions() Transactions {
54	return Transactions{client: c}
55}
56
57// ConversionRates returns ConversionRates API.
58func (c *Client) ConversionRates() ConversionRates {
59	return ConversionRates{client: c}
60}
61
62// do handles base API request routines.
63func (c *Client) do(ctx context.Context, cmd string, values url.Values) (_ json.RawMessage, err error) {
64	if c.creds.PublicKey == "" {
65		return nil, Error.Wrap(ErrMissingPublicKey.New(""))
66	}
67
68	values.Set("version", "1")
69	values.Set("format", "json")
70	values.Set("key", c.creds.PublicKey)
71	values.Set("cmd", cmd)
72
73	encoded := values.Encode()
74
75	buff := bytes.NewBufferString(encoded)
76
77	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://www.coinpayments.net/api.php", buff)
78	if err != nil {
79		return nil, err
80	}
81
82	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
83	req.Header.Set("HMAC", c.hmac([]byte(encoded)))
84
85	resp, err := c.http.Do(req.WithContext(ctx))
86	if err != nil {
87		return nil, err
88	}
89
90	defer func() {
91		err = errs.Combine(err, resp.Body.Close())
92	}()
93
94	if resp.StatusCode != http.StatusOK {
95		return nil, errs.New("internal server error")
96	}
97
98	var data struct {
99		Error  string          `json:"error"`
100		Result json.RawMessage `json:"result"`
101	}
102
103	if err = json.NewDecoder(resp.Body).Decode(&data); err != nil {
104		return nil, err
105	}
106
107	if data.Error != "ok" {
108		return nil, errs.New(data.Error)
109	}
110
111	return data.Result, nil
112}
113
114// hmac returns string representation of HMAC signature
115// signed with clients private key.
116func (c *Client) hmac(payload []byte) string {
117	mac := hmac.New(sha512.New, []byte(c.creds.PrivateKey))
118	_, _ = mac.Write(payload)
119	return hex.EncodeToString(mac.Sum(nil))
120}
121