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