fiat rates perf optimization

This commit is contained in:
pragmaxim
2026-02-18 11:14:56 +01:00
parent e2b494f2a5
commit b8ace17866
2 changed files with 227 additions and 13 deletions

View File

@@ -36,6 +36,10 @@ type Worker struct {
metrics *common.Metrics
}
var getTickersForTimestamps = func(fr *fiat.FiatRates, timestamps []int64, vsCurrency string, token string) (*[]*common.CurrencyRatesTicker, error) {
return fr.GetTickersForTimestamps(timestamps, vsCurrency, token)
}
// contractInfoCache is a temporary cache of contract information for ethereum token transfers
type contractInfoCache = map[string]*bchain.ContractInfo
@@ -1715,23 +1719,18 @@ func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid s
}
func (w *Worker) setFiatRateToBalanceHistories(histories BalanceHistories, currencies []string) error {
for i := range histories {
bh := &histories[i]
tickers, err := w.fiatRates.GetTickersForTimestamps([]int64{int64(bh.Time)}, "", "")
if err != nil || tickers == nil || len(*tickers) == 0 {
glog.Errorf("Error finding ticker by date %v. Error: %v", bh.Time, err)
continue
}
ticker := (*tickers)[0]
if len(histories) == 0 || w.fiatRates == nil {
return nil
}
applyTickerToHistory := func(bh *BalanceHistory, ticker *common.CurrencyRatesTicker, currenciesLowercase []string) {
if ticker == nil {
continue
return
}
if len(currencies) == 0 {
if len(currenciesLowercase) == 0 {
bh.FiatRates = ticker.Rates
} else {
rates := make(map[string]float32)
for _, currency := range currencies {
currency = strings.ToLower(currency)
rates := make(map[string]float32, len(currenciesLowercase))
for _, currency := range currenciesLowercase {
if rate, found := ticker.Rates[currency]; found {
rates[currency] = rate
} else {
@@ -1741,6 +1740,35 @@ func (w *Worker) setFiatRateToBalanceHistories(histories BalanceHistories, curre
bh.FiatRates = rates
}
}
timestamps := make([]int64, len(histories))
for i := range histories {
timestamps[i] = int64(histories[i].Time)
}
tickers, err := getTickersForTimestamps(w.fiatRates, timestamps, "", "")
batchFetchValid := err == nil && tickers != nil && len(*tickers) == len(histories)
if !batchFetchValid {
glog.Errorf("Error finding tickers for %d timestamps. Error: %v", len(timestamps), err)
}
currenciesLowercase := make([]string, len(currencies))
for i := range currencies {
currenciesLowercase[i] = strings.ToLower(currencies[i])
}
if batchFetchValid {
for i := range histories {
applyTickerToHistory(&histories[i], (*tickers)[i], currenciesLowercase)
}
return nil
}
// Fallback to per-point lookup to preserve original behavior on partial failures.
for i := range histories {
bh := &histories[i]
pointTickers, pointErr := getTickersForTimestamps(w.fiatRates, []int64{int64(bh.Time)}, "", "")
if pointErr != nil || pointTickers == nil || len(*pointTickers) == 0 {
glog.Errorf("Error finding ticker by date %v. Error: %v", bh.Time, pointErr)
continue
}
applyTickerToHistory(bh, (*pointTickers)[0], currenciesLowercase)
}
return nil
}

186
api/worker_test.go Normal file
View File

@@ -0,0 +1,186 @@
//go:build unittest
package api
import (
"reflect"
"testing"
"github.com/trezor/blockbook/common"
"github.com/trezor/blockbook/fiat"
)
func TestSetFiatRateToBalanceHistories_BatchesTickerLookup(t *testing.T) {
histories := BalanceHistories{
{Time: 100},
{Time: 200},
{Time: 300},
}
w := &Worker{
fiatRates: &fiat.FiatRates{Enabled: true},
}
originalGetter := getTickersForTimestamps
defer func() {
getTickersForTimestamps = originalGetter
}()
calls := 0
var gotTimestamps []int64
getTickersForTimestamps = func(_ *fiat.FiatRates, timestamps []int64, _, _ string) (*[]*common.CurrencyRatesTicker, error) {
calls++
gotTimestamps = append([]int64(nil), timestamps...)
tickers := []*common.CurrencyRatesTicker{
{Rates: map[string]float32{"usd": 11, "eur": 22}},
nil,
{Rates: map[string]float32{"usd": 33}},
}
return &tickers, nil
}
err := w.setFiatRateToBalanceHistories(histories, []string{"USD", "eur", "cad"})
if err != nil {
t.Fatalf("setFiatRateToBalanceHistories returned error: %v", err)
}
if calls != 1 {
t.Fatalf("expected 1 ticker lookup call, got %d", calls)
}
if !reflect.DeepEqual(gotTimestamps, []int64{100, 200, 300}) {
t.Fatalf("unexpected timestamps: got %v", gotTimestamps)
}
if !reflect.DeepEqual(histories[0].FiatRates, map[string]float32{"usd": 11, "eur": 22, "cad": -1}) {
t.Fatalf("unexpected rates for histories[0]: %v", histories[0].FiatRates)
}
if histories[1].FiatRates != nil {
t.Fatalf("expected nil rates for histories[1], got %v", histories[1].FiatRates)
}
if !reflect.DeepEqual(histories[2].FiatRates, map[string]float32{"usd": 33, "eur": -1, "cad": -1}) {
t.Fatalf("unexpected rates for histories[2]: %v", histories[2].FiatRates)
}
}
func TestSetFiatRateToBalanceHistories_AllRatesWhenCurrenciesNotSpecified(t *testing.T) {
histories := BalanceHistories{
{Time: 100},
}
w := &Worker{
fiatRates: &fiat.FiatRates{Enabled: true},
}
originalGetter := getTickersForTimestamps
defer func() {
getTickersForTimestamps = originalGetter
}()
getTickersForTimestamps = func(_ *fiat.FiatRates, _ []int64, _, _ string) (*[]*common.CurrencyRatesTicker, error) {
tickers := []*common.CurrencyRatesTicker{
{Rates: map[string]float32{"usd": 11, "eur": 22}},
}
return &tickers, nil
}
err := w.setFiatRateToBalanceHistories(histories, nil)
if err != nil {
t.Fatalf("setFiatRateToBalanceHistories returned error: %v", err)
}
if !reflect.DeepEqual(histories[0].FiatRates, map[string]float32{"usd": 11, "eur": 22}) {
t.Fatalf("unexpected rates for histories[0]: %v", histories[0].FiatRates)
}
}
func TestSetFiatRateToBalanceHistories_BatchFailureFallsBackToPerPoint(t *testing.T) {
histories := BalanceHistories{
{Time: 100},
{Time: 200},
{Time: 300},
}
w := &Worker{
fiatRates: &fiat.FiatRates{Enabled: true},
}
originalGetter := getTickersForTimestamps
defer func() {
getTickersForTimestamps = originalGetter
}()
calls := 0
var gotCalls [][]int64
getTickersForTimestamps = func(_ *fiat.FiatRates, timestamps []int64, _, _ string) (*[]*common.CurrencyRatesTicker, error) {
calls++
gotCalls = append(gotCalls, append([]int64(nil), timestamps...))
if len(timestamps) > 1 {
return nil, assertError("batch error")
}
switch timestamps[0] {
case 100:
tickers := []*common.CurrencyRatesTicker{
{Rates: map[string]float32{"usd": 11}},
}
return &tickers, nil
case 200:
return nil, assertError("point error")
case 300:
tickers := []*common.CurrencyRatesTicker{
{Rates: map[string]float32{"usd": 33}},
}
return &tickers, nil
default:
tickers := []*common.CurrencyRatesTicker{}
return &tickers, nil
}
}
err := w.setFiatRateToBalanceHistories(histories, []string{"usd"})
if err != nil {
t.Fatalf("setFiatRateToBalanceHistories returned error: %v", err)
}
if calls != 4 {
t.Fatalf("expected 4 ticker lookup calls (1 batch + 3 point), got %d", calls)
}
wantCalls := [][]int64{
{100, 200, 300},
{100},
{200},
{300},
}
if !reflect.DeepEqual(gotCalls, wantCalls) {
t.Fatalf("unexpected lookup calls: got %v, want %v", gotCalls, wantCalls)
}
if !reflect.DeepEqual(histories[0].FiatRates, map[string]float32{"usd": 11}) {
t.Fatalf("unexpected rates for histories[0]: %v", histories[0].FiatRates)
}
if histories[1].FiatRates != nil {
t.Fatalf("expected nil rates for histories[1], got %v", histories[1].FiatRates)
}
if !reflect.DeepEqual(histories[2].FiatRates, map[string]float32{"usd": 33}) {
t.Fatalf("unexpected rates for histories[2]: %v", histories[2].FiatRates)
}
}
func TestSetFiatRateToBalanceHistories_SkipsLookupForEmptyHistory(t *testing.T) {
w := &Worker{
fiatRates: &fiat.FiatRates{Enabled: true},
}
originalGetter := getTickersForTimestamps
defer func() {
getTickersForTimestamps = originalGetter
}()
calls := 0
getTickersForTimestamps = func(_ *fiat.FiatRates, _ []int64, _, _ string) (*[]*common.CurrencyRatesTicker, error) {
calls++
tickers := []*common.CurrencyRatesTicker{}
return &tickers, nil
}
err := w.setFiatRateToBalanceHistories(BalanceHistories{}, []string{"usd"})
if err != nil {
t.Fatalf("setFiatRateToBalanceHistories returned error: %v", err)
}
if calls != 0 {
t.Fatalf("expected 0 ticker lookup calls, got %d", calls)
}
}
type assertError string
func (e assertError) Error() string {
return string(e)
}