183 lines
4.9 KiB
Go
183 lines
4.9 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
var httpClient = &http.Client{Timeout: 30 * time.Second}
|
|
|
|
type NowPaymentsClient struct {
|
|
APIKey string
|
|
IPNSecret string
|
|
BaseURL string
|
|
}
|
|
|
|
func NewNowPaymentsClient(apiKey, ipnSecret string) *NowPaymentsClient {
|
|
return &NowPaymentsClient{
|
|
APIKey: apiKey,
|
|
IPNSecret: ipnSecret,
|
|
BaseURL: "https://api.nowpayments.io/v1",
|
|
}
|
|
}
|
|
|
|
type CreatePaymentRequest struct {
|
|
PriceAmount float64 `json:"price_amount"`
|
|
PriceCurrency string `json:"price_currency"`
|
|
PayCurrency string `json:"pay_currency"`
|
|
OrderID string `json:"order_id"`
|
|
IPNCallbackURL string `json:"ipn_callback_url"`
|
|
}
|
|
|
|
type CreatePaymentResponse struct {
|
|
PaymentID json.Number `json:"payment_id"`
|
|
PayAddress string `json:"pay_address"`
|
|
PayAmount json.Number `json:"pay_amount"`
|
|
PayCurrency string `json:"pay_currency"`
|
|
PriceAmount json.Number `json:"price_amount"`
|
|
PriceCurrency string `json:"price_currency"`
|
|
Status string `json:"payment_status"`
|
|
OrderID string `json:"order_id"`
|
|
}
|
|
|
|
type IPNPayload struct {
|
|
PaymentID json.Number `json:"payment_id"`
|
|
PaymentStatus string `json:"payment_status"`
|
|
PayAddress string `json:"pay_address"`
|
|
PriceAmount json.Number `json:"price_amount"`
|
|
PriceCurrency string `json:"price_currency"`
|
|
PayAmount json.Number `json:"pay_amount"`
|
|
PayCurrency string `json:"pay_currency"`
|
|
OrderID string `json:"order_id"`
|
|
}
|
|
|
|
func (c *NowPaymentsClient) CreatePayment(req *CreatePaymentRequest) (*CreatePaymentResponse, error) {
|
|
body, err := json.Marshal(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
httpReq, err := http.NewRequest("POST", c.BaseURL+"/payment", bytes.NewBuffer(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
httpReq.Header.Set("x-api-key", c.APIKey)
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := httpClient.Do(httpReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("nowpayments error (%d): %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var result CreatePaymentResponse
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
type GetPaymentStatusResponse struct {
|
|
PaymentID json.Number `json:"payment_id"`
|
|
PaymentStatus string `json:"payment_status"`
|
|
PayAddress string `json:"pay_address"`
|
|
PriceAmount json.Number `json:"price_amount"`
|
|
PriceCurrency string `json:"price_currency"`
|
|
PayAmount json.Number `json:"pay_amount"`
|
|
ActuallyPaid json.Number `json:"actually_paid"`
|
|
PayCurrency string `json:"pay_currency"`
|
|
OrderID string `json:"order_id"`
|
|
}
|
|
|
|
func (c *NowPaymentsClient) GetPaymentStatus(paymentID string) (*GetPaymentStatusResponse, error) {
|
|
httpReq, err := http.NewRequest("GET", c.BaseURL+"/payment/"+paymentID, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
httpReq.Header.Set("x-api-key", c.APIKey)
|
|
|
|
resp, err := httpClient.Do(httpReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("nowpayments error (%d): %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var result GetPaymentStatusResponse
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *NowPaymentsClient) VerifyIPN(body []byte, signature string) bool {
|
|
if c.IPNSecret == "" || signature == "" {
|
|
return false
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return false
|
|
}
|
|
|
|
sorted := sortedJSON(payload)
|
|
|
|
mac := hmac.New(sha512.New, []byte(c.IPNSecret))
|
|
mac.Write(sorted)
|
|
expected := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
return hmac.Equal([]byte(expected), []byte(signature))
|
|
}
|
|
|
|
func sortedJSON(data map[string]interface{}) []byte {
|
|
keys := make([]string, 0, len(data))
|
|
for k := range data {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
ordered := make([]byte, 0, 256)
|
|
ordered = append(ordered, '{')
|
|
for i, k := range keys {
|
|
if i > 0 {
|
|
ordered = append(ordered, ',')
|
|
}
|
|
key, _ := json.Marshal(k)
|
|
val, _ := json.Marshal(data[k])
|
|
ordered = append(ordered, key...)
|
|
ordered = append(ordered, ':')
|
|
ordered = append(ordered, val...)
|
|
}
|
|
ordered = append(ordered, '}')
|
|
return ordered
|
|
}
|