wormhole-explorer/notional/prices/service.go

131 lines
3.7 KiB
Go

package prices
import (
"context"
"errors"
"time"
"github.com/shopspring/decimal"
wormscanNotionalCache "github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
"go.uber.org/zap"
)
var (
ErrTokenNotFound = errors.New("token not found")
)
type Price struct {
CoingeckoID string `json:"coingeckoId"`
Symbol string `json:"symbol"`
Price string `json:"price"`
Datetime time.Time `json:"dateTime"`
}
// PriceService provides an interface to interact with prices.
type PriceService struct {
priceRepository *PriceRepository
tokenProvider *domain.TokenProvider
notionalCache wormscanNotionalCache.NotionalLocalCacheReadable
logger *zap.Logger
}
// NewPriceService creates a new price service.
func NewPriceService(priceRepository *PriceRepository,
tokenProvider *domain.TokenProvider,
notionalCache wormscanNotionalCache.NotionalLocalCacheReadable,
logger *zap.Logger) *PriceService {
return &PriceService{
priceRepository: priceRepository,
tokenProvider: tokenProvider,
notionalCache: notionalCache,
logger: logger.With(zap.String("module", "priceService")),
}
}
// GetPrice returns the price of a token at a given datetime.
func (s *PriceService) GetPrice(ctx context.Context, tokenChainID sdk.ChainID, tokenAddress string, datetime time.Time) (*Price, error) {
log := s.logger.With(zap.Uint16("chainID", uint16(tokenChainID)), zap.String("tokenAddress", tokenAddress))
token, found := s.tokenProvider.GetTokenByAddress(tokenChainID, tokenAddress)
if !found {
log.Warn("Token not found")
return nil, ErrTokenNotFound
}
v, err := s.GetPriceBySymbol(ctx, token, datetime, log)
if err != nil {
return nil, err
}
return &Price{
CoingeckoID: token.CoingeckoID,
Symbol: token.Symbol.String(),
Price: v.price.String(),
Datetime: v.datetime,
}, nil
}
func (s *PriceService) GetPriceByCoingeckoID(ctx context.Context, coingeckoID string, datetime time.Time) (*Price, error) {
log := s.logger.With(zap.String("coingeckoId", coingeckoID))
token, found := s.tokenProvider.GetTokenByCoingeckoID(coingeckoID)
if !found {
log.Warn("Token not found")
return nil, ErrTokenNotFound
}
v, err := s.GetPriceBySymbol(ctx, token, datetime, log)
if err != nil {
return nil, err
}
return &Price{
CoingeckoID: coingeckoID,
Symbol: token.Symbol.String(),
Price: v.price.String(),
Datetime: v.datetime,
}, nil
}
type priceSymbol struct {
price decimal.Decimal
datetime time.Time
}
func (s *PriceService) GetPriceBySymbol(ctx context.Context, token *domain.TokenMetadata, datetime time.Time, log *zap.Logger) (*priceSymbol, error) {
if token.CoingeckoID == "" {
log.Warn("CoingeckoID not found")
return nil, ErrTokenNotFound
}
dayDatetime := datetime.Truncate(24 * time.Hour)
cachePrice, err := s.notionalCache.Get(token.GetTokenID())
if err != nil {
return nil, err
}
diffCachePrice := cachePrice.UpdatedAt.Sub(datetime).Abs()
diffDayPrice := dayDatetime.Sub(datetime).Abs()
var price decimal.Decimal
var priceDatetime time.Time
if diffCachePrice > diffDayPrice {
p, err := s.priceRepository.Find(ctx, token.CoingeckoID, dayDatetime)
if err != nil {
if err == ErrPriceNotFound {
return nil, ErrTokenNotFound
}
log.Error("Failed to find price", zap.Error(err))
return nil, err
}
price, err = decimal.NewFromString(p.Price)
if err != nil {
log.Error("Failed to parse price", zap.Error(err))
return nil, err
}
priceDatetime = p.Datetime
} else {
price = cachePrice.NotionalUsd
priceDatetime = cachePrice.UpdatedAt
}
return &priceSymbol{price: price, datetime: priceDatetime}, nil
}