Store current ticker in the internal state

This commit is contained in:
Martin Boehm 2022-11-18 00:19:43 +01:00 committed by Martin
parent 3e816b931f
commit 715567d7f3
13 changed files with 236 additions and 221 deletions

View File

@ -1633,7 +1633,7 @@ func removeEmpty(stringSlice []string) []string {
} }
// getFiatRatesResult checks if CurrencyRatesTicker contains all necessary data and returns formatted result // getFiatRatesResult checks if CurrencyRatesTicker contains all necessary data and returns formatted result
func (w *Worker) getFiatRatesResult(currencies []string, ticker *db.CurrencyRatesTicker, token string) (*FiatTicker, error) { func (w *Worker) getFiatRatesResult(currencies []string, ticker *common.CurrencyRatesTicker, token string) (*FiatTicker, error) {
if token != "" { if token != "" {
if len(currencies) != 1 { if len(currencies) != 1 {
return nil, NewAPIError("Rates for token only for a single currency", true) return nil, NewAPIError("Rates for token only for a single currency", true)
@ -1672,7 +1672,7 @@ func (w *Worker) getFiatRatesResult(currencies []string, ticker *db.CurrencyRate
// GetFiatRatesForBlockID returns fiat rates for block height or block hash // GetFiatRatesForBlockID returns fiat rates for block height or block hash
func (w *Worker) GetFiatRatesForBlockID(blockID string, currencies []string, token string) (*FiatTicker, error) { func (w *Worker) GetFiatRatesForBlockID(blockID string, currencies []string, token string) (*FiatTicker, error) {
var ticker *db.CurrencyRatesTicker var ticker *common.CurrencyRatesTicker
bi, err := w.getBlockInfoFromBlockID(blockID) bi, err := w.getBlockInfoFromBlockID(blockID)
if err != nil { if err != nil {
if err == bchain.ErrBlockNotFound { if err == bchain.ErrBlockNotFound {
@ -1707,13 +1707,14 @@ func (w *Worker) GetCurrentFiatRates(currencies []string, token string) (*FiatTi
if len(currencies) == 1 { if len(currencies) == 1 {
vsCurrency = currencies[0] vsCurrency = currencies[0]
} }
ticker, err := w.db.FiatRatesGetCurrentTicker(vsCurrency, token) ticker := w.is.GetCurrentTicker(vsCurrency, token)
if ticker == nil || err != nil { var err error
if ticker == nil {
ticker, err = w.db.FiatRatesFindLastTicker(vsCurrency, token) ticker, err = w.db.FiatRatesFindLastTicker(vsCurrency, token)
if err != nil { if err != nil {
return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false) return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false)
} else if ticker == nil { } else if ticker == nil {
return nil, NewAPIError(fmt.Sprintf("No tickers found!"), true) return nil, NewAPIError("No tickers found!", true)
} }
} }
result, err := w.getFiatRatesResult(currencies, ticker, token) result, err := w.getFiatRatesResult(currencies, ticker, token)
@ -2101,6 +2102,10 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) {
columnStats = w.is.GetAllDBColumnStats() columnStats = w.is.GetAllDBColumnStats()
internalDBSize = w.is.DBSizeTotal() internalDBSize = w.is.DBSizeTotal()
} }
var currentFiatRatesTime time.Time
if w.is.CurrentTicker != nil {
currentFiatRatesTime = w.is.CurrentTicker.Timestamp
}
blockbookInfo := &BlockbookInfo{ blockbookInfo := &BlockbookInfo{
Coin: w.is.Coin, Coin: w.is.Coin,
Host: w.is.Host, Host: w.is.Host,
@ -2118,7 +2123,7 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) {
Decimals: w.chainParser.AmountDecimals(), Decimals: w.chainParser.AmountDecimals(),
HasFiatRates: w.is.HasFiatRates, HasFiatRates: w.is.HasFiatRates,
HasTokenFiatRates: w.is.HasTokenFiatRates, HasTokenFiatRates: w.is.HasTokenFiatRates,
CurrentFiatRatesTime: nonZeroTime(w.is.CurrentFiatRatesTime), CurrentFiatRatesTime: nonZeroTime(currentFiatRatesTime),
HistoricalFiatRatesTime: nonZeroTime(w.is.HistoricalFiatRatesTime), HistoricalFiatRatesTime: nonZeroTime(w.is.HistoricalFiatRatesTime),
HistoricalTokenFiatRatesTime: nonZeroTime(w.is.HistoricalTokenFiatRatesTime), HistoricalTokenFiatRatesTime: nonZeroTime(w.is.HistoricalTokenFiatRatesTime),
DbSize: w.db.DatabaseSizeOnDisk(), DbSize: w.db.DatabaseSizeOnDisk(),

View File

@ -527,7 +527,7 @@ func onNewBlockHash(hash string, height uint32) {
} }
} }
func onNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) { func onNewFiatRatesTicker(ticker *common.CurrencyRatesTicker) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
glog.Error("onNewFiatRatesTicker recovered from panic: ", r) glog.Error("onNewFiatRatesTicker recovered from panic: ", r)

View File

@ -0,0 +1,74 @@
package common
import "time"
// CurrencyRatesTicker contains coin ticker data fetched from API
type CurrencyRatesTicker struct {
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
TokenRates map[string]float32 `json:"tokenRates"` // rates of the tokens (identified by the address of the contract) against the base currency
}
// Convert converts value in base currency to toCurrency
func (t *CurrencyRatesTicker) Convert(baseValue float64, toCurrency string) float64 {
rate, found := t.Rates[toCurrency]
if !found {
return 0
}
return baseValue * float64(rate)
}
// ConvertTokenToBase converts token value to base currency
func (t *CurrencyRatesTicker) ConvertTokenToBase(value float64, token string) float64 {
if t.TokenRates != nil {
rate, found := t.TokenRates[token]
if found {
return value * float64(rate)
}
}
return 0
}
// ConvertTokenToBase converts token value to toCurrency currency
func (t *CurrencyRatesTicker) ConvertToken(value float64, token string, toCurrency string) float64 {
baseValue := t.ConvertTokenToBase(value, token)
if baseValue > 0 {
return t.Convert(baseValue, toCurrency)
}
return 0
}
// TokenRateInCurrency return token rate in toCurrency currency
func (t *CurrencyRatesTicker) TokenRateInCurrency(token string, toCurrency string) float32 {
if t.TokenRates != nil {
rate, found := t.TokenRates[token]
if found {
baseRate, found := t.Rates[toCurrency]
if found {
return baseRate * rate
}
}
}
return 0
}
// IsSuitableTicker checks if the ticker can provide data for given vsCurrency or token
func IsSuitableTicker(ticker *CurrencyRatesTicker, vsCurrency string, token string) bool {
if vsCurrency != "" {
if ticker.Rates == nil {
return false
}
if _, found := ticker.Rates[vsCurrency]; !found {
return false
}
}
if token != "" {
if ticker.TokenRates == nil {
return false
}
if _, found := ticker.TokenRates[token]; !found {
return false
}
}
return true
}

View File

@ -0,0 +1,62 @@
package common
import (
"testing"
)
func TestCurrencyRatesTicker_ConvertToken(t *testing.T) {
ticker := &CurrencyRatesTicker{
Rates: map[string]float32{
"usd": 2129.987654321,
"eur": 1332.12345678,
},
TokenRates: map[string]float32{
"0x82df128257a7d7556262e1ab7f1f639d9775b85e": 0.4092341123,
"0x6b175474e89094c44da98b954eedeac495271d0f": 12.32323232323232,
"0xdac17f958d2ee523a2206206994597c13d831ec7": 1332421341235.51234,
},
}
tests := []struct {
name string
value float64
token string
toCurrency string
want float64
}{
{
name: "usd 0x82df128257a7d7556262e1ab7f1f639d9775b85e",
value: 10,
token: "0x82df128257a7d7556262e1ab7f1f639d9775b85e",
toCurrency: "usd",
want: 8716.635514874506,
},
{
name: "eur 0xdac17f958d2ee523a2206206994597c13d831ec7",
value: 23.123,
token: "0xdac17f958d2ee523a2206206994597c13d831ec7",
toCurrency: "eur",
want: 4.104216071804417e+16,
},
{
name: "eur 0xdac17f958d2ee523a2206206994597c13d831ec8",
value: 23.123,
token: "0xdac17f958d2ee523a2206206994597c13d831ec8",
toCurrency: "eur",
want: 0,
},
{
name: "eur 0xdac17f958d2ee523a2206206994597c13d831ec7",
value: 23.123,
token: "0xdac17f958d2ee523a2206206994597c13d831ec7",
toCurrency: "czk",
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ticker.ConvertToken(tt.value, tt.token, tt.toCurrency); got != tt.want {
t.Errorf("CurrencyRatesTicker.ConvertToken() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -78,11 +78,11 @@ type InternalState struct {
UtxoChecked bool `json:"utxoChecked"` UtxoChecked bool `json:"utxoChecked"`
// store only the historical state, not the current state of the fiat rates in DB // store only the historical state, not the current state of the fiat rates in DB
HasFiatRates bool `json:"-"` HasFiatRates bool `json:"-"`
HasTokenFiatRates bool `json:"-"` HasTokenFiatRates bool `json:"-"`
CurrentFiatRatesTime time.Time `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"`
BackendInfo BackendInfo `json:"-"` BackendInfo BackendInfo `json:"-"`
} }
@ -268,6 +268,24 @@ 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

@ -10,64 +10,14 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
"github.com/juju/errors" "github.com/juju/errors"
"github.com/linxGnu/grocksdb" "github.com/linxGnu/grocksdb"
"github.com/trezor/blockbook/common"
) )
// 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 tickersMux sync.Mutex var lastTickerInDB *common.CurrencyRatesTicker
var lastTickerInDB *CurrencyRatesTicker var lastTickerInDBMux sync.Mutex
var currentTicker *CurrencyRatesTicker
// CurrencyRatesTicker contains coin ticker data fetched from API
type CurrencyRatesTicker struct {
Timestamp time.Time // return as unix timestamp in API
Rates map[string]float32 // rates of the base currency against a list of vs currencies
TokenRates map[string]float32 // rates of the tokens (identified by the address of the contract) against the base currency
}
// Convert converts value in base currency to toCurrency
func (t *CurrencyRatesTicker) Convert(baseValue float64, toCurrency string) float64 {
rate, found := t.Rates[toCurrency]
if !found {
return 0
}
return baseValue * float64(rate)
}
// ConvertTokenToBase converts token value to base currency
func (t *CurrencyRatesTicker) ConvertTokenToBase(value float64, token string) float64 {
if t.TokenRates != nil {
rate, found := t.TokenRates[token]
if found {
return value * float64(rate)
}
}
return 0
}
// ConvertTokenToBase converts token value to toCurrency currency
func (t *CurrencyRatesTicker) ConvertToken(value float64, token string, toCurrency string) float64 {
baseValue := t.ConvertTokenToBase(value, token)
if baseValue > 0 {
return t.Convert(baseValue, toCurrency)
}
return 0
}
// TokenRateInCurrency return token rate in toCurrency currency
func (t *CurrencyRatesTicker) TokenRateInCurrency(token string, toCurrency string) float32 {
if t.TokenRates != nil {
rate, found := t.TokenRates[token]
if found {
baseRate, found := t.Rates[toCurrency]
if found {
return baseRate * rate
}
}
}
return 0
}
func packTimestamp(t *time.Time) []byte { func packTimestamp(t *time.Time) []byte {
return []byte(t.UTC().Format(FiatRatesTimeFormat)) return []byte(t.UTC().Format(FiatRatesTimeFormat))
@ -82,7 +32,7 @@ func unpackFloat32(buf []byte) (float32, int) {
return math.Float32frombits(binary.BigEndian.Uint32(buf)), 4 return math.Float32frombits(binary.BigEndian.Uint32(buf)), 4
} }
func packCurrencyRatesTicker(ticker *CurrencyRatesTicker) []byte { func packCurrencyRatesTicker(ticker *common.CurrencyRatesTicker) []byte {
buf := make([]byte, 0, 32) buf := make([]byte, 0, 32)
varBuf := make([]byte, vlq.MaxLen64) varBuf := make([]byte, vlq.MaxLen64)
l := packVaruint(uint(len(ticker.Rates)), varBuf) l := packVaruint(uint(len(ticker.Rates)), varBuf)
@ -102,9 +52,9 @@ func packCurrencyRatesTicker(ticker *CurrencyRatesTicker) []byte {
return buf return buf
} }
func unpackCurrencyRatesTicker(buf []byte) (*CurrencyRatesTicker, error) { func unpackCurrencyRatesTicker(buf []byte) (*common.CurrencyRatesTicker, error) {
var ( var (
ticker CurrencyRatesTicker ticker common.CurrencyRatesTicker
s string s string
l int l int
len uint len uint
@ -152,7 +102,7 @@ func FiatRatesConvertDate(date string) (*time.Time, error) {
} }
// FiatRatesStoreTicker stores ticker data at the specified time // FiatRatesStoreTicker stores ticker data at the specified time
func (d *RocksDB) FiatRatesStoreTicker(wb *grocksdb.WriteBatch, ticker *CurrencyRatesTicker) error { func (d *RocksDB) FiatRatesStoreTicker(wb *grocksdb.WriteBatch, ticker *common.CurrencyRatesTicker) error {
if len(ticker.Rates) == 0 { if len(ticker.Rates) == 0 {
return errors.New("Error storing ticker: empty rates") return errors.New("Error storing ticker: empty rates")
} }
@ -160,27 +110,7 @@ func (d *RocksDB) FiatRatesStoreTicker(wb *grocksdb.WriteBatch, ticker *Currency
return nil return nil
} }
func isSuitableTicker(ticker *CurrencyRatesTicker, vsCurrency string, token string) bool { func getTickerFromIterator(it *grocksdb.Iterator, vsCurrency string, token string) (*common.CurrencyRatesTicker, error) {
if vsCurrency != "" {
if ticker.Rates == nil {
return false
}
if _, found := ticker.Rates[vsCurrency]; !found {
return false
}
}
if token != "" {
if ticker.TokenRates == nil {
return false
}
if _, found := ticker.TokenRates[token]; !found {
return false
}
}
return true
}
func getTickerFromIterator(it *grocksdb.Iterator, vsCurrency string, token string) (*CurrencyRatesTicker, error) {
timeObj, err := time.Parse(FiatRatesTimeFormat, string(it.Key().Data())) timeObj, err := time.Parse(FiatRatesTimeFormat, string(it.Key().Data()))
if err != nil { if err != nil {
return nil, err return nil, err
@ -189,7 +119,7 @@ func getTickerFromIterator(it *grocksdb.Iterator, vsCurrency string, token strin
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !isSuitableTicker(ticker, vsCurrency, token) { if !common.IsSuitableTicker(ticker, vsCurrency, token) {
return nil, nil return nil, nil
} }
ticker.Timestamp = timeObj.UTC() ticker.Timestamp = timeObj.UTC()
@ -197,7 +127,7 @@ func getTickerFromIterator(it *grocksdb.Iterator, vsCurrency string, token strin
} }
// FiatRatesGetTicker gets FiatRates ticker at the specified timestamp if it exist // FiatRatesGetTicker gets FiatRates ticker at the specified timestamp if it exist
func (d *RocksDB) FiatRatesGetTicker(tickerTime *time.Time) (*CurrencyRatesTicker, error) { func (d *RocksDB) FiatRatesGetTicker(tickerTime *time.Time) (*common.CurrencyRatesTicker, error) {
tickerTimeFormatted := tickerTime.UTC().Format(FiatRatesTimeFormat) tickerTimeFormatted := tickerTime.UTC().Format(FiatRatesTimeFormat)
val, err := d.db.GetCF(d.ro, d.cfh[cfFiatRates], []byte(tickerTimeFormatted)) val, err := d.db.GetCF(d.ro, d.cfh[cfFiatRates], []byte(tickerTimeFormatted))
if err != nil { if err != nil {
@ -217,21 +147,22 @@ func (d *RocksDB) FiatRatesGetTicker(tickerTime *time.Time) (*CurrencyRatesTicke
} }
// 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) (*CurrencyRatesTicker, error) { func (d *RocksDB) FiatRatesFindTicker(tickerTime *time.Time, vsCurrency string, token string) (*common.CurrencyRatesTicker, error) {
tickersMux.Lock() currentTicker := d.is.GetCurrentTicker("", "")
lastTickerInDBMux.Lock()
dbTicker := lastTickerInDB
lastTickerInDBMux.Unlock()
if currentTicker != nil { if currentTicker != nil {
if !tickerTime.Before(currentTicker.Timestamp) || (lastTickerInDB != nil && tickerTime.After(lastTickerInDB.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 {
tickersMux.Unlock()
return currentTicker, nil return currentTicker, nil
} }
} }
} }
tickersMux.Unlock()
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])
@ -251,7 +182,7 @@ func (d *RocksDB) FiatRatesFindTicker(tickerTime *time.Time, vsCurrency string,
} }
// FiatRatesFindLastTicker gets the last FiatRates record, of the base currency, vsCurrency or the token if specified // FiatRatesFindLastTicker gets the last FiatRates record, of the base currency, vsCurrency or the token if specified
func (d *RocksDB) FiatRatesFindLastTicker(vsCurrency string, token string) (*CurrencyRatesTicker, error) { func (d *RocksDB) FiatRatesFindLastTicker(vsCurrency string, token string) (*common.CurrencyRatesTicker, error) {
it := d.db.NewIteratorCF(d.ro, d.cfh[cfFiatRates]) it := d.db.NewIteratorCF(d.ro, d.cfh[cfFiatRates])
defer it.Close() defer it.Close()
@ -264,29 +195,12 @@ func (d *RocksDB) FiatRatesFindLastTicker(vsCurrency string, token string) (*Cur
if ticker != nil { if ticker != nil {
// if without filter, store the ticker for later use // if without filter, store the ticker for later use
if vsCurrency == "" && token == "" { if vsCurrency == "" && token == "" {
tickersMux.Lock() lastTickerInDBMux.Lock()
lastTickerInDB = ticker lastTickerInDB = ticker
tickersMux.Unlock() lastTickerInDBMux.Unlock()
} }
return ticker, nil return ticker, nil
} }
} }
return nil, nil return nil, nil
} }
// FiatRatesGetCurrentTicker returns current ticker
func (d *RocksDB) FiatRatesGetCurrentTicker(vsCurrency string, token string) (*CurrencyRatesTicker, error) {
tickersMux.Lock()
defer tickersMux.Unlock()
if currentTicker != nil && isSuitableTicker(currentTicker, vsCurrency, token) {
return currentTicker, nil
}
return nil, nil
}
// FiatRatesCurrentTicker sets current ticker
func (d *RocksDB) FiatRatesSetCurrentTicker(t *CurrencyRatesTicker) {
tickersMux.Lock()
defer tickersMux.Unlock()
currentTicker = t
}

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/linxGnu/grocksdb" "github.com/linxGnu/grocksdb"
"github.com/trezor/blockbook/common"
) )
func TestRocksTickers(t *testing.T) { func TestRocksTickers(t *testing.T) {
@ -37,7 +38,7 @@ func TestRocksTickers(t *testing.T) {
futureKey, _ := time.Parse(FiatRatesTimeFormat, "20190630000000") futureKey, _ := time.Parse(FiatRatesTimeFormat, "20190630000000")
ts1, _ := time.Parse(FiatRatesTimeFormat, "20190628000000") ts1, _ := time.Parse(FiatRatesTimeFormat, "20190628000000")
ticker1 := &CurrencyRatesTicker{ ticker1 := &common.CurrencyRatesTicker{
Timestamp: ts1, Timestamp: ts1,
Rates: map[string]float32{ Rates: map[string]float32{
"usd": 20000, "usd": 20000,
@ -49,7 +50,7 @@ func TestRocksTickers(t *testing.T) {
} }
ts2, _ := time.Parse(FiatRatesTimeFormat, "20190629000000") ts2, _ := time.Parse(FiatRatesTimeFormat, "20190629000000")
ticker2 := &CurrencyRatesTicker{ ticker2 := &common.CurrencyRatesTicker{
Timestamp: ts2, Timestamp: ts2,
Rates: map[string]float32{ Rates: map[string]float32{
"usd": 30000, "usd": 30000,
@ -157,15 +158,13 @@ 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, err = d.FiatRatesGetCurrentTicker("", "") ticker = d.is.GetCurrentTicker("", "")
if err != nil { if ticker != nil {
t.Errorf("TestRocksTickers err: %+v", err)
} else if ticker != nil {
t.Errorf("FiatRatesGetCurrentTicker %v found unexpectedly", ticker) t.Errorf("FiatRatesGetCurrentTicker %v found unexpectedly", ticker)
} }
d.FiatRatesSetCurrentTicker(ticker1) d.is.SetCurrentTicker(ticker1)
ticker, err = d.FiatRatesGetCurrentTicker("", "") ticker = d.is.GetCurrentTicker("", "")
if err != nil { if err != nil {
t.Errorf("TestRocksTickers err: %+v", err) t.Errorf("TestRocksTickers err: %+v", err)
} else if ticker == nil { } else if ticker == nil {
@ -174,7 +173,7 @@ func TestRocksTickers(t *testing.T) {
t.Errorf("Incorrect ticker found. Expected: %v, found: %+v", ticker1.Timestamp, ticker.Timestamp) t.Errorf("Incorrect ticker found. Expected: %v, found: %+v", ticker1.Timestamp, ticker.Timestamp)
} }
d.FiatRatesSetCurrentTicker(nil) d.is.SetCurrentTicker(nil)
} }
func Test_packUnpackCurrencyRatesTicker(t *testing.T) { func Test_packUnpackCurrencyRatesTicker(t *testing.T) {
@ -182,15 +181,15 @@ func Test_packUnpackCurrencyRatesTicker(t *testing.T) {
} }
tests := []struct { tests := []struct {
name string name string
data CurrencyRatesTicker data common.CurrencyRatesTicker
}{ }{
{ {
name: "empty", name: "empty",
data: CurrencyRatesTicker{}, data: common.CurrencyRatesTicker{},
}, },
{ {
name: "rates", name: "rates",
data: CurrencyRatesTicker{ data: common.CurrencyRatesTicker{
Rates: map[string]float32{ Rates: map[string]float32{
"usd": 2129.2341123, "usd": 2129.2341123,
"eur": 1332.51234, "eur": 1332.51234,
@ -199,7 +198,7 @@ func Test_packUnpackCurrencyRatesTicker(t *testing.T) {
}, },
{ {
name: "rates&tokenrates", name: "rates&tokenrates",
data: CurrencyRatesTicker{ data: common.CurrencyRatesTicker{
Rates: map[string]float32{ Rates: map[string]float32{
"usd": 322129.987654321, "usd": 322129.987654321,
"eur": 291332.12345678, "eur": 291332.12345678,
@ -226,64 +225,3 @@ func Test_packUnpackCurrencyRatesTicker(t *testing.T) {
}) })
} }
} }
func TestCurrencyRatesTicker_ConvertToken(t *testing.T) {
ticker := &CurrencyRatesTicker{
Rates: map[string]float32{
"usd": 2129.987654321,
"eur": 1332.12345678,
},
TokenRates: map[string]float32{
"0x82df128257a7d7556262e1ab7f1f639d9775b85e": 0.4092341123,
"0x6b175474e89094c44da98b954eedeac495271d0f": 12.32323232323232,
"0xdac17f958d2ee523a2206206994597c13d831ec7": 1332421341235.51234,
},
}
type args struct {
baseValue float64
toCurrency string
}
tests := []struct {
name string
value float64
token string
toCurrency string
want float64
}{
{
name: "usd 0x82df128257a7d7556262e1ab7f1f639d9775b85e",
value: 10,
token: "0x82df128257a7d7556262e1ab7f1f639d9775b85e",
toCurrency: "usd",
want: 8716.635514874506,
},
{
name: "eur 0xdac17f958d2ee523a2206206994597c13d831ec7",
value: 23.123,
token: "0xdac17f958d2ee523a2206206994597c13d831ec7",
toCurrency: "eur",
want: 4.104216071804417e+16,
},
{
name: "eur 0xdac17f958d2ee523a2206206994597c13d831ec8",
value: 23.123,
token: "0xdac17f958d2ee523a2206206994597c13d831ec8",
toCurrency: "eur",
want: 0,
},
{
name: "eur 0xdac17f958d2ee523a2206206994597c13d831ec7",
value: 23.123,
token: "0xdac17f958d2ee523a2206206994597c13d831ec7",
toCurrency: "czk",
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ticker.ConvertToken(tt.value, tt.token, tt.toCurrency); got != tt.want {
t.Errorf("CurrencyRatesTicker.ConvertToken() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -12,6 +12,7 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
"github.com/linxGnu/grocksdb" "github.com/linxGnu/grocksdb"
"github.com/trezor/blockbook/common"
"github.com/trezor/blockbook/db" "github.com/trezor/blockbook/db"
) )
@ -223,11 +224,11 @@ func (cg *Coingecko) platformIds() error {
return nil return nil
} }
func (cg *Coingecko) CurrentTickers() (*db.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 }()
var newTickers = db.CurrencyRatesTicker{} var newTickers = common.CurrencyRatesTicker{}
if vsCurrencies == nil { if vsCurrencies == nil {
vs, err := cg.simpleSupportedVSCurrencies() vs, err := cg.simpleSupportedVSCurrencies()
@ -275,7 +276,7 @@ func (cg *Coingecko) CurrentTickers() (*db.CurrencyRatesTicker, error) {
return &newTickers, nil return &newTickers, nil
} }
func (cg *Coingecko) getHistoricalTicker(tickersToUpdate map[uint]*db.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 {
return false, err return false, err
@ -306,7 +307,7 @@ func (cg *Coingecko) getHistoricalTicker(tickersToUpdate map[uint]*db.CurrencyRa
rate := float32(p[1]) rate := float32(p[1])
if timestamp%(24*3600) == 0 && timestamp != 0 && rate != 0 { // process only tickers for the whole day with non 0 value if timestamp%(24*3600) == 0 && timestamp != 0 && rate != 0 { // process only tickers for the whole day with non 0 value
var found bool var found bool
var ticker *db.CurrencyRatesTicker var ticker *common.CurrencyRatesTicker
if ticker, found = tickersToUpdate[timestamp]; !found { if ticker, found = tickersToUpdate[timestamp]; !found {
u := time.Unix(int64(timestamp), 0).UTC() u := time.Unix(int64(timestamp), 0).UTC()
ticker, err = cg.db.FiatRatesGetTicker(&u) ticker, err = cg.db.FiatRatesGetTicker(&u)
@ -321,7 +322,7 @@ func (cg *Coingecko) getHistoricalTicker(tickersToUpdate map[uint]*db.CurrencyRa
} }
continue continue
} }
ticker = &db.CurrencyRatesTicker{ ticker = &common.CurrencyRatesTicker{
Timestamp: u, Timestamp: u,
Rates: make(map[string]float32), Rates: make(map[string]float32),
} }
@ -341,7 +342,7 @@ func (cg *Coingecko) getHistoricalTicker(tickersToUpdate map[uint]*db.CurrencyRa
return true, nil return true, nil
} }
func (cg *Coingecko) storeTickers(tickersToUpdate map[uint]*db.CurrencyRatesTicker) error { func (cg *Coingecko) storeTickers(tickersToUpdate map[uint]*common.CurrencyRatesTicker) error {
if len(tickersToUpdate) > 0 { if len(tickersToUpdate) > 0 {
wb := grocksdb.NewWriteBatch() wb := grocksdb.NewWriteBatch()
defer wb.Destroy() defer wb.Destroy()
@ -368,7 +369,7 @@ func (cg *Coingecko) throttleHistoricalDownload() {
// UpdateHistoricalTickers gets historical tickers for the main crypto currency // UpdateHistoricalTickers gets historical tickers for the main crypto currency
func (cg *Coingecko) UpdateHistoricalTickers() error { func (cg *Coingecko) UpdateHistoricalTickers() error {
tickersToUpdate := make(map[uint]*db.CurrencyRatesTicker) tickersToUpdate := make(map[uint]*common.CurrencyRatesTicker)
// reload vs_currencies // reload vs_currencies
vs, err := cg.simpleSupportedVSCurrencies() vs, err := cg.simpleSupportedVSCurrencies()
@ -401,7 +402,7 @@ func (cg *Coingecko) UpdateHistoricalTokenTickers() error {
} }
cg.updatingTokens = true cg.updatingTokens = true
defer func() { cg.updatingTokens = false }() defer func() { cg.updatingTokens = false }()
tickersToUpdate := make(map[uint]*db.CurrencyRatesTicker) tickersToUpdate := make(map[uint]*common.CurrencyRatesTicker)
if cg.platformIdentifier != "" && cg.platformVsCurrency != "" { if cg.platformIdentifier != "" && cg.platformVsCurrency != "" {
// reload platform ids // reload platform ids
@ -425,7 +426,7 @@ func (cg *Coingecko) UpdateHistoricalTokenTickers() error {
if err != nil { if err != nil {
return err return err
} }
tickersToUpdate = make(map[uint]*db.CurrencyRatesTicker) tickersToUpdate = make(map[uint]*common.CurrencyRatesTicker)
glog.Infof("Coingecko updated %d of %d token tickers", count, len(platformIds)) glog.Infof("Coingecko updated %d of %d token tickers", count, len(platformIds))
} }
if req { if req {

View File

@ -8,15 +8,16 @@ import (
"time" "time"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/trezor/blockbook/common"
"github.com/trezor/blockbook/db" "github.com/trezor/blockbook/db"
) )
// 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 *db.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() (*db.CurrencyRatesTicker, error) CurrentTickers() (*common.CurrencyRatesTicker, error)
UpdateHistoricalTickers() error UpdateHistoricalTickers() error
UpdateHistoricalTokenTickers() error UpdateHistoricalTokenTickers() error
} }
@ -80,17 +81,25 @@ func NewFiatRatesDownloader(db *db.RocksDB, apiType string, params string, callb
func (rd *RatesDownloader) Run() error { func (rd *RatesDownloader) Run() error {
var lastHistoricalTickers time.Time var lastHistoricalTickers time.Time
is := rd.db.GetInternalState() is := rd.db.GetInternalState()
tickerFromIs := is.GetCurrentTicker("", "")
firstRun := true
for { for {
unix := time.Now().Unix()
next := unix + rd.periodSeconds
next -= next % rd.periodSeconds
// 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) {
// wait for the next run with a slight random value to avoid too many request at the same time
next += int64(rand.Intn(12))
time.Sleep(time.Duration(next-unix) * time.Second)
}
firstRun = false
tickers, err := rd.downloader.CurrentTickers() tickers, err := rd.downloader.CurrentTickers()
if err != nil || tickers == nil { if err != nil || tickers == nil {
glog.Error("FiatRatesDownloader: CurrentTickers error ", err) glog.Error("FiatRatesDownloader: CurrentTickers error ", err)
} else { } else {
rd.db.FiatRatesSetCurrentTicker(tickers) is.SetCurrentTicker(tickers)
glog.Info("FiatRatesDownloader: CurrentTickers updated") glog.Info("FiatRatesDownloader: CurrentTickers updated")
if is != nil {
is.CurrentFiatRatesTime = time.Now()
}
if rd.callbackOnNewTicker != nil { if rd.callbackOnNewTicker != nil {
rd.callbackOnNewTicker(tickers) rd.callbackOnNewTicker(tickers)
} }
@ -128,11 +137,5 @@ func (rd *RatesDownloader) Run() error {
} }
} }
} }
// wait for the next run with a slight random value to avoid too many request at the same time
unix := time.Now().Unix()
next := unix + rd.periodSeconds
next -= next % rd.periodSeconds
next += int64(rand.Intn(12))
time.Sleep(time.Duration(next-unix) * time.Second)
} }
} }

View File

@ -165,7 +165,7 @@ func TestFiatRates(t *testing.T) {
return return
} }
wantCurrentTickers := db.CurrencyRatesTicker{ wantCurrentTickers := common.CurrencyRatesTicker{
Rates: map[string]float32{ Rates: map[string]float32{
"aed": 8447.1, "aed": 8447.1,
"ars": 268901, "ars": 268901,
@ -208,7 +208,7 @@ func TestFiatRates(t *testing.T) {
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 := db.CurrencyRatesTicker{ wantTicker := common.CurrencyRatesTicker{
Rates: map[string]float32{ Rates: map[string]float32{
"aed": 241272.48, "aed": 241272.48,
"ars": 241272.48, "ars": 241272.48,
@ -233,7 +233,7 @@ func TestFiatRates(t *testing.T) {
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 = db.CurrencyRatesTicker{ wantTicker = common.CurrencyRatesTicker{
Rates: map[string]float32{ Rates: map[string]float32{
"aed": 240402.97, "aed": 240402.97,
"ars": 240402.97, "ars": 240402.97,
@ -266,7 +266,7 @@ func TestFiatRates(t *testing.T) {
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 = db.CurrencyRatesTicker{ wantTicker = common.CurrencyRatesTicker{
Rates: map[string]float32{ Rates: map[string]float32{
"aed": 240402.97, "aed": 240402.97,
"ars": 240402.97, "ars": 240402.97,

View File

@ -222,7 +222,7 @@ func (s *PublicServer) OnNewBlock(hash string, height uint32) {
} }
// OnNewFiatRatesTicker notifies users subscribed to bitcoind/fiatrates about new ticker // OnNewFiatRatesTicker notifies users subscribed to bitcoind/fiatrates about new ticker
func (s *PublicServer) OnNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) { func (s *PublicServer) OnNewFiatRatesTicker(ticker *common.CurrencyRatesTicker) {
s.websocket.OnNewFiatRatesTicker(ticker) s.websocket.OnNewFiatRatesTicker(ticker)
} }

View File

@ -173,7 +173,7 @@ func insertFiatRate(date string, rates map[string]float32, tokenRates map[string
if err != nil { if err != nil {
return err return err
} }
ticker := &db.CurrencyRatesTicker{ ticker := &common.CurrencyRatesTicker{
Timestamp: *convertedDate, Timestamp: *convertedDate,
Rates: rates, Rates: rates,
TokenRates: tokenRates, TokenRates: tokenRates,

View File

@ -979,7 +979,7 @@ func (s *WebsocketServer) OnNewTx(tx *bchain.MempoolTx) {
} }
} }
func (s *WebsocketServer) broadcastTicker(currency string, rates map[string]float32, ticker *db.CurrencyRatesTicker) { func (s *WebsocketServer) broadcastTicker(currency string, rates map[string]float32, ticker *common.CurrencyRatesTicker) {
as, ok := s.fiatRatesSubscriptions[currency] as, ok := s.fiatRatesSubscriptions[currency]
if ok && len(as) > 0 { if ok && len(as) > 0 {
data := struct { data := struct {
@ -1022,7 +1022,7 @@ func (s *WebsocketServer) broadcastTicker(currency string, rates map[string]floa
} }
// OnNewFiatRatesTicker is a callback that broadcasts info about fiat rates affecting subscribed currency // OnNewFiatRatesTicker is a callback that broadcasts info about fiat rates affecting subscribed currency
func (s *WebsocketServer) OnNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) { func (s *WebsocketServer) OnNewFiatRatesTicker(ticker *common.CurrencyRatesTicker) {
s.fiatRatesSubscriptionsLock.Lock() s.fiatRatesSubscriptionsLock.Lock()
defer s.fiatRatesSubscriptionsLock.Unlock() defer s.fiatRatesSubscriptionsLock.Unlock()
for currency, rate := range ticker.Rates { for currency, rate := range ticker.Rates {