Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ type Handler struct {

limits Limits
spamFilter SpamFilter
ratesSource conversionRatesSource
ratesSource ratesSource
score scoreSource
metaCache metadataCache
tonConnect *tonconnect.Server
Expand Down
6 changes: 0 additions & 6 deletions pkg/api/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,6 @@ type ratesSource interface {
GetMarketsTonPrice() ([]rates.Market, error)
}

type conversionRatesSource interface {
ratesSource
GetTodayRatesWithTimestamps() (map[string]float64, map[string]int64)
GetMinuteAgoRatesWithTimestamps() (map[string]float64, map[string]int64)
}

type scoreSource interface {
GetJettonScore(masterID ton.AccountID) (int32, error)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/api/jetton_converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func (h *Handler) convertJettonOperation(ctx context.Context, op core.JettonOper

func (h *Handler) convertJettonBalance(ctx context.Context, wallet core.JettonWallet, currencies []string, scaledUiLt *int64, assetInfo *oas.JettonAssetInfo) (oas.JettonBalance, error) {
// the latest scaled ui parameters for jetton master if scaledUiLt == nil
_, yesterdayRates, weekRates, monthRates, _ := h.getRates()
todayRates, yesterdayRates, weekRates, monthRates, _ := h.getRates()
for idx, currency := range currencies {
if jetton, err := tongo.ParseAddress(currency); err == nil {
currency = jetton.ID.ToRaw()
Expand All @@ -173,7 +173,7 @@ func (h *Handler) convertJettonBalance(ctx context.Context, wallet core.JettonWa
}
rates := make(map[string]oas.TokenRates)
for _, currency := range currencies {
rates, err = h.convertRates(ctx, rates, wallet.JettonAddress.ToRaw(), currency, yesterdayRates, weekRates, monthRates)
rates, err = h.convertRates(ctx, rates, wallet.JettonAddress.ToRaw(), currency, todayRates, yesterdayRates, weekRates, monthRates)
if err != nil {
rates = make(map[string]oas.TokenRates)
continue
Expand Down
67 changes: 4 additions & 63 deletions pkg/api/rates_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,15 @@ func (h *Handler) GetRates(ctx context.Context, params oas.GetRatesParams) (*oas
}
}

_, yesterdayRates, weekRates, monthRates, err := h.getRates()
todayRates, yesterdayRates, weekRates, monthRates, err := h.getRates()
if err != nil {
return nil, toError(http.StatusInternalServerError, err)
}

rates := make(map[string]oas.TokenRates)
for _, token := range tokens {
for _, currency := range currencies {
rates, err = h.convertRates(ctx, rates, token, currency, yesterdayRates, weekRates, monthRates)
rates, err = h.convertRates(ctx, rates, token, currency, todayRates, yesterdayRates, weekRates, monthRates)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -176,63 +176,11 @@ func (h *Handler) getRates() (todayRates, yesterdayRates, weekRates, monthRates
return results[0], results[1], results[2], results[3], nil
}

const maxGenerationSkewSeconds = 180

func alignConversionPrices(
token, currency string,
todayRates map[string]float64, todayTs map[string]int64,
prevRates map[string]float64, prevTs map[string]int64,
) (tokenPrice, currencyPrice float64) {
tokenPrice = todayRates[token]
currencyPrice = todayRates[currency]

tokenTs, currencyTs := todayTs[token], todayTs[currency]
if tokenTs == 0 || currencyTs == 0 || tokenTs == currencyTs {
return tokenPrice, currencyPrice
}
gap := tokenTs - currencyTs
if gap < 0 {
gap = -gap
}
if gap > maxGenerationSkewSeconds {
return tokenPrice, currencyPrice
}

if tokenTs < currencyTs {
// the token price is a cycle behind: take the currency price from the previous
// snapshot when its generation matches the token's better than the current one
if prev, ok := prevRates[currency]; ok && prev != 0 && closerTo(prevTs[currency], tokenTs, currencyTs) {
currencyPrice = prev
}
} else {
if prev, ok := prevRates[token]; ok && prev != 0 && closerTo(prevTs[token], currencyTs, tokenTs) {
tokenPrice = prev
}
}
return tokenPrice, currencyPrice
}

// closerTo reports whether candidate is strictly closer to target than current is
func closerTo(candidate, target, current int64) bool {
if candidate == 0 {
return false
}
candidateDist := candidate - target
if candidateDist < 0 {
candidateDist = -candidateDist
}
currentDist := current - target
if currentDist < 0 {
currentDist = -currentDist
}
return candidateDist < currentDist
}

func (h *Handler) convertRates(
ctx context.Context,
rates map[string]oas.TokenRates,
token, currency string,
yesterdayRates, weekRates, monthRates map[string]float64,
todayRates, yesterdayRates, weekRates, monthRates map[string]float64,
) (map[string]oas.TokenRates, error) {
trust := core.TrustNone
if len(token) >= minTonAddressLength {
Expand All @@ -243,8 +191,6 @@ func (h *Handler) convertRates(
}
}

todayRates, todayTimestamps := h.ratesSource.GetTodayRatesWithTimestamps()

todayCurrencyPrice, ok := todayRates[currency]
if !ok {
return nil, toError(http.StatusBadRequest, fmt.Errorf("invalid currency: %v", currency))
Expand All @@ -260,12 +206,7 @@ func (h *Handler) convertRates(
}
}

minuteAgoRates, minuteAgoTimestamps := h.ratesSource.GetMinuteAgoRatesWithTimestamps()
tokenPrice, todayCurrencyPrice := alignConversionPrices(
token, currency,
todayRates, todayTimestamps,
minuteAgoRates, minuteAgoTimestamps,
)
tokenPrice := todayRates[token]
if trust == core.TrustBlacklist {
tokenPrice = 0
}
Expand Down
100 changes: 0 additions & 100 deletions pkg/api/rates_handlers_test.go

This file was deleted.

42 changes: 6 additions & 36 deletions pkg/rates/calculator.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ type ratesSource interface {
GetMarketsTonPrice() ([]Market, error)
}

// timestampedRatesSource is implemented by sources backed by the rates service's
// /v1/rates/timestamped endpoint. Only its prices are consumed; the per-token
// timestamps it reports are ignored.
type timestampedRatesSource interface {
GetRatesWithTimestamps(date int64) (map[string]float64, map[string]int64, error)
}
Expand All @@ -24,14 +27,7 @@ type calculator struct {
// See the Mock description for details
source ratesSource
todayRates, yesterdayRates, weekRates, monthRates map[string]float64
// todayTimestamps holds, for each token in todayRates, the unix timestamp its price
// was produced at; empty when the source cannot report timestamps
todayTimestamps map[string]int64
// minuteAgoRates and minuteAgoTimestamps hold the today snapshot from the previous
// refresh cycle; empty until the second refresh completes (cold start)
minuteAgoRates map[string]float64
minuteAgoTimestamps map[string]int64
marketsTonPrice []Market
marketsTonPrice []Market
}

type Point struct {
Expand All @@ -47,7 +43,6 @@ func InitCalculator(source ratesSource) *calculator {
c := &calculator{
source: source,
todayRates: map[string]float64{},
todayTimestamps: map[string]int64{},
yesterdayRates: map[string]float64{},
weekRates: map[string]float64{},
monthRates: map[string]float64{},
Expand All @@ -74,13 +69,12 @@ func (c *calculator) refresh() {

marketsTonPrice, marketErr := c.source.GetMarketsTonPrice()
var todayRates map[string]float64
var todayTimestamps map[string]int64
var err error
if tsSource, ok := c.source.(timestampedRatesSource); ok {
todayRates, todayTimestamps, err = tsSource.GetRatesWithTimestamps(today.Unix())
// prices come from the timestamped endpoint; the timestamps are ignored
todayRates, _, err = tsSource.GetRatesWithTimestamps(today.Unix())
} else {
todayRates, err = c.source.GetRates(today.Unix())
todayTimestamps = map[string]int64{}
}
if err != nil {
slog.Error("[refresh-rates] error getting today rates", slog.String("err", err.Error()))
Expand All @@ -103,10 +97,7 @@ func (c *calculator) refresh() {
}

c.mu.Lock()
c.minuteAgoRates = c.todayRates
c.minuteAgoTimestamps = c.todayTimestamps
c.todayRates = todayRates
c.todayTimestamps = todayTimestamps
c.yesterdayRates = yesterdayRates
c.weekRates = weekRates
c.monthRates = monthRates
Expand Down Expand Up @@ -141,27 +132,6 @@ func (c *calculator) GetRates(date int64) (map[string]float64, error) {
return nil, fmt.Errorf("invalid period")
}

// GetTodayRatesWithTimestamps returns today's rates together with, for each token, the unix
// timestamp its price was produced at. The timestamps map is empty when the source does not
// implement timestampedRatesSource
func (c *calculator) GetTodayRatesWithTimestamps() (map[string]float64, map[string]int64) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.todayRates, c.todayTimestamps
}

// GetMinuteAgoRatesWithTimestamps returns the today snapshot from the previous refresh
// cycle. Until the second refresh completes (cold start) there is no previous snapshot,
// so it falls back to the current one — callers always get a usable map
func (c *calculator) GetMinuteAgoRatesWithTimestamps() (map[string]float64, map[string]int64) {
c.mu.RLock()
defer c.mu.RUnlock()
if len(c.minuteAgoRates) == 0 {
return c.todayRates, c.todayTimestamps
}
return c.minuteAgoRates, c.minuteAgoTimestamps
}

func (c *calculator) GetRatesChart(token string, currency string, pointsCount int, startDate *int64, endDate *int64) ([]Point, error) {
return c.source.GetRatesChart(token, currency, pointsCount, startDate, endDate)
}
Expand Down
Loading
Loading