converter/infrastructure/coinmarketcap/client.go

139 lines
3.6 KiB
Go
Raw Permalink Normal View History

2025-09-06 11:58:46 +03:00
package coinmarketcap
import (
"context"
"errors"
"fmt"
"github.com/go-resty/resty/v2"
)
// CoinMarketCap specific errors
var (
ErrUnknownSymbol = errors.New("unknown symbol")
ErrInvalidConvert = errors.New("invalid convert currency")
ErrAPIError = errors.New("coinmarketcap api error")
ErrInvalidResponse = errors.New("invalid response format")
)
const (
baseURL = "https://pro-api.coinmarketcap.com"
quotesPath = "/v1/cryptocurrency/quotes/latest"
)
// Client implements the RateProvider interface for CoinMarketCap API
type Client struct {
httpClient *resty.Client
apiKey string
}
// NewClient creates a new CoinMarketCap API client
func NewClient(apiKey string) *Client {
client := resty.New().
SetBaseURL(baseURL).
SetHeader("Accept", "application/json").
SetHeader("X-CMC_PRO_API_KEY", apiKey)
return &Client{
httpClient: client,
apiKey: apiKey,
}
}
// GetRate fetches exchange rate from CoinMarketCap API
func (c *Client) GetRate(ctx context.Context, fromCode, toCode string) (RateDTO, error) {
// Try direct conversion first
rateDTO, err := c.tryGetRate(ctx, fromCode, toCode)
if err == nil {
return rateDTO, nil
}
// If empty result but no other error, try reverse
if err == ErrUnknownSymbol {
reverseDTO, reverseErr := c.tryGetRate(ctx, toCode, fromCode)
if reverseErr == nil {
return reverseDTO, nil
}
// If reverse request failed with invalid convert, that confirms the original
// "from" currency is indeed unknown (it's now the "convert" parameter)
if reverseErr == ErrInvalidConvert {
return RateDTO{}, err // Return original unknown symbol error
}
// Any other error from reverse request (network, auth, etc) should be surfaced
return RateDTO{}, reverseErr
}
// Return original error (either non-unknown-symbol error or first currency not in crypto database)
return RateDTO{}, err
}
// tryGetRate attempts to get rate in specified direction
func (c *Client) tryGetRate(ctx context.Context, fromCode, toCode string) (RateDTO, error) {
var response APIResponse
// Make API request
resp, err := c.httpClient.R().
SetContext(ctx).
SetQueryParam("symbol", fromCode).
SetQueryParam("convert", toCode).
SetResult(&response).
Get(quotesPath)
if err != nil {
return RateDTO{}, fmt.Errorf("%w: %v", ErrAPIError, err)
}
// Check HTTP status first
if resp.StatusCode() != 200 {
switch resp.StatusCode() {
case 400:
return RateDTO{}, fmt.Errorf("%w: %s", ErrInvalidConvert, toCode)
case 401:
return RateDTO{}, fmt.Errorf("%w: unauthorized - bad or missing API key", ErrAPIError)
default:
return RateDTO{}, fmt.Errorf("%w: HTTP %d", ErrAPIError, resp.StatusCode())
}
}
// Check if data is empty - invalid currency
if len(response.Data) == 0 {
return RateDTO{}, fmt.Errorf("%w: %s", ErrUnknownSymbol, fromCode)
}
// Extract rate from response
rateDTO, err := c.extractRateDTO(response, fromCode, toCode)
if err != nil {
return RateDTO{}, err
}
return rateDTO, nil
}
// extractRateDTO converts API response to RateDTO
func (c *Client) extractRateDTO(response APIResponse, fromCode, toCode string) (RateDTO, error) {
// Get crypto data for the from currency
cryptoData, exists := response.Data[fromCode]
if !exists {
return RateDTO{}, ErrInvalidResponse
}
// Get quote for the to currency
quote, exists := cryptoData.Quote[toCode]
if !exists {
return RateDTO{}, ErrInvalidResponse
}
// Return the rate exactly as API provided it
return RateDTO{
FromCode: fromCode,
ToCode: toCode,
FromName: cryptoData.Name,
ToName: toCode,
Price: quote.Price,
Source: "coinmarketcap",
}, nil
}