293 lines
8.2 KiB
Go
293 lines
8.2 KiB
Go
package db
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
|
|
vlq "github.com/bsm/go-vlq"
|
|
"github.com/golang/glog"
|
|
"github.com/juju/errors"
|
|
"github.com/linxGnu/grocksdb"
|
|
)
|
|
|
|
// FiatRatesTimeFormat is a format string for storing FiatRates timestamps in rocksdb
|
|
const FiatRatesTimeFormat = "20060102150405" // YYYYMMDDhhmmss
|
|
|
|
var tickersMux sync.Mutex
|
|
var lastTickerInDB *CurrencyRatesTicker
|
|
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 {
|
|
return []byte(t.UTC().Format(FiatRatesTimeFormat))
|
|
}
|
|
|
|
func packFloat32(buf []byte, n float32) int {
|
|
binary.BigEndian.PutUint32(buf, math.Float32bits(n))
|
|
return 4
|
|
}
|
|
|
|
func unpackFloat32(buf []byte) (float32, int) {
|
|
return math.Float32frombits(binary.BigEndian.Uint32(buf)), 4
|
|
}
|
|
|
|
func packCurrencyRatesTicker(ticker *CurrencyRatesTicker) []byte {
|
|
buf := make([]byte, 0, 32)
|
|
varBuf := make([]byte, vlq.MaxLen64)
|
|
l := packVaruint(uint(len(ticker.Rates)), varBuf)
|
|
buf = append(buf, varBuf[:l]...)
|
|
for c, v := range ticker.Rates {
|
|
buf = append(buf, packString(c)...)
|
|
l = packFloat32(varBuf, v)
|
|
buf = append(buf, varBuf[:l]...)
|
|
}
|
|
l = packVaruint(uint(len(ticker.TokenRates)), varBuf)
|
|
buf = append(buf, varBuf[:l]...)
|
|
for c, v := range ticker.TokenRates {
|
|
buf = append(buf, packString(c)...)
|
|
l = packFloat32(varBuf, v)
|
|
buf = append(buf, varBuf[:l]...)
|
|
}
|
|
return buf
|
|
}
|
|
|
|
func unpackCurrencyRatesTicker(buf []byte) (*CurrencyRatesTicker, error) {
|
|
var (
|
|
ticker CurrencyRatesTicker
|
|
s string
|
|
l int
|
|
len uint
|
|
v float32
|
|
)
|
|
len, l = unpackVaruint(buf)
|
|
buf = buf[l:]
|
|
if len > 0 {
|
|
ticker.Rates = make(map[string]float32, len)
|
|
for i := 0; i < int(len); i++ {
|
|
s, l = unpackString(buf)
|
|
buf = buf[l:]
|
|
v, l = unpackFloat32(buf)
|
|
buf = buf[l:]
|
|
ticker.Rates[s] = v
|
|
}
|
|
}
|
|
len, l = unpackVaruint(buf)
|
|
buf = buf[l:]
|
|
if len > 0 {
|
|
ticker.TokenRates = make(map[string]float32, len)
|
|
for i := 0; i < int(len); i++ {
|
|
s, l = unpackString(buf)
|
|
buf = buf[l:]
|
|
v, l = unpackFloat32(buf)
|
|
buf = buf[l:]
|
|
ticker.TokenRates[s] = v
|
|
}
|
|
}
|
|
return &ticker, nil
|
|
}
|
|
|
|
// FiatRatesConvertDate checks if the date is in correct format and returns the Time object.
|
|
// Possible formats are: YYYYMMDDhhmmss, YYYYMMDDhhmm, YYYYMMDDhh, YYYYMMDD
|
|
func FiatRatesConvertDate(date string) (*time.Time, error) {
|
|
for format := FiatRatesTimeFormat; len(format) >= 8; format = format[:len(format)-2] {
|
|
convertedDate, err := time.Parse(format, date)
|
|
if err == nil {
|
|
return &convertedDate, nil
|
|
}
|
|
}
|
|
msg := "Date \"" + date + "\" does not match any of available formats. "
|
|
msg += "Possible formats are: YYYYMMDDhhmmss, YYYYMMDDhhmm, YYYYMMDDhh, YYYYMMDD"
|
|
return nil, errors.New(msg)
|
|
}
|
|
|
|
// FiatRatesStoreTicker stores ticker data at the specified time
|
|
func (d *RocksDB) FiatRatesStoreTicker(wb *grocksdb.WriteBatch, ticker *CurrencyRatesTicker) error {
|
|
if len(ticker.Rates) == 0 {
|
|
return errors.New("Error storing ticker: empty rates")
|
|
}
|
|
wb.PutCF(d.cfh[cfFiatRates], packTimestamp(&ticker.Timestamp), packCurrencyRatesTicker(ticker))
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func getTickerFromIterator(it *grocksdb.Iterator, vsCurrency string, token string) (*CurrencyRatesTicker, error) {
|
|
timeObj, err := time.Parse(FiatRatesTimeFormat, string(it.Key().Data()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ticker, err := unpackCurrencyRatesTicker(it.Value().Data())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !isSuitableTicker(ticker, vsCurrency, token) {
|
|
return nil, nil
|
|
}
|
|
ticker.Timestamp = timeObj.UTC()
|
|
return ticker, nil
|
|
}
|
|
|
|
// FiatRatesGetTicker gets FiatRates ticker at the specified timestamp if it exist
|
|
func (d *RocksDB) FiatRatesGetTicker(tickerTime *time.Time) (*CurrencyRatesTicker, error) {
|
|
tickerTimeFormatted := tickerTime.UTC().Format(FiatRatesTimeFormat)
|
|
val, err := d.db.GetCF(d.ro, d.cfh[cfFiatRates], []byte(tickerTimeFormatted))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer val.Free()
|
|
data := val.Data()
|
|
if len(data) == 0 {
|
|
return nil, nil
|
|
}
|
|
ticker, err := unpackCurrencyRatesTicker(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ticker.Timestamp = tickerTime.UTC()
|
|
return ticker, nil
|
|
}
|
|
|
|
// 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) {
|
|
tickersMux.Lock()
|
|
if currentTicker != nil {
|
|
if !tickerTime.Before(currentTicker.Timestamp) || (lastTickerInDB != nil && tickerTime.After(lastTickerInDB.Timestamp)) {
|
|
f := true
|
|
if token != "" && currentTicker.TokenRates != nil {
|
|
_, f = currentTicker.TokenRates[token]
|
|
}
|
|
if f {
|
|
tickersMux.Unlock()
|
|
return currentTicker, nil
|
|
}
|
|
}
|
|
}
|
|
tickersMux.Unlock()
|
|
|
|
tickerTimeFormatted := tickerTime.UTC().Format(FiatRatesTimeFormat)
|
|
it := d.db.NewIteratorCF(d.ro, d.cfh[cfFiatRates])
|
|
defer it.Close()
|
|
|
|
for it.Seek([]byte(tickerTimeFormatted)); it.Valid(); it.Next() {
|
|
ticker, err := getTickerFromIterator(it, vsCurrency, token)
|
|
if err != nil {
|
|
glog.Error("FiatRatesFindTicker error: ", err)
|
|
return nil, err
|
|
}
|
|
if ticker != nil {
|
|
return ticker, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// 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) {
|
|
it := d.db.NewIteratorCF(d.ro, d.cfh[cfFiatRates])
|
|
defer it.Close()
|
|
|
|
for it.SeekToLast(); it.Valid(); it.Prev() {
|
|
ticker, err := getTickerFromIterator(it, vsCurrency, token)
|
|
if err != nil {
|
|
glog.Error("FiatRatesFindLastTicker error: ", err)
|
|
return nil, err
|
|
}
|
|
if ticker != nil {
|
|
// if without filter, store the ticker for later use
|
|
if vsCurrency == "" && token == "" {
|
|
tickersMux.Lock()
|
|
lastTickerInDB = ticker
|
|
tickersMux.Unlock()
|
|
}
|
|
return ticker, 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
|
|
}
|