139 lines
3.6 KiB
Go
139 lines
3.6 KiB
Go
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
|
|
}
|