From fcaf515e71076ab53ee6467ede5efc088a3c9dce Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 22 May 2022 16:05:47 +0300 Subject: [PATCH] Add IPN secret and hmac compute --- client.go | 49 ++++++++++++++------------ client_config.go | 1 + client_option.go | 7 ++++ go.mod | 6 ++-- internal/stubs/payment.go | 11 ++++++ payment_service.go | 27 ++++++++++++++- payments.go | 73 ++++++++++++++++++++++++++++++--------- 7 files changed, 131 insertions(+), 43 deletions(-) diff --git a/client.go b/client.go index df4ee8a..b70864e 100644 --- a/client.go +++ b/client.go @@ -9,14 +9,21 @@ import ( "io/ioutil" "net/http" "net/url" - strconv "strconv" + "strconv" "strings" + + "github.com/davecgh/go-spew/spew" ) type service struct { client *Client } +const ( + // HeaderNameHMAC is used for authentication + HeaderNameHMAC = "HMAC" +) + // Client is the coinpayments API client. // Do not instantiate this client with Client{}. Use the New method instead. type Client struct { @@ -26,6 +33,7 @@ type Client struct { apiKey string apiSecret string version string + ipnSecret string Payment *paymentService } @@ -44,6 +52,7 @@ func New(options ...Option) *Client { apiSecret: config.apiSecret, httpClient: config.httpClient, baseURL: config.baseURL, + ipnSecret: config.ipnSecret, } client.common.client = client @@ -55,41 +64,30 @@ func New(options ...Option) *Client { // in which case it is resolved relative to the BaseURL of the Client. // URI's should always be specified without a preceding slash. func (client *Client) newRequest(ctx context.Context, method, cmd string, body url.Values) (*http.Request, error) { + body.Add("cmd", cmd) + body.Add("key", client.apiKey) + body.Add("format", "json") + body.Add("version", client.version) + req, err := http.NewRequestWithContext(ctx, method, client.baseURL, strings.NewReader(body.Encode())) if err != nil { return nil, err } - client.addURLParams(req, map[string]string{ - "cmd": cmd, - "key": client.apiKey, - "format": "json", - "version": client.version, - }) - // generate hmac hash of data and private key - hash, err := client.computeHMAC(body.Encode()) + hash, err := client.computeHMAC(body.Encode(), client.apiSecret) if err != nil { return nil, err } - req.Header.Add("HMAC", hash) + spew.Dump(hash) + req.Header.Add(HeaderNameHMAC, hash) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Content-Length", strconv.Itoa(len(body.Encode()))) return req, nil } -// addURLParams adds urls parameters to an *http.Request -func (client *Client) addURLParams(request *http.Request, params map[string]string) *http.Request { - q := request.URL.Query() - for key, value := range params { - q.Add(key, value) - } - request.URL.RawQuery = q.Encode() - return request -} - // do carries out an HTTP request and returns a Response func (client *Client) do(req *http.Request) (*Response, error) { if req == nil { @@ -134,11 +132,16 @@ func (client *Client) newResponse(httpResponse *http.Response) (*Response, error return resp, resp.Error() } -// computeHMAC returns our hmac because on the secret key of our account -func (client *Client) computeHMAC(data string) (string, error) { - hash := hmac.New(sha512.New, []byte(client.apiSecret)) +// computeHMAC returns the hmac hash of the data +func (client *Client) computeHMAC(data string, secret string) (string, error) { + hash := hmac.New(sha512.New, []byte(secret)) if _, err := hash.Write([]byte(data)); err != nil { return "", err } return fmt.Sprintf("%x", hash.Sum(nil)), nil } + +// IpnHMAC returns the hmac hash of the data +func (client *Client) IpnHMAC(data string) (string, error) { + return client.computeHMAC(data, client.ipnSecret) +} diff --git a/client_config.go b/client_config.go index 1018218..6a1bb19 100644 --- a/client_config.go +++ b/client_config.go @@ -7,6 +7,7 @@ type clientConfig struct { version string apiKey string apiSecret string + ipnSecret string baseURL string } diff --git a/client_option.go b/client_option.go index 86941a2..d51097a 100644 --- a/client_option.go +++ b/client_option.go @@ -48,3 +48,10 @@ func WithAPISecret(apiSecret string) Option { config.apiSecret = apiSecret }) } + +// WithIPNSecret the coinpayments IPN secret +func WithIPNSecret(ipnSecret string) Option { + return clientOptionFunc(func(config *clientConfig) { + config.ipnSecret = ipnSecret + }) +} diff --git a/go.mod b/go.mod index bb95f95..5d54861 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,12 @@ module github.com/NdoleStudio/coinpayments-go go 1.18 -require github.com/stretchr/testify v1.7.1 +require ( + github.com/davecgh/go-spew v1.1.1 + github.com/stretchr/testify v1.7.1 +) require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/internal/stubs/payment.go b/internal/stubs/payment.go index b327444..d87680a 100644 --- a/internal/stubs/payment.go +++ b/internal/stubs/payment.go @@ -1,5 +1,6 @@ package stubs +// CreatePaymentsOkResponse response from the create payments endpoint func CreatePaymentsOkResponse() []byte { return []byte(` { @@ -15,6 +16,16 @@ func CreatePaymentsOkResponse() []byte { "status_url":"https:\/\/www.coinpayments.net\/index.php?cmd=status&id=XXX&key=ZZZ", "qrcode_url":"https:\/\/www.coinpayments.net\/qrgen.php?id=XXX&key=ZZZ" } + } +`) } + +// GetTransactionErrorResponse invalid transaction response +func GetTransactionErrorResponse() []byte { + return []byte(` + { + "error":"Invalid API version - no version specified", + "result":[] + } `) } diff --git a/payment_service.go b/payment_service.go index ec68049..1d774a0 100644 --- a/payment_service.go +++ b/payment_service.go @@ -19,7 +19,7 @@ func (service *paymentService) CreateTransaction(ctx context.Context, params *Cr payload.Add("currency1", params.OriginalCurrency) payload.Add("currency2", params.SendingCurrency) - request, err := service.client.newRequest(ctx, http.MethodGet, "create_transaction", payload) + request, err := service.client.newRequest(ctx, http.MethodPost, "create_transaction", payload) if err != nil { return nil, nil, err } @@ -36,3 +36,28 @@ func (service *paymentService) CreateTransaction(ctx context.Context, params *Cr return createPaymentResponse, response, nil } + +// GetTransaction fetches information on a transaction +// +// API Docs: https://www.coinpayments.net/apidoc-get-tx-info +func (service *paymentService) GetTransaction(ctx context.Context, transactionID string) (*map[string]interface{}, *Response, error) { + payload := url.Values{} + payload.Add("txid", transactionID) + + request, err := service.client.newRequest(ctx, http.MethodPost, "get_tx_info", payload) + if err != nil { + return nil, nil, err + } + + response, err := service.client.do(request) + if err != nil { + return nil, response, err + } + + createPaymentResponse := new(map[string]interface{}) + if err = json.Unmarshal(*response.Body, createPaymentResponse); err != nil { + return nil, response, err + } + + return createPaymentResponse, response, nil +} diff --git a/payments.go b/payments.go index 1108ad7..d942074 100644 --- a/payments.go +++ b/payments.go @@ -29,21 +29,60 @@ type CreatePaymentRequest struct { // PaymentIpnRequest is the response we expect back from the server when the command is "api" type PaymentIpnRequest struct { - Status string `json:"status"` - StatusText string `json:"status_text"` - TxnID string `json:"txn_id"` - Currency1 string `json:"currency1"` - Currency2 string `json:"currency2"` - Amount1 string `json:"amount1"` - Amount2 string `json:"amount2"` - Fee string `json:"fee"` - BuyerName string `json:"buyer_name"` - Email string `json:"email"` - ItemName string `json:"item_name"` - ItemNumber string `json:"item_number"` - Invoice string `json:"invoice"` - Custom string `json:"custom"` - SendTX string `json:"send_tx"` // the tx id of the payment to the merchant. only included when 'status' >= 100 and the payment mode is set to ASAP or nightly or if the payment is paypal passthru - ReceivedAmount string `json:"received_amount"` - ReceivedConfirms string `json:"received_confirms"` + Status string `form:"status"` + StatusText string `form:"status_text"` + TxnID string `form:"txn_id"` + Currency1 string `form:"currency1"` + Currency2 string `form:"currency2"` + Amount1 string `form:"amount1"` + Amount2 string `form:"amount2"` + Fee string `form:"fee"` + IpnType string `form:"ipn_type"` + BuyerName string `form:"buyer_name"` + Email string `form:"email"` + ItemName string `form:"item_name"` + ItemNumber string `form:"item_number"` + Invoice string `form:"invoice"` + Custom string `form:"custom"` + ReceivedAmount string `form:"received_amount"` + ReceivedConfirms string `form:"received_confirms"` +} + +// IsWaiting returns true when the payment is in the waiting state +func (request PaymentIpnRequest) IsWaiting() bool { + return request.Status == "0" || request.Status == "1" || request.Status == "2" || request.Status == "3" +} + +// IsComplete returns true with the payment is completed +func (request PaymentIpnRequest) IsComplete() bool { + return request.Status == "100" +} + +// IsFailed returns ttrue when the payment is failed +func (request PaymentIpnRequest) IsFailed() bool { + return request.Status == "-2" || request.Status == "-1" +} + +// PaymentTransactionResponse is the response gotten when we fetch a transaction +type PaymentTransactionResponse struct { + Error string `json:"error"` + Result PaymentTransaction `json:"result"` +} + +// PaymentTransaction is the transaction details +type PaymentTransaction struct { + TimeCreated int `json:"time_created"` + TimeExpires int `json:"time_expires"` + Status int `json:"status"` + StatusText string `json:"status_text"` + Type string `json:"type"` + Coin string `json:"coin"` + Amount int `json:"amount"` + AmountFormatted string `json:"amountf"` + Received int `json:"received"` + ReceivedFormatted string `json:"receivedf"` + ReceiveConfirms int `json:"recv_confirms"` + PaymentAddress string `json:"payment_address"` + TimeCompleted int `json:"time_completed"` + SenderIP string `json:"sender_ip"` }