Skip to content

Commit

Permalink
Add IPN secret and hmac compute
Browse files Browse the repository at this point in the history
  • Loading branch information
AchoArnold committed May 22, 2022
1 parent b0cf540 commit fcaf515
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 43 deletions.
49 changes: 26 additions & 23 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -26,6 +33,7 @@ type Client struct {
apiKey string
apiSecret string
version string
ipnSecret string

Payment *paymentService
}
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions client_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type clientConfig struct {
version string
apiKey string
apiSecret string
ipnSecret string
baseURL string
}

Expand Down
7 changes: 7 additions & 0 deletions client_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
11 changes: 11 additions & 0 deletions internal/stubs/payment.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package stubs

// CreatePaymentsOkResponse response from the create payments endpoint
func CreatePaymentsOkResponse() []byte {
return []byte(`
{
Expand All @@ -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":[]
}
`)
}
27 changes: 26 additions & 1 deletion payment_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
73 changes: 56 additions & 17 deletions payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

0 comments on commit fcaf515

Please sign in to comment.