Refactor fiat rates handler

This commit is contained in:
Martin Boehm 2023-04-18 01:32:37 +02:00
parent ad50758984
commit d856618607
15 changed files with 426 additions and 305 deletions

View File

@ -19,6 +19,7 @@ import (
"github.com/trezor/blockbook/bchain/coins/eth" "github.com/trezor/blockbook/bchain/coins/eth"
"github.com/trezor/blockbook/common" "github.com/trezor/blockbook/common"
"github.com/trezor/blockbook/db" "github.com/trezor/blockbook/db"
"github.com/trezor/blockbook/fiat"
) )
// Worker is handle to api worker // Worker is handle to api worker
@ -31,11 +32,12 @@ type Worker struct {
useAddressAliases bool useAddressAliases bool
mempool bchain.Mempool mempool bchain.Mempool
is *common.InternalState is *common.InternalState
fiatRates *fiat.FiatRates
metrics *common.Metrics metrics *common.Metrics
} }
// NewWorker creates new api worker // NewWorker creates new api worker
func NewWorker(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*Worker, error) { func NewWorker(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState, fiatRates *fiat.FiatRates) (*Worker, error) {
w := &Worker{ w := &Worker{
db: db, db: db,
txCache: txCache, txCache: txCache,
@ -45,6 +47,7 @@ func NewWorker(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool,
useAddressAliases: chain.GetChainParser().UseAddressAliases(), useAddressAliases: chain.GetChainParser().UseAddressAliases(),
mempool: mempool, mempool: mempool,
is: is, is: is,
fiatRates: fiatRates,
metrics: metrics, metrics: metrics,
} }
if w.chainType == bchain.ChainBitcoinType { if w.chainType == bchain.ChainBitcoinType {
@ -1072,7 +1075,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
if err != nil { if err != nil {
return nil, nil, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc) return nil, nil, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc)
} }
ticker := w.is.GetCurrentTicker("", "") ticker := w.fiatRates.GetCurrentTicker("", "")
if details > AccountDetailsBasic { if details > AccountDetailsBasic {
d.tokens = make([]Token, len(ca.Contracts)) d.tokens = make([]Token, len(ca.Contracts))
var j int var j int
@ -1346,7 +1349,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
} }
var secondaryRate, totalSecondaryValue, totalBaseValue, secondaryValue float64 var secondaryRate, totalSecondaryValue, totalBaseValue, secondaryValue float64
if secondaryCoin != "" { if secondaryCoin != "" {
ticker := w.is.GetCurrentTicker("", "") ticker := w.fiatRates.GetCurrentTicker("", "")
balance, err := strconv.ParseFloat((*Amount)(&ba.BalanceSat).DecimalString(w.chainParser.AmountDecimals()), 64) balance, err := strconv.ParseFloat((*Amount)(&ba.BalanceSat).DecimalString(w.chainParser.AmountDecimals()), 64)
if ticker != nil && err == nil { if ticker != nil && err == nil {
r, found := ticker.Rates[secondaryCoin] r, found := ticker.Rates[secondaryCoin]
@ -1887,7 +1890,7 @@ func (w *Worker) GetCurrentFiatRates(currencies []string, token string) (*FiatTi
if len(currencies) == 1 { if len(currencies) == 1 {
vsCurrency = currencies[0] vsCurrency = currencies[0]
} }
ticker := w.is.GetCurrentTicker(vsCurrency, token) ticker := w.fiatRates.GetCurrentTicker(vsCurrency, token)
var err error var err error
if ticker == nil { if ticker == nil {
ticker, err = w.db.FiatRatesFindLastTicker(vsCurrency, token) ticker, err = w.db.FiatRatesFindLastTicker(vsCurrency, token)
@ -2289,8 +2292,9 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) {
internalDBSize = w.is.DBSizeTotal() internalDBSize = w.is.DBSizeTotal()
} }
var currentFiatRatesTime time.Time var currentFiatRatesTime time.Time
if w.is.CurrentTicker != nil { ct := w.fiatRates.GetCurrentTicker("", "")
currentFiatRatesTime = w.is.CurrentTicker.Timestamp if ct != nil {
currentFiatRatesTime = ct.Timestamp
} }
blockbookInfo := &BlockbookInfo{ blockbookInfo := &BlockbookInfo{
Coin: w.is.Coin, Coin: w.is.Coin,

View File

@ -571,7 +571,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc
var secondaryValue float64 var secondaryValue float64
if secondaryCoin != "" { if secondaryCoin != "" {
ticker := w.is.GetCurrentTicker("", "") ticker := w.fiatRates.GetCurrentTicker("", "")
balance, err := strconv.ParseFloat((*Amount)(&data.balanceSat).DecimalString(w.chainParser.AmountDecimals()), 64) balance, err := strconv.ParseFloat((*Amount)(&data.balanceSat).DecimalString(w.chainParser.AmountDecimals()), 64)
if ticker != nil && err == nil { if ticker != nil && err == nil {
r, found := ticker.Rates[secondaryCoin] r, found := ticker.Rates[secondaryCoin]

View File

@ -102,6 +102,7 @@ var (
metrics *common.Metrics metrics *common.Metrics
syncWorker *db.SyncWorker syncWorker *db.SyncWorker
internalState *common.InternalState internalState *common.InternalState
fiatRates *fiat.FiatRates
callbacksOnNewBlock []bchain.OnNewBlockFunc callbacksOnNewBlock []bchain.OnNewBlockFunc
callbacksOnNewTxAddr []bchain.OnNewTxAddrFunc callbacksOnNewTxAddr []bchain.OnNewTxAddrFunc
callbacksOnNewTx []bchain.OnNewTxFunc callbacksOnNewTx []bchain.OnNewTxFunc
@ -273,6 +274,11 @@ func mainWithExitCode() int {
return exitCodeFatal return exitCodeFatal
} }
if fiatRates, err = fiat.NewFiatRates(index, *configFile, onNewFiatRatesTicker); err != nil {
glog.Error("fiatRates ", err)
return exitCodeFatal
}
// report BlockbookAppInfo metric, only log possible error // report BlockbookAppInfo metric, only log possible error
if err = blockbookAppInfoMetric(index, chain, txCache, internalState, metrics); err != nil { if err = blockbookAppInfoMetric(index, chain, txCache, internalState, metrics); err != nil {
glog.Error("blockbookAppInfoMetric ", err) glog.Error("blockbookAppInfoMetric ", err)
@ -397,7 +403,7 @@ func getBlockChainWithRetry(coin string, configFile string, pushHandler func(bch
} }
func startInternalServer() (*server.InternalServer, error) { func startInternalServer() (*server.InternalServer, error) {
internalServer, err := server.NewInternalServer(*internalBinding, *certFiles, index, chain, mempool, txCache, metrics, internalState) internalServer, err := server.NewInternalServer(*internalBinding, *certFiles, index, chain, mempool, txCache, metrics, internalState, fiatRates)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -417,7 +423,7 @@ func startInternalServer() (*server.InternalServer, error) {
func startPublicServer() (*server.PublicServer, error) { func startPublicServer() (*server.PublicServer, error) {
// start public server in limited functionality, extend it after sync is finished by calling ConnectFullPublicInterface // start public server in limited functionality, extend it after sync is finished by calling ConnectFullPublicInterface
publicServer, err := server.NewPublicServer(*publicBinding, *certFiles, index, chain, mempool, txCache, *explorerURL, metrics, internalState, *debugMode) publicServer, err := server.NewPublicServer(*publicBinding, *certFiles, index, chain, mempool, txCache, *explorerURL, metrics, internalState, fiatRates, *debugMode)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -463,7 +469,7 @@ func performRollback() error {
} }
func blockbookAppInfoMetric(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is *common.InternalState, metrics *common.Metrics) error { func blockbookAppInfoMetric(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is *common.InternalState, metrics *common.Metrics) error {
api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates)
if err != nil { if err != nil {
return err return err
} }
@ -682,7 +688,7 @@ func waitForSignalAndShutdown(internal *server.InternalServer, public *server.Pu
func computeFeeStats(stopCompute chan os.Signal, blockFrom, blockTo int, db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is *common.InternalState, metrics *common.Metrics) error { func computeFeeStats(stopCompute chan os.Signal, blockFrom, blockTo int, db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is *common.InternalState, metrics *common.Metrics) error {
start := time.Now() start := time.Now()
glog.Info("computeFeeStats start") glog.Info("computeFeeStats start")
api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates)
if err != nil { if err != nil {
return err return err
} }
@ -692,6 +698,10 @@ func computeFeeStats(stopCompute chan os.Signal, blockFrom, blockTo int, db *db.
} }
func initDownloaders(db *db.RocksDB, chain bchain.BlockChain, configFile string) { func initDownloaders(db *db.RocksDB, chain bchain.BlockChain, configFile string) {
if fiatRates.Enabled {
go fiatRates.RunDownloader()
}
data, err := ioutil.ReadFile(configFile) data, err := ioutil.ReadFile(configFile)
if err != nil { if err != nil {
glog.Errorf("Error reading file %v, %v", configFile, err) glog.Errorf("Error reading file %v, %v", configFile, err)
@ -699,10 +709,7 @@ func initDownloaders(db *db.RocksDB, chain bchain.BlockChain, configFile string)
} }
var config struct { var config struct {
FiatRates string `json:"fiat_rates"` FourByteSignatures string `json:"fourByteSignatures"`
FiatRatesParams string `json:"fiat_rates_params"`
FiatRatesVsCurrencies string `json:"fiat_rates_vs_currencies"`
FourByteSignatures string `json:"fourByteSignatures"`
} }
err = json.Unmarshal(data, &config) err = json.Unmarshal(data, &config)
@ -711,18 +718,6 @@ func initDownloaders(db *db.RocksDB, chain bchain.BlockChain, configFile string)
return return
} }
if config.FiatRates == "" || config.FiatRatesParams == "" {
glog.Infof("FiatRates config (%v) is empty, not downloading fiat rates", configFile)
} else {
fiatRates, err := fiat.NewFiatRatesDownloader(db, config.FiatRates, config.FiatRatesParams, config.FiatRatesVsCurrencies, onNewFiatRatesTicker)
if err != nil {
glog.Errorf("NewFiatRatesDownloader Init error: %v", err)
} else {
glog.Infof("Starting %v FiatRates downloader...", config.FiatRates)
go fiatRates.Run()
}
}
if config.FourByteSignatures != "" && chain.GetChainParser().GetChainType() == bchain.ChainEthereumType { if config.FourByteSignatures != "" && chain.GetChainParser().GetChainType() == bchain.ChainEthereumType {
fbsd, err := fourbyte.NewFourByteSignaturesDownloader(db, config.FourByteSignatures) fbsd, err := fourbyte.NewFourByteSignaturesDownloader(db, config.FourByteSignatures)
if err != nil { if err != nil {

View File

@ -7,9 +7,9 @@ import (
// CurrencyRatesTicker contains coin ticker data fetched from API // CurrencyRatesTicker contains coin ticker data fetched from API
type CurrencyRatesTicker struct { type CurrencyRatesTicker struct {
Timestamp time.Time `json:"timestamp"` // return as unix timestamp in API Timestamp time.Time `json:"timestamp"` // return as unix timestamp in API
Rates map[string]float32 `json:"rates"` // rates of the base currency against a list of vs currencies Rates map[string]float32 `json:"rates"` // rates of the base currency against a list of vs currencies
TokenRates map[string]float32 `json:"tokenRates"` // rates of the tokens (identified by the address of the contract) against the base currency TokenRates map[string]float32 `json:"tokenRates,omitempty"` // rates of the tokens (identified by the address of the contract) against the base currency
} }
var ( var (

View File

@ -80,11 +80,10 @@ type InternalState struct {
DbColumns []InternalStateColumn `json:"dbColumns"` DbColumns []InternalStateColumn `json:"dbColumns"`
HasFiatRates bool `json:"-"` HasFiatRates bool `json:"-"`
HasTokenFiatRates bool `json:"-"` HasTokenFiatRates bool `json:"-"`
HistoricalFiatRatesTime time.Time `json:"historicalFiatRatesTime"` HistoricalFiatRatesTime time.Time `json:"historicalFiatRatesTime"`
HistoricalTokenFiatRatesTime time.Time `json:"historicalTokenFiatRatesTime"` HistoricalTokenFiatRatesTime time.Time `json:"historicalTokenFiatRatesTime"`
CurrentTicker *CurrencyRatesTicker `json:"currentTicker"`
EnableSubNewTx bool `json:"-"` EnableSubNewTx bool `json:"-"`
@ -319,24 +318,6 @@ func (is *InternalState) Pack() ([]byte, error) {
return json.Marshal(is) return json.Marshal(is)
} }
// GetCurrentTicker returns current ticker
func (is *InternalState) GetCurrentTicker(vsCurrency string, token string) *CurrencyRatesTicker {
is.mux.Lock()
currentTicker := is.CurrentTicker
is.mux.Unlock()
if currentTicker != nil && IsSuitableTicker(currentTicker, vsCurrency, token) {
return currentTicker
}
return nil
}
// SetCurrentTicker sets current ticker
func (is *InternalState) SetCurrentTicker(t *CurrencyRatesTicker) {
is.mux.Lock()
defer is.mux.Unlock()
is.CurrentTicker = t
}
// UnpackInternalState unmarshals internal state from json // UnpackInternalState unmarshals internal state from json
func UnpackInternalState(buf []byte) (*InternalState, error) { func UnpackInternalState(buf []byte) (*InternalState, error) {
var is InternalState var is InternalState

View File

@ -2,8 +2,8 @@ package db
import ( import (
"encoding/binary" "encoding/binary"
"encoding/json"
"math" "math"
"sync"
"time" "time"
vlq "github.com/bsm/go-vlq" vlq "github.com/bsm/go-vlq"
@ -16,9 +16,6 @@ import (
// FiatRatesTimeFormat is a format string for storing FiatRates timestamps in rocksdb // FiatRatesTimeFormat is a format string for storing FiatRates timestamps in rocksdb
const FiatRatesTimeFormat = "20060102150405" // YYYYMMDDhhmmss const FiatRatesTimeFormat = "20060102150405" // YYYYMMDDhhmmss
var lastTickerInDB *common.CurrencyRatesTicker
var lastTickerInDBMux sync.Mutex
func packTimestamp(t *time.Time) []byte { func packTimestamp(t *time.Time) []byte {
return []byte(t.UTC().Format(FiatRatesTimeFormat)) return []byte(t.UTC().Format(FiatRatesTimeFormat))
} }
@ -148,21 +145,21 @@ func (d *RocksDB) FiatRatesGetTicker(tickerTime *time.Time) (*common.CurrencyRat
// FiatRatesFindTicker gets FiatRates data closest to the specified timestamp, of the base currency, vsCurrency or the token if specified // FiatRatesFindTicker gets FiatRates data closest to the specified timestamp, of the base currency, vsCurrency or the token if specified
func (d *RocksDB) FiatRatesFindTicker(tickerTime *time.Time, vsCurrency string, token string) (*common.CurrencyRatesTicker, error) { func (d *RocksDB) FiatRatesFindTicker(tickerTime *time.Time, vsCurrency string, token string) (*common.CurrencyRatesTicker, error) {
currentTicker := d.is.GetCurrentTicker("", "") // currentTicker := d.is.GetCurrentTicker("", "")
lastTickerInDBMux.Lock() // lastTickerInDBMux.Lock()
dbTicker := lastTickerInDB // dbTicker := lastTickerInDB
lastTickerInDBMux.Unlock() // lastTickerInDBMux.Unlock()
if currentTicker != nil { // if currentTicker != nil {
if !tickerTime.Before(currentTicker.Timestamp) || (dbTicker != nil && tickerTime.After(dbTicker.Timestamp)) { // if !tickerTime.Before(currentTicker.Timestamp) || (dbTicker != nil && tickerTime.After(dbTicker.Timestamp)) {
f := true // f := true
if token != "" && currentTicker.TokenRates != nil { // if token != "" && currentTicker.TokenRates != nil {
_, f = currentTicker.TokenRates[token] // _, f = currentTicker.TokenRates[token]
} // }
if f { // if f {
return currentTicker, nil // return currentTicker, nil
} // }
} // }
} // }
tickerTimeFormatted := tickerTime.UTC().Format(FiatRatesTimeFormat) tickerTimeFormatted := tickerTime.UTC().Format(FiatRatesTimeFormat)
it := d.db.NewIteratorCF(d.ro, d.cfh[cfFiatRates]) it := d.db.NewIteratorCF(d.ro, d.cfh[cfFiatRates])
@ -193,14 +190,33 @@ func (d *RocksDB) FiatRatesFindLastTicker(vsCurrency string, token string) (*com
return nil, err return nil, err
} }
if ticker != nil { if ticker != nil {
// if without filter, store the ticker for later use
if vsCurrency == "" && token == "" {
lastTickerInDBMux.Lock()
lastTickerInDB = ticker
lastTickerInDBMux.Unlock()
}
return ticker, nil return ticker, nil
} }
} }
return nil, nil return nil, nil
} }
func (d *RocksDB) FiatRatesGetSpecialTickers(key string) (*[]common.CurrencyRatesTicker, error) {
val, err := d.db.GetCF(d.ro, d.cfh[cfDefault], []byte(key))
if err != nil {
return nil, err
}
defer val.Free()
data := val.Data()
if data == nil {
return nil, nil
}
var tickers []common.CurrencyRatesTicker
if err := json.Unmarshal(data, &tickers); err != nil {
return nil, err
}
return &tickers, nil
}
func (d *RocksDB) FiatRatesStoreSpecialTickers(key string, tickers *[]common.CurrencyRatesTicker) error {
data, err := json.Marshal(tickers)
if err != nil {
return err
}
return d.db.PutCF(d.wo, d.cfh[cfDefault], []byte(key), data)
}

View File

@ -158,22 +158,6 @@ func TestRocksTickers(t *testing.T) {
t.Errorf("Ticker %v found unexpectedly for aud vsCurrency", ticker) t.Errorf("Ticker %v found unexpectedly for aud vsCurrency", ticker)
} }
ticker = d.is.GetCurrentTicker("", "")
if ticker != nil {
t.Errorf("FiatRatesGetCurrentTicker %v found unexpectedly", ticker)
}
d.is.SetCurrentTicker(ticker1)
ticker = d.is.GetCurrentTicker("", "")
if err != nil {
t.Errorf("TestRocksTickers err: %+v", err)
} else if ticker == nil {
t.Errorf("Ticker not found")
} else if ticker.Timestamp.Format(FiatRatesTimeFormat) != ticker1.Timestamp.Format(FiatRatesTimeFormat) {
t.Errorf("Incorrect ticker found. Expected: %v, found: %+v", ticker1.Timestamp, ticker.Timestamp)
}
d.is.SetCurrentTicker(nil)
} }
func Test_packUnpackCurrencyRatesTicker(t *testing.T) { func Test_packUnpackCurrencyRatesTicker(t *testing.T) {

View File

@ -190,13 +190,15 @@ func (cg *Coingecko) coinsList() (coinList, error) {
} }
// coinMarketChart /coins/{id}/market_chart?vs_currency={usd, eur, jpy, etc.}&days={1,14,30,max} // coinMarketChart /coins/{id}/market_chart?vs_currency={usd, eur, jpy, etc.}&days={1,14,30,max}
func (cg *Coingecko) coinMarketChart(id string, vs_currency string, days string) (*marketChartPrices, error) { func (cg *Coingecko) coinMarketChart(id string, vs_currency string, days string, daily bool) (*marketChartPrices, error) {
if len(id) == 0 || len(vs_currency) == 0 || len(days) == 0 { if len(id) == 0 || len(vs_currency) == 0 || len(days) == 0 {
return nil, fmt.Errorf("id, vs_currency, and days is required") return nil, fmt.Errorf("id, vs_currency, and days is required")
} }
params := url.Values{} params := url.Values{}
params.Add("interval", "daily") if daily {
params.Add("interval", "daily")
}
params.Add("vs_currency", vs_currency) params.Add("vs_currency", vs_currency)
params.Add("days", days) params.Add("days", days)
@ -241,6 +243,7 @@ func (cg *Coingecko) platformIds() error {
return nil return nil
} }
// CurrentTickers returns the latest exchange rates
func (cg *Coingecko) CurrentTickers() (*common.CurrencyRatesTicker, error) { func (cg *Coingecko) CurrentTickers() (*common.CurrencyRatesTicker, error) {
cg.updatingCurrent = true cg.updatingCurrent = true
defer func() { cg.updatingCurrent = false }() defer func() { cg.updatingCurrent = false }()
@ -296,6 +299,16 @@ func (cg *Coingecko) CurrentTickers() (*common.CurrencyRatesTicker, error) {
return &newTickers, nil return &newTickers, nil
} }
// HourlyTickers returns the array of the exchange rates in hourly granularity
func (cg *Coingecko) HourlyTickers() (*[]common.CurrencyRatesTicker, error) {
return nil, nil
}
// HourlyTickers returns the array of the exchange rates in five minutes granularity
func (cg *Coingecko) FiveMinutesTickers() (*[]common.CurrencyRatesTicker, error) {
return nil, nil
}
func (cg *Coingecko) getHistoricalTicker(tickersToUpdate map[uint]*common.CurrencyRatesTicker, coinId string, vsCurrency string, token string) (bool, error) { func (cg *Coingecko) getHistoricalTicker(tickersToUpdate map[uint]*common.CurrencyRatesTicker, coinId string, vsCurrency string, token string) (bool, error) {
lastTicker, err := cg.db.FiatRatesFindLastTicker(vsCurrency, token) lastTicker, err := cg.db.FiatRatesFindLastTicker(vsCurrency, token)
if err != nil { if err != nil {
@ -312,7 +325,7 @@ func (cg *Coingecko) getHistoricalTicker(tickersToUpdate map[uint]*common.Curren
} }
days = strconv.Itoa(d) days = strconv.Itoa(d)
} }
mc, err := cg.coinMarketChart(coinId, vsCurrency, days) mc, err := cg.coinMarketChart(coinId, vsCurrency, days, true)
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@ -4,8 +4,10 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"math/rand" "math/rand"
"strings" "strings"
"sync"
"time" "time"
"github.com/golang/glog" "github.com/golang/glog"
@ -13,29 +15,84 @@ import (
"github.com/trezor/blockbook/db" "github.com/trezor/blockbook/db"
) )
const CurrentTickersKey = "CurrentTickers"
const HourlyTickersKey = "HourlyTickers"
const FiveMinutesTickersKey = "FiveMinutesTickers"
// OnNewFiatRatesTicker is used to send notification about a new FiatRates ticker // OnNewFiatRatesTicker is used to send notification about a new FiatRates ticker
type OnNewFiatRatesTicker func(ticker *common.CurrencyRatesTicker) type OnNewFiatRatesTicker func(ticker *common.CurrencyRatesTicker)
// RatesDownloaderInterface provides method signatures for specific fiat rates downloaders // RatesDownloaderInterface provides method signatures for specific fiat rates downloaders
type RatesDownloaderInterface interface { type RatesDownloaderInterface interface {
CurrentTickers() (*common.CurrencyRatesTicker, error) CurrentTickers() (*common.CurrencyRatesTicker, error)
HourlyTickers() (*[]common.CurrencyRatesTicker, error)
FiveMinutesTickers() (*[]common.CurrencyRatesTicker, error)
UpdateHistoricalTickers() error UpdateHistoricalTickers() error
UpdateHistoricalTokenTickers() error UpdateHistoricalTokenTickers() error
} }
// RatesDownloader stores FiatRates API parameters // FiatRates stores FiatRates API parameters
type RatesDownloader struct { type FiatRates struct {
periodSeconds int64 Enabled bool
db *db.RocksDB periodSeconds int64
timeFormat string db *db.RocksDB
callbackOnNewTicker OnNewFiatRatesTicker timeFormat string
downloader RatesDownloaderInterface callbackOnNewTicker OnNewFiatRatesTicker
downloadTokens bool downloader RatesDownloaderInterface
downloadTokens bool
provider string
allowedVsCurrencies string
mux sync.RWMutex
currentTicker *common.CurrencyRatesTicker
hourlyTickers map[int64]*common.CurrencyRatesTicker
hourlyTickersFrom int64
hourlyTickersTo int64
fiveMinutesTickers map[int64]*common.CurrencyRatesTicker
fiveMinutesTickersFrom int64
fiveMinutesTickersTo int64
dailyTickers map[int64]*common.CurrencyRatesTicker
dailyTickersFrom int64
dailyTickersTo int64
} }
// NewFiatRatesDownloader initializes the downloader for FiatRates API. func tickersToMap(tickers *[]common.CurrencyRatesTicker, granularitySeconds int64) (map[int64]*common.CurrencyRatesTicker, int64, int64) {
func NewFiatRatesDownloader(db *db.RocksDB, apiType string, params string, allowedVsCurrencies string, callback OnNewFiatRatesTicker) (*RatesDownloader, error) { if tickers == nil || len(*tickers) == 0 {
var rd = &RatesDownloader{} return nil, 0, 0
}
halfGranularity := granularitySeconds / 2
m := make(map[int64]*common.CurrencyRatesTicker, len(*tickers))
from := ((*tickers)[0].Timestamp.UTC().Unix() + halfGranularity) % granularitySeconds
to := ((*tickers)[len(*tickers)-1].Timestamp.UTC().Unix() + halfGranularity) % granularitySeconds
return m, from, to
}
// NewFiatRates initializes the FiatRates handler
func NewFiatRates(db *db.RocksDB, configFile string, callback OnNewFiatRatesTicker) (*FiatRates, error) {
var config struct {
FiatRates string `json:"fiat_rates"`
FiatRatesParams string `json:"fiat_rates_params"`
FiatRatesVsCurrencies string `json:"fiat_rates_vs_currencies"`
}
data, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("error reading file %v, %v", configFile, err)
}
err = json.Unmarshal(data, &config)
if err != nil {
return nil, fmt.Errorf("error parsing config file %v, %v", configFile, err)
}
var fr = &FiatRates{
provider: config.FiatRates,
allowedVsCurrencies: config.FiatRatesVsCurrencies,
}
if config.FiatRates == "" || config.FiatRatesParams == "" {
glog.Infof("FiatRates config (%v) is empty, not downloading fiat rates", configFile)
fr.Enabled = false
return fr, nil
}
type fiatRatesParams struct { type fiatRatesParams struct {
URL string `json:"url"` URL string `json:"url"`
Coin string `json:"coin"` Coin string `json:"coin"`
@ -44,80 +101,151 @@ func NewFiatRatesDownloader(db *db.RocksDB, apiType string, params string, allow
PeriodSeconds int64 `json:"periodSeconds"` PeriodSeconds int64 `json:"periodSeconds"`
} }
rdParams := &fiatRatesParams{} rdParams := &fiatRatesParams{}
err := json.Unmarshal([]byte(params), &rdParams) err = json.Unmarshal([]byte(config.FiatRatesParams), &rdParams)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rdParams.URL == "" || rdParams.PeriodSeconds == 0 { if rdParams.URL == "" || rdParams.PeriodSeconds == 0 {
return nil, errors.New("Missing parameters") return nil, errors.New("missing parameters")
} }
rd.timeFormat = "02-01-2006" // Layout string for FiatRates date formatting (DD-MM-YYYY) fr.timeFormat = "02-01-2006" // Layout string for FiatRates date formatting (DD-MM-YYYY)
rd.periodSeconds = rdParams.PeriodSeconds // Time period for syncing the latest market data fr.periodSeconds = rdParams.PeriodSeconds // Time period for syncing the latest market data
if rd.periodSeconds < 60 { // minimum is one minute if fr.periodSeconds < 60 { // minimum is one minute
rd.periodSeconds = 60 fr.periodSeconds = 60
} }
rd.db = db fr.db = db
rd.callbackOnNewTicker = callback fr.callbackOnNewTicker = callback
rd.downloadTokens = rdParams.PlatformIdentifier != "" && rdParams.PlatformVsCurrency != "" fr.downloadTokens = rdParams.PlatformIdentifier != "" && rdParams.PlatformVsCurrency != ""
if rd.downloadTokens { if fr.downloadTokens {
common.TickerRecalculateTokenRate = strings.ToLower(db.GetInternalState().CoinShortcut) != rdParams.PlatformVsCurrency common.TickerRecalculateTokenRate = strings.ToLower(db.GetInternalState().CoinShortcut) != rdParams.PlatformVsCurrency
common.TickerTokenVsCurrency = rdParams.PlatformVsCurrency common.TickerTokenVsCurrency = rdParams.PlatformVsCurrency
} }
is := rd.db.GetInternalState() is := fr.db.GetInternalState()
if apiType == "coingecko" { if fr.provider == "coingecko" {
throttle := true throttle := true
if callback == nil { if callback == nil {
// a small hack - in tests the callback is not used, therefore there is no delay slowing down the test // a small hack - in tests the callback is not used, therefore there is no delay slowing down the test
throttle = false throttle = false
} }
rd.downloader = NewCoinGeckoDownloader(db, rdParams.URL, rdParams.Coin, rdParams.PlatformIdentifier, rdParams.PlatformVsCurrency, allowedVsCurrencies, rd.timeFormat, throttle) fr.downloader = NewCoinGeckoDownloader(db, rdParams.URL, rdParams.Coin, rdParams.PlatformIdentifier, rdParams.PlatformVsCurrency, fr.allowedVsCurrencies, fr.timeFormat, throttle)
if is != nil { if is != nil {
is.HasFiatRates = true is.HasFiatRates = true
is.HasTokenFiatRates = rd.downloadTokens is.HasTokenFiatRates = fr.downloadTokens
} fr.Enabled = true
currentTickers, err := db.FiatRatesGetSpecialTickers(CurrentTickersKey)
if err != nil {
glog.Error("FiatRatesDownloader: get CurrentTickers from DB error ", err)
}
if currentTickers != nil && len(*currentTickers) > 0 {
fr.currentTicker = &(*currentTickers)[0]
}
hourlyTickers, err := db.FiatRatesGetSpecialTickers(HourlyTickersKey)
if err != nil {
glog.Error("FiatRatesDownloader: get HourlyTickers from DB error ", err)
}
fr.hourlyTickers, fr.hourlyTickersFrom, fr.hourlyTickersTo = tickersToMap(hourlyTickers, 3600)
fiveMinutesTickers, err := db.FiatRatesGetSpecialTickers(FiveMinutesTickersKey)
if err != nil {
glog.Error("FiatRatesDownloader: get FiveMinutesTickers from DB error ", err)
}
fr.fiveMinutesTickers, fr.fiveMinutesTickersFrom, fr.fiveMinutesTickersTo = tickersToMap(fiveMinutesTickers, 5*60)
}
} else { } else {
return nil, fmt.Errorf("NewFiatRatesDownloader: incorrect API type %q", apiType) return nil, fmt.Errorf("unknown provider %q", fr.provider)
} }
return rd, nil return fr, nil
} }
// Run periodically downloads current (every 15 minutes) and historical (once a day) tickers // GetCurrentTicker returns current ticker
func (rd *RatesDownloader) Run() error { func (fr *FiatRates) GetCurrentTicker(vsCurrency string, token string) *common.CurrencyRatesTicker {
fr.mux.RLock()
currentTicker := fr.currentTicker
fr.mux.RUnlock()
if currentTicker != nil && common.IsSuitableTicker(currentTicker, vsCurrency, token) {
return currentTicker
}
return nil
}
// setCurrentTicker sets current ticker
func (fr *FiatRates) setCurrentTicker(t *common.CurrencyRatesTicker) {
fr.mux.Lock()
defer fr.mux.Unlock()
fr.currentTicker = t
fr.db.FiatRatesStoreSpecialTickers(CurrentTickersKey, &[]common.CurrencyRatesTicker{*t})
}
// setCurrentTicker sets hourly tickers
func (fr *FiatRates) setHourlyTickers(t *[]common.CurrencyRatesTicker) {
fr.mux.Lock()
defer fr.mux.Unlock()
fr.hourlyTickers, fr.hourlyTickersFrom, fr.hourlyTickersTo = tickersToMap(t, 3600)
fr.db.FiatRatesStoreSpecialTickers(HourlyTickersKey, t)
}
// setCurrentTicker sets hourly tickers
func (fr *FiatRates) setFiveMinutesTickers(t *[]common.CurrencyRatesTicker) {
fr.mux.Lock()
defer fr.mux.Unlock()
fr.fiveMinutesTickers, fr.fiveMinutesTickersFrom, fr.fiveMinutesTickersTo = tickersToMap(t, 5*60)
fr.db.FiatRatesStoreSpecialTickers(FiveMinutesTickersKey, t)
}
// RunDownloader periodically downloads current (every 15 minutes) and historical (once a day) tickers
func (fr *FiatRates) RunDownloader() error {
glog.Infof("Starting %v FiatRates downloader...", fr.provider)
var lastHistoricalTickers time.Time var lastHistoricalTickers time.Time
is := rd.db.GetInternalState() is := fr.db.GetInternalState()
tickerFromIs := is.GetCurrentTicker("", "") tickerFromIs := fr.GetCurrentTicker("", "")
firstRun := true firstRun := true
for { for {
unix := time.Now().Unix() unix := time.Now().Unix()
next := unix + rd.periodSeconds next := unix + fr.periodSeconds
next -= next % rd.periodSeconds next -= next % fr.periodSeconds
// skip waiting for the period for the first run if there are no tickerFromIs or they are too old // skip waiting for the period for the first run if there are no tickerFromIs or they are too old
if !firstRun || (tickerFromIs != nil && next-tickerFromIs.Timestamp.Unix() < rd.periodSeconds) { if !firstRun || (tickerFromIs != nil && next-tickerFromIs.Timestamp.Unix() < fr.periodSeconds) {
// wait for the next run with a slight random value to avoid too many request at the same time // wait for the next run with a slight random value to avoid too many request at the same time
next += int64(rand.Intn(12)) next += int64(rand.Intn(12))
time.Sleep(time.Duration(next-unix) * time.Second) time.Sleep(time.Duration(next-unix) * time.Second)
} }
firstRun = false firstRun = false
tickers, err := rd.downloader.CurrentTickers() currentTicker, err := fr.downloader.CurrentTickers()
if err != nil || tickers == nil { if err != nil || currentTicker == nil {
glog.Error("FiatRatesDownloader: CurrentTickers error ", err) glog.Error("FiatRatesDownloader: CurrentTickers error ", err)
} else { } else {
is.SetCurrentTicker(tickers) fr.setCurrentTicker(currentTicker)
glog.Info("FiatRatesDownloader: CurrentTickers updated") glog.Info("FiatRatesDownloader: CurrentTickers updated")
if rd.callbackOnNewTicker != nil { if fr.callbackOnNewTicker != nil {
rd.callbackOnNewTicker(tickers) fr.callbackOnNewTicker(currentTicker)
} }
} }
hourlyTickers, err := fr.downloader.HourlyTickers()
if err != nil || hourlyTickers == nil {
glog.Error("FiatRatesDownloader: HourlyTickers error ", err)
} else {
fr.setHourlyTickers(hourlyTickers)
glog.Info("FiatRatesDownloader: HourlyTickers updated")
}
fiveMinutesTickers, err := fr.downloader.FiveMinutesTickers()
if err != nil || fiveMinutesTickers == nil {
glog.Error("FiatRatesDownloader: FiveMinutesTickers error ", err)
} else {
fr.setFiveMinutesTickers(fiveMinutesTickers)
glog.Info("FiatRatesDownloader: FiveMinutesTickers updated")
}
now := time.Now().UTC() now := time.Now().UTC()
// once a day, 1 hour after UTC midnight (to let the provider prepare historical rates) update historical tickers // once a day, 1 hour after UTC midnight (to let the provider prepare historical rates) update historical tickers
if (now.YearDay() != lastHistoricalTickers.YearDay() || now.Year() != lastHistoricalTickers.Year()) && now.Hour() > 0 { if (now.YearDay() != lastHistoricalTickers.YearDay() || now.Year() != lastHistoricalTickers.Year()) && now.Hour() > 0 {
err = rd.downloader.UpdateHistoricalTickers() err = fr.downloader.UpdateHistoricalTickers()
if err != nil { if err != nil {
glog.Error("FiatRatesDownloader: UpdateHistoricalTickers error ", err) glog.Error("FiatRatesDownloader: UpdateHistoricalTickers error ", err)
} else { } else {
lastHistoricalTickers = time.Now().UTC() lastHistoricalTickers = time.Now().UTC()
ticker, err := rd.db.FiatRatesFindLastTicker("", "") ticker, err := fr.db.FiatRatesFindLastTicker("", "")
if err != nil || ticker == nil { if err != nil || ticker == nil {
glog.Error("FiatRatesDownloader: FiatRatesFindLastTicker error ", err) glog.Error("FiatRatesDownloader: FiatRatesFindLastTicker error ", err)
} else { } else {
@ -126,10 +254,10 @@ func (rd *RatesDownloader) Run() error {
is.HistoricalFiatRatesTime = ticker.Timestamp is.HistoricalFiatRatesTime = ticker.Timestamp
} }
} }
if rd.downloadTokens { if fr.downloadTokens {
// UpdateHistoricalTokenTickers in a goroutine, it can take quite some time as there are many tokens // UpdateHistoricalTokenTickers in a goroutine, it can take quite some time as there are many tokens
go func() { go func() {
err := rd.downloader.UpdateHistoricalTokenTickers() err := fr.downloader.UpdateHistoricalTokenTickers()
if err != nil { if err != nil {
glog.Error("FiatRatesDownloader: UpdateHistoricalTokenTickers error ", err) glog.Error("FiatRatesDownloader: UpdateHistoricalTokenTickers error ", err)
} else { } else {

View File

@ -3,9 +3,8 @@
package fiat package fiat
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -32,7 +31,7 @@ func TestMain(m *testing.M) {
} }
func setupRocksDB(t *testing.T, parser bchain.BlockChainParser) (*db.RocksDB, *common.InternalState, string) { func setupRocksDB(t *testing.T, parser bchain.BlockChainParser) (*db.RocksDB, *common.InternalState, string) {
tmp, err := ioutil.TempDir("", "testdb") tmp, err := os.MkdirTemp("", "testdb")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -75,7 +74,7 @@ func getFiatRatesMockData(name string) (string, error) {
glog.Errorf("Cannot open file %v", filename) glog.Errorf("Cannot open file %v", filename)
return "", err return "", err
} }
b, err := ioutil.ReadAll(mockFile) b, err := io.ReadAll(mockFile)
if err != nil { if err != nil {
glog.Errorf("Cannot read file %v", filename) glog.Errorf("Cannot read file %v", filename)
return "", err return "", err
@ -133,165 +132,159 @@ func TestFiatRates(t *testing.T) {
// mocked CoinGecko API // mocked CoinGecko API
configJSON := `{"fiat_rates": "coingecko", "fiat_rates_params": "{\"url\": \"` + mockServer.URL + `\", \"coin\": \"ethereum\",\"platformIdentifier\":\"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 60}"}` configJSON := `{"fiat_rates": "coingecko", "fiat_rates_params": "{\"url\": \"` + mockServer.URL + `\", \"coin\": \"ethereum\",\"platformIdentifier\":\"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 60}"}`
type fiatRatesConfig struct { configFile, err := os.CreateTemp("", "config*.json")
FiatRates string `json:"fiat_rates"`
FiatRatesParams string `json:"fiat_rates_params"`
}
var config fiatRatesConfig
err := json.Unmarshal([]byte(configJSON), &config)
if err != nil { if err != nil {
t.Fatalf("Error parsing config: %v", err) t.Fatalf("FiatRates configFile error: %v", err)
}
if _, err := configFile.WriteString(configJSON); err != nil {
t.Fatalf("FiatRates configFile WriteString error: %v", err)
}
if err := configFile.Close(); err != nil {
t.Fatalf("FiatRates configFile Close error: %v", err)
} }
if config.FiatRates == "" || config.FiatRatesParams == "" { fiatRates, err := NewFiatRates(d, configFile.Name(), nil)
t.Fatalf("Error parsing FiatRates config - empty parameter")
return
}
fiatRates, err := NewFiatRatesDownloader(d, config.FiatRates, config.FiatRatesParams, "", nil)
if err != nil { if err != nil {
t.Fatalf("FiatRates init error: %v", err) t.Fatalf("FiatRates init error: %v", err)
} }
if config.FiatRates == "coingecko" {
// get current tickers // get current tickers
currentTickers, err := fiatRates.downloader.CurrentTickers() currentTickers, err := fiatRates.downloader.CurrentTickers()
if err != nil { if err != nil {
t.Fatalf("Error in CurrentTickers: %v", err) t.Fatalf("Error in CurrentTickers: %v", err)
return return
} }
if currentTickers == nil { if currentTickers == nil {
t.Fatalf("CurrentTickers returned nil value") t.Fatalf("CurrentTickers returned nil value")
return return
} }
wantCurrentTickers := common.CurrencyRatesTicker{ wantCurrentTickers := common.CurrencyRatesTicker{
Rates: map[string]float32{ Rates: map[string]float32{
"aed": 8447.1, "aed": 8447.1,
"ars": 268901, "ars": 268901,
"aud": 3314.36, "aud": 3314.36,
"btc": 0.07531005, "btc": 0.07531005,
"eth": 1, "eth": 1,
"eur": 2182.99, "eur": 2182.99,
"ltc": 29.097696, "ltc": 29.097696,
"usd": 2299.72, "usd": 2299.72,
}, },
TokenRates: map[string]float32{ TokenRates: map[string]float32{
"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 5.58195e-07, "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 5.58195e-07,
"0x906710835d1ae85275eb770f06873340ca54274b": 1.39852e-10, "0x906710835d1ae85275eb770f06873340ca54274b": 1.39852e-10,
}, },
Timestamp: currentTickers.Timestamp, Timestamp: currentTickers.Timestamp,
} }
if !reflect.DeepEqual(currentTickers, &wantCurrentTickers) { if !reflect.DeepEqual(currentTickers, &wantCurrentTickers) {
t.Fatalf("CurrentTickers() = %v, want %v", *currentTickers, wantCurrentTickers) t.Fatalf("CurrentTickers() = %v, want %v", *currentTickers, wantCurrentTickers)
} }
ticker, err := fiatRates.db.FiatRatesFindLastTicker("usd", "") ticker, err := fiatRates.db.FiatRatesFindLastTicker("usd", "")
if err != nil { if err != nil {
t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
} }
if ticker != nil { if ticker != nil {
t.Fatalf("FiatRatesFindLastTicker found unexpected data") t.Fatalf("FiatRatesFindLastTicker found unexpected data")
} }
// update historical tickers for the first time // update historical tickers for the first time
err = fiatRates.downloader.UpdateHistoricalTickers() err = fiatRates.downloader.UpdateHistoricalTickers()
if err != nil { if err != nil {
t.Fatalf("UpdateHistoricalTickers 1st pass failed with error: %v", err) t.Fatalf("UpdateHistoricalTickers 1st pass failed with error: %v", err)
} }
err = fiatRates.downloader.UpdateHistoricalTokenTickers() err = fiatRates.downloader.UpdateHistoricalTokenTickers()
if err != nil { if err != nil {
t.Fatalf("UpdateHistoricalTokenTickers 1st pass failed with error: %v", err) t.Fatalf("UpdateHistoricalTokenTickers 1st pass failed with error: %v", err)
} }
ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "") ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "")
if err != nil || ticker == nil { if err != nil || ticker == nil {
t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
} }
wantTicker := common.CurrencyRatesTicker{ wantTicker := common.CurrencyRatesTicker{
Rates: map[string]float32{ Rates: map[string]float32{
"aed": 241272.48, "aed": 241272.48,
"ars": 241272.48, "ars": 241272.48,
"aud": 241272.48, "aud": 241272.48,
"btc": 241272.48, "btc": 241272.48,
"eth": 241272.48, "eth": 241272.48,
"eur": 241272.48, "eur": 241272.48,
"ltc": 241272.48, "ltc": 241272.48,
"usd": 1794.5397, "usd": 1794.5397,
}, },
TokenRates: map[string]float32{ TokenRates: map[string]float32{
"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.161734e+07, "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.161734e+07,
"0x906710835d1ae85275eb770f06873340ca54274b": 4.161734e+07, "0x906710835d1ae85275eb770f06873340ca54274b": 4.161734e+07,
}, },
Timestamp: time.Unix(1654732800, 0).UTC(), Timestamp: time.Unix(1654732800, 0).UTC(),
} }
if !reflect.DeepEqual(ticker, &wantTicker) { if !reflect.DeepEqual(ticker, &wantTicker) {
t.Fatalf("UpdateHistoricalTickers(usd) 1st pass = %v, want %v", *ticker, wantTicker) t.Fatalf("UpdateHistoricalTickers(usd) 1st pass = %v, want %v", *ticker, wantTicker)
} }
ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "") ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "")
if err != nil || ticker == nil { if err != nil || ticker == nil {
t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
} }
wantTicker = common.CurrencyRatesTicker{ wantTicker = common.CurrencyRatesTicker{
Rates: map[string]float32{ Rates: map[string]float32{
"aed": 240402.97, "aed": 240402.97,
"ars": 240402.97, "ars": 240402.97,
"aud": 240402.97, "aud": 240402.97,
"btc": 240402.97, "btc": 240402.97,
"eth": 240402.97, "eth": 240402.97,
"eur": 240402.97, "eur": 240402.97,
"ltc": 240402.97, "ltc": 240402.97,
}, },
TokenRates: map[string]float32{ TokenRates: map[string]float32{
"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07, "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07,
"0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07, "0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07,
}, },
Timestamp: time.Unix(1654819200, 0).UTC(), Timestamp: time.Unix(1654819200, 0).UTC(),
} }
if !reflect.DeepEqual(ticker, &wantTicker) { if !reflect.DeepEqual(ticker, &wantTicker) {
t.Fatalf("UpdateHistoricalTickers(eur) 1st pass = %v, want %v", *ticker, wantTicker) t.Fatalf("UpdateHistoricalTickers(eur) 1st pass = %v, want %v", *ticker, wantTicker)
} }
// update historical tickers for the second time // update historical tickers for the second time
err = fiatRates.downloader.UpdateHistoricalTickers() err = fiatRates.downloader.UpdateHistoricalTickers()
if err != nil { if err != nil {
t.Fatalf("UpdateHistoricalTickers 2nd pass failed with error: %v", err) t.Fatalf("UpdateHistoricalTickers 2nd pass failed with error: %v", err)
} }
err = fiatRates.downloader.UpdateHistoricalTokenTickers() err = fiatRates.downloader.UpdateHistoricalTokenTickers()
if err != nil { if err != nil {
t.Fatalf("UpdateHistoricalTokenTickers 2nd pass failed with error: %v", err) t.Fatalf("UpdateHistoricalTokenTickers 2nd pass failed with error: %v", err)
} }
ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "") ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "")
if err != nil || ticker == nil { if err != nil || ticker == nil {
t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
} }
wantTicker = common.CurrencyRatesTicker{ wantTicker = common.CurrencyRatesTicker{
Rates: map[string]float32{ Rates: map[string]float32{
"aed": 240402.97, "aed": 240402.97,
"ars": 240402.97, "ars": 240402.97,
"aud": 240402.97, "aud": 240402.97,
"btc": 240402.97, "btc": 240402.97,
"eth": 240402.97, "eth": 240402.97,
"eur": 240402.97, "eur": 240402.97,
"ltc": 240402.97, "ltc": 240402.97,
"usd": 1788.4183, "usd": 1788.4183,
}, },
TokenRates: map[string]float32{ TokenRates: map[string]float32{
"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07, "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07,
"0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07, "0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07,
}, },
Timestamp: time.Unix(1654819200, 0).UTC(), Timestamp: time.Unix(1654819200, 0).UTC(),
} }
if !reflect.DeepEqual(ticker, &wantTicker) { if !reflect.DeepEqual(ticker, &wantTicker) {
t.Fatalf("UpdateHistoricalTickers(usd) 2nd pass = %v, want %v", *ticker, wantTicker) t.Fatalf("UpdateHistoricalTickers(usd) 2nd pass = %v, want %v", *ticker, wantTicker)
} }
ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "") ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "")
if err != nil || ticker == nil { if err != nil || ticker == nil {
t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
} }
if !reflect.DeepEqual(ticker, &wantTicker) { if !reflect.DeepEqual(ticker, &wantTicker) {
t.Fatalf("UpdateHistoricalTickers(eur) 2nd pass = %v, want %v", *ticker, wantTicker) t.Fatalf("UpdateHistoricalTickers(eur) 2nd pass = %v, want %v", *ticker, wantTicker)
}
} }
} }

View File

@ -14,6 +14,7 @@ import (
"github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/common" "github.com/trezor/blockbook/common"
"github.com/trezor/blockbook/db" "github.com/trezor/blockbook/db"
"github.com/trezor/blockbook/fiat"
) )
// InternalServer is handle to internal http server // InternalServer is handle to internal http server
@ -31,8 +32,8 @@ type InternalServer struct {
} }
// NewInternalServer creates new internal http interface to blockbook and returns its handle // NewInternalServer creates new internal http interface to blockbook and returns its handle
func NewInternalServer(binding, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*InternalServer, error) { func NewInternalServer(binding, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState, fiatRates *fiat.FiatRates) (*InternalServer, error) {
api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -25,6 +25,7 @@ import (
"github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/common" "github.com/trezor/blockbook/common"
"github.com/trezor/blockbook/db" "github.com/trezor/blockbook/db"
"github.com/trezor/blockbook/fiat"
) )
const txsOnPage = 25 const txsOnPage = 25
@ -57,23 +58,24 @@ type PublicServer struct {
explorerURL string explorerURL string
internalExplorer bool internalExplorer bool
is *common.InternalState is *common.InternalState
fiatRates *fiat.FiatRates
} }
// NewPublicServer creates new public server http interface to blockbook and returns its handle // NewPublicServer creates new public server http interface to blockbook and returns its handle
// only basic functionality is mapped, to map all functions, call // only basic functionality is mapped, to map all functions, call
func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, explorerURL string, metrics *common.Metrics, is *common.InternalState, debugMode bool) (*PublicServer, error) { func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, explorerURL string, metrics *common.Metrics, is *common.InternalState, fiatRates *fiat.FiatRates, debugMode bool) (*PublicServer, error) {
api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates)
if err != nil { if err != nil {
return nil, err return nil, err
} }
socketio, err := NewSocketIoServer(db, chain, mempool, txCache, metrics, is) socketio, err := NewSocketIoServer(db, chain, mempool, txCache, metrics, is, fiatRates)
if err != nil { if err != nil {
return nil, err return nil, err
} }
websocket, err := NewWebsocketServer(db, chain, mempool, txCache, metrics, is) websocket, err := NewWebsocketServer(db, chain, mempool, txCache, metrics, is, fiatRates)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -104,6 +106,7 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
explorerURL: explorerURL, explorerURL: explorerURL,
internalExplorer: explorerURL == "", internalExplorer: explorerURL == "",
is: is, is: is,
fiatRates: fiatRates,
} }
s.htmlTemplates.newTemplateData = s.newTemplateData s.htmlTemplates.newTemplateData = s.newTemplateData
s.htmlTemplates.newTemplateDataWithError = s.newTemplateDataWithError s.htmlTemplates.newTemplateDataWithError = s.newTemplateDataWithError
@ -377,10 +380,10 @@ func (s *PublicServer) newTemplateData(r *http.Request) *TemplateData {
secondary = "usd" secondary = "usd"
} }
} }
ticker := s.is.GetCurrentTicker(secondary, "") ticker := s.fiatRates.GetCurrentTicker(secondary, "")
if ticker == nil && secondary != "usd" { if ticker == nil && secondary != "usd" {
secondary = "usd" secondary = "usd"
ticker = s.is.GetCurrentTicker(secondary, "") ticker = s.fiatRates.GetCurrentTicker(secondary, "")
} }
if ticker != nil { if ticker != nil {
t.SecondaryCoin = strings.ToUpper(secondary) t.SecondaryCoin = strings.ToUpper(secondary)

View File

@ -24,6 +24,7 @@ import (
"github.com/trezor/blockbook/bchain/coins/btc" "github.com/trezor/blockbook/bchain/coins/btc"
"github.com/trezor/blockbook/common" "github.com/trezor/blockbook/common"
"github.com/trezor/blockbook/db" "github.com/trezor/blockbook/db"
"github.com/trezor/blockbook/fiat"
"github.com/trezor/blockbook/tests/dbtestdata" "github.com/trezor/blockbook/tests/dbtestdata"
) )
@ -121,7 +122,7 @@ func setupPublicHTTPServer(parser bchain.BlockChainParser, chain bchain.BlockCha
} }
// s.Run is never called, binding can be to any port // s.Run is never called, binding can be to any port
s, err := NewPublicServer("localhost:12345", "", d, chain, mempool, txCache, "", metrics, is, false) s, err := NewPublicServer("localhost:12345", "", d, chain, mempool, txCache, "", metrics, is, &fiat.FiatRates{}, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -17,6 +17,7 @@ import (
"github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/common" "github.com/trezor/blockbook/common"
"github.com/trezor/blockbook/db" "github.com/trezor/blockbook/db"
"github.com/trezor/blockbook/fiat"
) )
// SocketIoServer is handle to SocketIoServer // SocketIoServer is handle to SocketIoServer
@ -33,8 +34,8 @@ type SocketIoServer struct {
} }
// NewSocketIoServer creates new SocketIo interface to blockbook and returns its handle // NewSocketIoServer creates new SocketIo interface to blockbook and returns its handle
func NewSocketIoServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*SocketIoServer, error) { func NewSocketIoServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState, fiatRates *fiat.FiatRates) (*SocketIoServer, error) {
api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -18,6 +18,7 @@ import (
"github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/common" "github.com/trezor/blockbook/common"
"github.com/trezor/blockbook/db" "github.com/trezor/blockbook/db"
"github.com/trezor/blockbook/fiat"
) )
const upgradeFailed = "Upgrade failed: " const upgradeFailed = "Upgrade failed: "
@ -70,8 +71,8 @@ type WebsocketServer struct {
} }
// NewWebsocketServer creates new websocket interface to blockbook and returns its handle // NewWebsocketServer creates new websocket interface to blockbook and returns its handle
func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*WebsocketServer, error) { func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState, fiatRates *fiat.FiatRates) (*WebsocketServer, error) {
api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates)
if err != nil { if err != nil {
return nil, err return nil, err
} }