Fiat rates refactor, fetch rates for tokens
This commit is contained in:
parent
db91824dc3
commit
e0be8aa400
21
api/types.go
21
api/types.go
@ -331,7 +331,7 @@ type BalanceHistory struct {
|
||||
ReceivedSat *Amount `json:"received"`
|
||||
SentSat *Amount `json:"sent"`
|
||||
SentToSelfSat *Amount `json:"sentToSelf"`
|
||||
FiatRates map[string]float64 `json:"rates,omitempty"`
|
||||
FiatRates map[string]float32 `json:"rates,omitempty"`
|
||||
Txid string `json:"txid,omitempty"`
|
||||
}
|
||||
|
||||
@ -468,3 +468,22 @@ type MempoolTxids struct {
|
||||
Mempool []MempoolTxid `json:"mempool"`
|
||||
MempoolSize int `json:"mempoolSize"`
|
||||
}
|
||||
|
||||
// FiatTicker contains formatted CurrencyRatesTicker data
|
||||
type FiatTicker struct {
|
||||
Timestamp int64 `json:"ts,omitempty"`
|
||||
Rates map[string]float32 `json:"rates"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// FiatTickers contains a formatted CurrencyRatesTicker list
|
||||
type FiatTickers struct {
|
||||
Tickers []FiatTicker `json:"tickers"`
|
||||
}
|
||||
|
||||
// AvailableVsCurrencies contains formatted data about available versus currencies for exchange rates
|
||||
type AvailableVsCurrencies struct {
|
||||
Timestamp int64 `json:"ts,omitempty"`
|
||||
Tickers []string `json:"available_currencies"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
@ -1342,7 +1342,8 @@ func (w *Worker) setFiatRateToBalanceHistories(histories BalanceHistories, curre
|
||||
for i := range histories {
|
||||
bh := &histories[i]
|
||||
t := time.Unix(int64(bh.Time), 0)
|
||||
ticker, err := w.db.FiatRatesFindTicker(&t)
|
||||
// TODO
|
||||
ticker, err := w.db.FiatRatesFindTicker(&t, "", "")
|
||||
if err != nil {
|
||||
glog.Errorf("Error finding ticker by date %v. Error: %v", t, err)
|
||||
continue
|
||||
@ -1352,7 +1353,7 @@ func (w *Worker) setFiatRateToBalanceHistories(histories BalanceHistories, curre
|
||||
if len(currencies) == 0 {
|
||||
bh.FiatRates = ticker.Rates
|
||||
} else {
|
||||
rates := make(map[string]float64)
|
||||
rates := make(map[string]float32)
|
||||
for _, currency := range currencies {
|
||||
currency = strings.ToLower(currency)
|
||||
if rate, found := ticker.Rates[currency]; found {
|
||||
@ -1593,17 +1594,17 @@ func removeEmpty(stringSlice []string) []string {
|
||||
}
|
||||
|
||||
// getFiatRatesResult checks if CurrencyRatesTicker contains all necessary data and returns formatted result
|
||||
func (w *Worker) getFiatRatesResult(currencies []string, ticker *db.CurrencyRatesTicker) (*db.ResultTickerAsString, error) {
|
||||
func (w *Worker) getFiatRatesResult(currencies []string, ticker *db.CurrencyRatesTicker) (*FiatTicker, error) {
|
||||
currencies = removeEmpty(currencies)
|
||||
if len(currencies) == 0 {
|
||||
// Return all available ticker rates
|
||||
return &db.ResultTickerAsString{
|
||||
return &FiatTicker{
|
||||
Timestamp: ticker.Timestamp.UTC().Unix(),
|
||||
Rates: ticker.Rates,
|
||||
}, nil
|
||||
}
|
||||
// Check if currencies from the list are available in the ticker rates
|
||||
rates := make(map[string]float64)
|
||||
rates := make(map[string]float32)
|
||||
for _, currency := range currencies {
|
||||
currency = strings.ToLower(currency)
|
||||
if rate, found := ticker.Rates[currency]; found {
|
||||
@ -1612,25 +1613,26 @@ func (w *Worker) getFiatRatesResult(currencies []string, ticker *db.CurrencyRate
|
||||
rates[currency] = -1
|
||||
}
|
||||
}
|
||||
return &db.ResultTickerAsString{
|
||||
return &FiatTicker{
|
||||
Timestamp: ticker.Timestamp.UTC().Unix(),
|
||||
Rates: rates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetFiatRatesForBlockID returns fiat rates for block height or block hash
|
||||
func (w *Worker) GetFiatRatesForBlockID(bid string, currencies []string) (*db.ResultTickerAsString, error) {
|
||||
func (w *Worker) GetFiatRatesForBlockID(blockID string, currencies []string) (*FiatTicker, error) {
|
||||
var ticker *db.CurrencyRatesTicker
|
||||
bi, err := w.getBlockInfoFromBlockID(bid)
|
||||
bi, err := w.getBlockInfoFromBlockID(blockID)
|
||||
if err != nil {
|
||||
if err == bchain.ErrBlockNotFound {
|
||||
return nil, NewAPIError(fmt.Sprintf("Block %v not found", bid), true)
|
||||
return nil, NewAPIError(fmt.Sprintf("Block %v not found", blockID), true)
|
||||
}
|
||||
return nil, NewAPIError(fmt.Sprintf("Block %v not found, error: %v", bid, err), false)
|
||||
return nil, NewAPIError(fmt.Sprintf("Block %v not found, error: %v", blockID, err), false)
|
||||
}
|
||||
dbi := &db.BlockInfo{Time: bi.Time} // get Unix timestamp from block
|
||||
tm := time.Unix(dbi.Time, 0) // convert it to Time object
|
||||
ticker, err = w.db.FiatRatesFindTicker(&tm)
|
||||
// TODO
|
||||
ticker, err = w.db.FiatRatesFindTicker(&tm, "", "")
|
||||
if err != nil {
|
||||
return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false)
|
||||
} else if ticker == nil {
|
||||
@ -1644,8 +1646,9 @@ func (w *Worker) GetFiatRatesForBlockID(bid string, currencies []string) (*db.Re
|
||||
}
|
||||
|
||||
// GetCurrentFiatRates returns last available fiat rates
|
||||
func (w *Worker) GetCurrentFiatRates(currencies []string) (*db.ResultTickerAsString, error) {
|
||||
ticker, err := w.db.FiatRatesFindLastTicker()
|
||||
func (w *Worker) GetCurrentFiatRates(currencies []string) (*FiatTicker, error) {
|
||||
// TODO
|
||||
ticker, err := w.db.FiatRatesFindLastTicker("", "")
|
||||
if err != nil {
|
||||
return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false)
|
||||
} else if ticker == nil {
|
||||
@ -1660,8 +1663,8 @@ func (w *Worker) GetCurrentFiatRates(currencies []string) (*db.ResultTickerAsStr
|
||||
|
||||
// makeErrorRates returns a map of currrencies, with each value equal to -1
|
||||
// used when there was an error finding ticker
|
||||
func makeErrorRates(currencies []string) map[string]float64 {
|
||||
rates := make(map[string]float64)
|
||||
func makeErrorRates(currencies []string) map[string]float32 {
|
||||
rates := make(map[string]float32)
|
||||
for _, currency := range currencies {
|
||||
rates[strings.ToLower(currency)] = -1
|
||||
}
|
||||
@ -1669,28 +1672,29 @@ func makeErrorRates(currencies []string) map[string]float64 {
|
||||
}
|
||||
|
||||
// GetFiatRatesForTimestamps returns fiat rates for each of the provided dates
|
||||
func (w *Worker) GetFiatRatesForTimestamps(timestamps []int64, currencies []string) (*db.ResultTickersAsString, error) {
|
||||
func (w *Worker) GetFiatRatesForTimestamps(timestamps []int64, currencies []string) (*FiatTickers, error) {
|
||||
if len(timestamps) == 0 {
|
||||
return nil, NewAPIError("No timestamps provided", true)
|
||||
}
|
||||
currencies = removeEmpty(currencies)
|
||||
|
||||
ret := &db.ResultTickersAsString{}
|
||||
ret := &FiatTickers{}
|
||||
for _, timestamp := range timestamps {
|
||||
date := time.Unix(timestamp, 0)
|
||||
date = date.UTC()
|
||||
ticker, err := w.db.FiatRatesFindTicker(&date)
|
||||
// TODO
|
||||
ticker, err := w.db.FiatRatesFindTicker(&date, "", "")
|
||||
if err != nil {
|
||||
glog.Errorf("Error finding ticker for date %v. Error: %v", date, err)
|
||||
ret.Tickers = append(ret.Tickers, db.ResultTickerAsString{Timestamp: date.Unix(), Rates: makeErrorRates(currencies)})
|
||||
ret.Tickers = append(ret.Tickers, FiatTicker{Timestamp: date.Unix(), Rates: makeErrorRates(currencies)})
|
||||
continue
|
||||
} else if ticker == nil {
|
||||
ret.Tickers = append(ret.Tickers, db.ResultTickerAsString{Timestamp: date.Unix(), Rates: makeErrorRates(currencies)})
|
||||
ret.Tickers = append(ret.Tickers, FiatTicker{Timestamp: date.Unix(), Rates: makeErrorRates(currencies)})
|
||||
continue
|
||||
}
|
||||
result, err := w.getFiatRatesResult(currencies, ticker)
|
||||
if err != nil {
|
||||
ret.Tickers = append(ret.Tickers, db.ResultTickerAsString{Timestamp: date.Unix(), Rates: makeErrorRates(currencies)})
|
||||
ret.Tickers = append(ret.Tickers, FiatTicker{Timestamp: date.Unix(), Rates: makeErrorRates(currencies)})
|
||||
continue
|
||||
}
|
||||
ret.Tickers = append(ret.Tickers, *result)
|
||||
@ -1698,12 +1702,13 @@ func (w *Worker) GetFiatRatesForTimestamps(timestamps []int64, currencies []stri
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// GetFiatRatesTickersList returns the list of available fiatRates tickers
|
||||
func (w *Worker) GetFiatRatesTickersList(timestamp int64) (*db.ResultTickerListAsString, error) {
|
||||
// GetAvailableVsCurrencies returns the list of available versus currencies for exchange rates
|
||||
func (w *Worker) GetAvailableVsCurrencies(timestamp int64) (*AvailableVsCurrencies, error) {
|
||||
date := time.Unix(timestamp, 0)
|
||||
date = date.UTC()
|
||||
|
||||
ticker, err := w.db.FiatRatesFindTicker(&date)
|
||||
// TODO
|
||||
ticker, err := w.db.FiatRatesFindTicker(&date, "", "")
|
||||
if err != nil {
|
||||
return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false)
|
||||
} else if ticker == nil {
|
||||
@ -1716,7 +1721,7 @@ func (w *Worker) GetFiatRatesTickersList(timestamp int64) (*db.ResultTickerListA
|
||||
}
|
||||
sort.Strings(keys) // sort to get deterministic results
|
||||
|
||||
return &db.ResultTickerListAsString{
|
||||
return &AvailableVsCurrencies{
|
||||
Timestamp: ticker.Timestamp.Unix(),
|
||||
Tickers: keys,
|
||||
}, nil
|
||||
|
||||
@ -688,9 +688,9 @@ func initDownloaders(db *db.RocksDB, chain bchain.BlockChain, configfile string)
|
||||
}
|
||||
|
||||
if config.FiatRates == "" || config.FiatRatesParams == "" {
|
||||
glog.Infof("FiatRates config (%v) is empty, not downloading fiat rates.", configfile)
|
||||
glog.Infof("FiatRates config (%v) is empty, not downloading fiat rates", configfile)
|
||||
} else {
|
||||
fiatRates, err := fiat.NewFiatRatesDownloader(db, config.FiatRates, config.FiatRatesParams, nil, onNewFiatRatesTicker)
|
||||
fiatRates, err := fiat.NewFiatRatesDownloader(db, config.FiatRates, config.FiatRatesParams, onNewFiatRatesTicker)
|
||||
if err != nil {
|
||||
glog.Errorf("NewFiatRatesDownloader Init error: %v", err)
|
||||
} else {
|
||||
|
||||
@ -53,6 +53,8 @@
|
||||
"mempoolTxTimeoutHours": 12,
|
||||
"processInternalTransactions": true,
|
||||
"queryBackendOnMempoolResync": false,
|
||||
"fiat_rates": "coingecko",
|
||||
"fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}",
|
||||
"fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/"
|
||||
}
|
||||
}
|
||||
|
||||
246
db/fiat.go
246
db/fiat.go
@ -1,9 +1,13 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
vlq "github.com/bsm/go-vlq"
|
||||
"github.com/flier/gorocksdb"
|
||||
"github.com/golang/glog"
|
||||
"github.com/juju/errors"
|
||||
)
|
||||
@ -11,29 +15,83 @@ import (
|
||||
// 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]float64
|
||||
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
|
||||
}
|
||||
|
||||
// ResultTickerAsString contains formatted CurrencyRatesTicker data
|
||||
type ResultTickerAsString struct {
|
||||
Timestamp int64 `json:"ts,omitempty"`
|
||||
Rates map[string]float64 `json:"rates"`
|
||||
Error string `json:"error,omitempty"`
|
||||
func packTimestamp(t *time.Time) []byte {
|
||||
return []byte(t.UTC().Format(FiatRatesTimeFormat))
|
||||
}
|
||||
|
||||
// ResultTickersAsString contains a formatted CurrencyRatesTicker list
|
||||
type ResultTickersAsString struct {
|
||||
Tickers []ResultTickerAsString `json:"tickers"`
|
||||
func packFloat32(buf []byte, n float32) int {
|
||||
binary.BigEndian.PutUint32(buf, math.Float32bits(n))
|
||||
return 4
|
||||
}
|
||||
|
||||
// ResultTickerListAsString contains formatted data about available currency tickers
|
||||
type ResultTickerListAsString struct {
|
||||
Timestamp int64 `json:"ts,omitempty"`
|
||||
Tickers []string `json:"available_currencies"`
|
||||
Error string `json:"error,omitempty"`
|
||||
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.
|
||||
@ -51,85 +109,131 @@ func FiatRatesConvertDate(date string) (*time.Time, error) {
|
||||
}
|
||||
|
||||
// FiatRatesStoreTicker stores ticker data at the specified time
|
||||
func (d *RocksDB) FiatRatesStoreTicker(ticker *CurrencyRatesTicker) error {
|
||||
func (d *RocksDB) FiatRatesStoreTicker(wb *gorocksdb.WriteBatch, ticker *CurrencyRatesTicker) error {
|
||||
if len(ticker.Rates) == 0 {
|
||||
return errors.New("Error storing ticker: empty rates")
|
||||
} else if ticker.Timestamp == nil {
|
||||
return errors.New("Error storing ticker: empty timestamp")
|
||||
}
|
||||
ratesMarshalled, err := json.Marshal(ticker.Rates)
|
||||
if err != nil {
|
||||
glog.Error("Error marshalling ticker rates: ", err)
|
||||
return err
|
||||
}
|
||||
timeFormatted := ticker.Timestamp.UTC().Format(FiatRatesTimeFormat)
|
||||
err = d.db.PutCF(d.wo, d.cfh[cfFiatRates], []byte(timeFormatted), ratesMarshalled)
|
||||
if err != nil {
|
||||
glog.Error("Error storing ticker: ", err)
|
||||
return err
|
||||
}
|
||||
wb.PutCF(d.cfh[cfFiatRates], packTimestamp(&ticker.Timestamp), packCurrencyRatesTicker(ticker))
|
||||
return nil
|
||||
}
|
||||
|
||||
// FiatRatesFindTicker gets FiatRates data closest to the specified timestamp
|
||||
func (d *RocksDB) FiatRatesFindTicker(tickerTime *time.Time) (*CurrencyRatesTicker, error) {
|
||||
ticker := &CurrencyRatesTicker{}
|
||||
func getTickerFromIterator(it *gorocksdb.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 vsCurrency != "" {
|
||||
if ticker.Rates == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if _, found := ticker.Rates[vsCurrency]; !found {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
if token != "" {
|
||||
if ticker.TokenRates == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if _, found := ticker.TokenRates[token]; !found {
|
||||
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 && lastTickerInDB != nil {
|
||||
if 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() {
|
||||
timeObj, err := time.Parse(FiatRatesTimeFormat, string(it.Key().Data()))
|
||||
ticker, err := getTickerFromIterator(it, vsCurrency, token)
|
||||
if err != nil {
|
||||
glog.Error("FiatRatesFindTicker time parse error: ", err)
|
||||
glog.Error("FiatRatesFindTicker error: ", err)
|
||||
return nil, err
|
||||
}
|
||||
timeObj = timeObj.UTC()
|
||||
ticker.Timestamp = &timeObj
|
||||
err = json.Unmarshal(it.Value().Data(), &ticker.Rates)
|
||||
if err != nil {
|
||||
glog.Error("FiatRatesFindTicker error unpacking rates: ", err)
|
||||
return nil, err
|
||||
if ticker != nil {
|
||||
return ticker, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
if err := it.Err(); err != nil {
|
||||
glog.Error("FiatRatesFindTicker Iterator error: ", err)
|
||||
return nil, err
|
||||
}
|
||||
if !it.Valid() {
|
||||
return nil, nil // ticker not found
|
||||
}
|
||||
return ticker, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FiatRatesFindLastTicker gets the last FiatRates record
|
||||
func (d *RocksDB) FiatRatesFindLastTicker() (*CurrencyRatesTicker, error) {
|
||||
ticker := &CurrencyRatesTicker{}
|
||||
// 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.Next() {
|
||||
timeObj, err := time.Parse(FiatRatesTimeFormat, string(it.Key().Data()))
|
||||
for it.SeekToLast(); it.Valid(); it.Prev() {
|
||||
ticker, err := getTickerFromIterator(it, vsCurrency, token)
|
||||
if err != nil {
|
||||
glog.Error("FiatRatesFindTicker time parse error: ", err)
|
||||
glog.Error("FiatRatesFindLastTicker error: ", err)
|
||||
return nil, err
|
||||
}
|
||||
timeObj = timeObj.UTC()
|
||||
ticker.Timestamp = &timeObj
|
||||
err = json.Unmarshal(it.Value().Data(), &ticker.Rates)
|
||||
if err != nil {
|
||||
glog.Error("FiatRatesFindTicker error unpacking rates: ", 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
|
||||
}
|
||||
break
|
||||
}
|
||||
if err := it.Err(); err != nil {
|
||||
glog.Error("FiatRatesFindLastTicker Iterator error: ", err)
|
||||
return ticker, err
|
||||
}
|
||||
if !it.Valid() {
|
||||
return nil, nil // ticker not found
|
||||
}
|
||||
return ticker, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FiatRatesGetCurrentTicker return current ticker
|
||||
func (d *RocksDB) FiatRatesGetCurrentTicker(tickerTime *time.Time, token string) (*CurrencyRatesTicker, error) {
|
||||
tickersMux.Lock()
|
||||
defer tickersMux.Unlock()
|
||||
return currentTicker, nil
|
||||
}
|
||||
|
||||
// FiatRatesCurrentTicker return current ticker
|
||||
func (d *RocksDB) FiatRatesSetCurrentTicker(t *CurrencyRatesTicker) {
|
||||
tickersMux.Lock()
|
||||
defer tickersMux.Unlock()
|
||||
currentTicker = t
|
||||
}
|
||||
|
||||
146
db/fiat_test.go
146
db/fiat_test.go
@ -3,8 +3,11 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/flier/gorocksdb"
|
||||
)
|
||||
|
||||
func TestRocksTickers(t *testing.T) {
|
||||
@ -30,34 +33,63 @@ func TestRocksTickers(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test storing & finding tickers
|
||||
key, _ := time.Parse(FiatRatesTimeFormat, "20190627000000")
|
||||
pastKey, _ := time.Parse(FiatRatesTimeFormat, "20190627000000")
|
||||
futureKey, _ := time.Parse(FiatRatesTimeFormat, "20190630000000")
|
||||
|
||||
ts1, _ := time.Parse(FiatRatesTimeFormat, "20190628000000")
|
||||
ticker1 := &CurrencyRatesTicker{
|
||||
Timestamp: &ts1,
|
||||
Rates: map[string]float64{
|
||||
Timestamp: ts1,
|
||||
Rates: map[string]float32{
|
||||
"usd": 20000,
|
||||
"eur": 18000,
|
||||
},
|
||||
TokenRates: map[string]float32{
|
||||
"0x6B175474E89094C44Da98b954EedeAC495271d0F": 17.2,
|
||||
},
|
||||
}
|
||||
|
||||
ts2, _ := time.Parse(FiatRatesTimeFormat, "20190629000000")
|
||||
ticker2 := &CurrencyRatesTicker{
|
||||
Timestamp: &ts2,
|
||||
Rates: map[string]float64{
|
||||
Timestamp: ts2,
|
||||
Rates: map[string]float32{
|
||||
"usd": 30000,
|
||||
},
|
||||
TokenRates: map[string]float32{
|
||||
"0x82dF128257A7d7556262E1AB7F1f639d9775B85E": 13.1,
|
||||
"0x6B175474E89094C44Da98b954EedeAC495271d0F": 17.5,
|
||||
},
|
||||
}
|
||||
err := d.FiatRatesStoreTicker(ticker1)
|
||||
|
||||
wb := gorocksdb.NewWriteBatch()
|
||||
defer wb.Destroy()
|
||||
err := d.FiatRatesStoreTicker(wb, ticker1)
|
||||
if err != nil {
|
||||
t.Errorf("Error storing ticker! %v", err)
|
||||
}
|
||||
d.FiatRatesStoreTicker(ticker2)
|
||||
err = d.FiatRatesStoreTicker(wb, ticker2)
|
||||
if err != nil {
|
||||
t.Errorf("Error storing ticker! %v", err)
|
||||
}
|
||||
err = d.WriteBatch(wb)
|
||||
if err != nil {
|
||||
t.Errorf("Error storing ticker! %v", err)
|
||||
}
|
||||
|
||||
ticker, err := d.FiatRatesFindTicker(&key) // should find the closest key (ticker1)
|
||||
// test FiatRatesGetTicker with ticker that should be in DB
|
||||
t1, err := d.FiatRatesGetTicker(&ts1)
|
||||
if err != nil || t1 == nil {
|
||||
t.Fatalf("FiatRatesGetTicker t1 %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(t1, ticker1) {
|
||||
t.Fatalf("FiatRatesGetTicker(t1) = %v, want %v", *t1, *ticker1)
|
||||
}
|
||||
// test FiatRatesGetTicker with ticker that is not in DB
|
||||
t2, err := d.FiatRatesGetTicker(&pastKey)
|
||||
if err != nil || t2 != nil {
|
||||
t.Fatalf("FiatRatesGetTicker t2 %v, %v", err, t2)
|
||||
}
|
||||
|
||||
ticker, err := d.FiatRatesFindTicker(&pastKey, "", "") // should find the closest key (ticker1)
|
||||
if err != nil {
|
||||
t.Errorf("TestRocksTickers err: %+v", err)
|
||||
} else if ticker == nil {
|
||||
@ -66,7 +98,7 @@ func TestRocksTickers(t *testing.T) {
|
||||
t.Errorf("Incorrect ticker found. Expected: %v, found: %+v", ticker1.Timestamp, ticker.Timestamp)
|
||||
}
|
||||
|
||||
ticker, err = d.FiatRatesFindLastTicker() // should find the last key (ticker2)
|
||||
ticker, err = d.FiatRatesFindLastTicker("", "") // should find the last key (ticker2)
|
||||
if err != nil {
|
||||
t.Errorf("TestRocksTickers err: %+v", err)
|
||||
} else if ticker == nil {
|
||||
@ -75,10 +107,104 @@ func TestRocksTickers(t *testing.T) {
|
||||
t.Errorf("Incorrect ticker found. Expected: %v, found: %+v", ticker1.Timestamp, ticker.Timestamp)
|
||||
}
|
||||
|
||||
ticker, err = d.FiatRatesFindTicker(&futureKey) // should not find anything
|
||||
ticker, err = d.FiatRatesFindTicker(&futureKey, "", "") // should not find anything
|
||||
if err != nil {
|
||||
t.Errorf("TestRocksTickers err: %+v", err)
|
||||
} else if ticker != nil {
|
||||
t.Errorf("Ticker found, but the timestamp is older than the last ticker entry.")
|
||||
}
|
||||
|
||||
ticker, err = d.FiatRatesFindTicker(&pastKey, "", "0x6B175474E89094C44Da98b954EedeAC495271d0F") // should find the closest key (ticker1)
|
||||
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)
|
||||
}
|
||||
|
||||
ticker, err = d.FiatRatesFindTicker(&pastKey, "", "0x82dF128257A7d7556262E1AB7F1f639d9775B85E") // should find the last key (ticker2)
|
||||
if err != nil {
|
||||
t.Errorf("TestRocksTickers err: %+v", err)
|
||||
} else if ticker == nil {
|
||||
t.Errorf("Ticker not found")
|
||||
} else if ticker.Timestamp.Format(FiatRatesTimeFormat) != ticker2.Timestamp.Format(FiatRatesTimeFormat) {
|
||||
t.Errorf("Incorrect ticker found. Expected: %v, found: %+v", ticker2.Timestamp, ticker.Timestamp)
|
||||
}
|
||||
|
||||
ticker, err = d.FiatRatesFindLastTicker("eur", "") // should find the closest key (ticker1)
|
||||
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)
|
||||
}
|
||||
|
||||
ticker, err = d.FiatRatesFindLastTicker("usd", "") // should find the last key (ticker2)
|
||||
if err != nil {
|
||||
t.Errorf("TestRocksTickers err: %+v", err)
|
||||
} else if ticker == nil {
|
||||
t.Errorf("Ticker not found")
|
||||
} else if ticker.Timestamp.Format(FiatRatesTimeFormat) != ticker2.Timestamp.Format(FiatRatesTimeFormat) {
|
||||
t.Errorf("Incorrect ticker found. Expected: %v, found: %+v", ticker2.Timestamp, ticker.Timestamp)
|
||||
}
|
||||
|
||||
ticker, err = d.FiatRatesFindLastTicker("aud", "") // should not find any key
|
||||
if err != nil {
|
||||
t.Errorf("TestRocksTickers err: %+v", err)
|
||||
} else if ticker != nil {
|
||||
t.Errorf("Ticker %v found unexpectedly for aud vsCurrency", ticker)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_packUnpackCurrencyRatesTicker(t *testing.T) {
|
||||
type args struct {
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
data CurrencyRatesTicker
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
data: CurrencyRatesTicker{},
|
||||
},
|
||||
{
|
||||
name: "rates",
|
||||
data: CurrencyRatesTicker{
|
||||
Rates: map[string]float32{
|
||||
"usd": 2129.2341123,
|
||||
"eur": 1332.51234,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rates&tokenrates",
|
||||
data: CurrencyRatesTicker{
|
||||
Rates: map[string]float32{
|
||||
"usd": 322129.987654321,
|
||||
"eur": 291332.12345678,
|
||||
},
|
||||
TokenRates: map[string]float32{
|
||||
"0x82dF128257A7d7556262E1AB7F1f639d9775B85E": 0.4092341123,
|
||||
"0x6B175474E89094C44Da98b954EedeAC495271d0F": 12.32323232323232,
|
||||
"0xdAC17F958D2ee523a2206206994597C13D831ec7": 1332421341235.51234,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
packed := packCurrencyRatesTicker(&tt.data)
|
||||
got, err := unpackCurrencyRatesTicker(packed)
|
||||
if err != nil {
|
||||
t.Errorf("unpackCurrencyRatesTicker() error = %v", err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, &tt.data) {
|
||||
t.Errorf("unpackCurrencyRatesTicker() = %v, want %v", *got, tt.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,12 +2,15 @@ package fiat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/flier/gorocksdb"
|
||||
"github.com/golang/glog"
|
||||
"github.com/trezor/blockbook/db"
|
||||
)
|
||||
@ -16,121 +19,405 @@ import (
|
||||
type Coingecko struct {
|
||||
url string
|
||||
coin string
|
||||
platformIdentifier string
|
||||
platformVsCurrency string
|
||||
httpTimeoutSeconds time.Duration
|
||||
throttlingDelay time.Duration
|
||||
timeFormat string
|
||||
httpClient *http.Client
|
||||
db *db.RocksDB
|
||||
updatingTokens bool
|
||||
}
|
||||
|
||||
// simpleSupportedVSCurrencies https://api.coingecko.com/api/v3/simple/supported_vs_currencies
|
||||
type simpleSupportedVSCurrencies []string
|
||||
|
||||
type coinsListItem struct {
|
||||
ID string `json:"id"`
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"name"`
|
||||
Platforms map[string]string `json:"platforms"`
|
||||
}
|
||||
|
||||
// coinList https://api.coingecko.com/api/v3/coins/list
|
||||
type coinList []coinsListItem
|
||||
|
||||
type marketPoint [2]float32
|
||||
type marketChartPrices struct {
|
||||
Prices []marketPoint `json:"prices"`
|
||||
}
|
||||
|
||||
// NewCoinGeckoDownloader creates a coingecko structure that implements the RatesDownloaderInterface
|
||||
func NewCoinGeckoDownloader(url string, coin string, timeFormat string) RatesDownloaderInterface {
|
||||
func NewCoinGeckoDownloader(db *db.RocksDB, url string, coin string, platformIdentifier string, platformVsCurrency string, timeFormat string, throttlingDelayMs int) RatesDownloaderInterface {
|
||||
httpTimeoutSeconds := 15 * time.Second
|
||||
return &Coingecko{
|
||||
url: url,
|
||||
coin: coin,
|
||||
httpTimeoutSeconds: 15 * time.Second,
|
||||
platformIdentifier: platformIdentifier,
|
||||
platformVsCurrency: platformVsCurrency,
|
||||
httpTimeoutSeconds: httpTimeoutSeconds,
|
||||
timeFormat: timeFormat,
|
||||
httpClient: &http.Client{
|
||||
Timeout: httpTimeoutSeconds,
|
||||
},
|
||||
db: db,
|
||||
throttlingDelay: time.Duration(throttlingDelayMs) * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
// makeRequest retrieves the response from Coingecko API at the specified date.
|
||||
// If timestamp is nil, it fetches the latest market data available.
|
||||
func (cg *Coingecko) makeRequest(timestamp *time.Time) ([]byte, error) {
|
||||
requestURL := cg.url + "/coins/" + cg.coin
|
||||
if timestamp != nil {
|
||||
requestURL += "/history"
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", requestURL, nil)
|
||||
if err != nil {
|
||||
glog.Errorf("Error creating a new request for %v: %v", requestURL, err)
|
||||
return nil, err
|
||||
}
|
||||
req.Close = true
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Add query parameters
|
||||
q := req.URL.Query()
|
||||
|
||||
// Add a unix timestamp to query parameters to get uncached responses
|
||||
currentTimestamp := strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
|
||||
q.Add("current_timestamp", currentTimestamp)
|
||||
|
||||
if timestamp == nil {
|
||||
q.Add("market_data", "true")
|
||||
q.Add("localization", "false")
|
||||
q.Add("tickers", "false")
|
||||
q.Add("community_data", "false")
|
||||
q.Add("developer_data", "false")
|
||||
} else {
|
||||
timestampFormatted := timestamp.Format(cg.timeFormat)
|
||||
q.Add("date", timestampFormatted)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: cg.httpTimeoutSeconds,
|
||||
}
|
||||
// doReq HTTP client
|
||||
func doReq(req *http.Request, client *http.Client) ([]byte, error) {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("Invalid response status: " + string(resp.Status))
|
||||
}
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bodyBytes, nil
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("%s", body)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// GetData gets fiat rates from API at the specified date and returns a CurrencyRatesTicker
|
||||
// If timestamp is nil, it will download the current fiat rates.
|
||||
func (cg *Coingecko) getTicker(timestamp *time.Time) (*db.CurrencyRatesTicker, error) {
|
||||
dataTimestamp := timestamp
|
||||
if timestamp == nil {
|
||||
timeNow := time.Now()
|
||||
dataTimestamp = &timeNow
|
||||
// makeReq HTTP request helper - will retry the call after 1 minute on error
|
||||
func (cg *Coingecko) makeReq(url string) ([]byte, error) {
|
||||
for {
|
||||
// glog.Infof("Coingecko makeReq %v", url)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := doReq(req, cg.httpClient)
|
||||
if err == nil {
|
||||
return resp, err
|
||||
}
|
||||
if err.Error() != "error code: 1015" {
|
||||
glog.Errorf("Coingecko makeReq %v error %v", url, err)
|
||||
return nil, err
|
||||
}
|
||||
// if there is a throttling error, wait 70 seconds and retry
|
||||
glog.Errorf("Coingecko makeReq %v error %v, will retry in 70 seconds", url, err)
|
||||
time.Sleep(70 * time.Second)
|
||||
}
|
||||
dataTimestampUTC := dataTimestamp.UTC()
|
||||
ticker := &db.CurrencyRatesTicker{Timestamp: &dataTimestampUTC}
|
||||
bodyBytes, err := cg.makeRequest(timestamp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type FiatRatesResponse struct {
|
||||
MarketData struct {
|
||||
Prices map[string]float64 `json:"current_price"`
|
||||
} `json:"market_data"`
|
||||
}
|
||||
|
||||
var data FiatRatesResponse
|
||||
err = json.Unmarshal(bodyBytes, &data)
|
||||
if err != nil {
|
||||
glog.Errorf("Error parsing FiatRates response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
ticker.Rates = data.MarketData.Prices
|
||||
return ticker, nil
|
||||
}
|
||||
|
||||
// MarketDataExists checks if there's data available for the specific timestamp.
|
||||
func (cg *Coingecko) marketDataExists(timestamp *time.Time) (bool, error) {
|
||||
resp, err := cg.makeRequest(timestamp)
|
||||
// SimpleSupportedVSCurrencies /simple/supported_vs_currencies
|
||||
func (cg *Coingecko) simpleSupportedVSCurrencies() (simpleSupportedVSCurrencies, error) {
|
||||
url := cg.url + "/simple/supported_vs_currencies"
|
||||
resp, err := cg.makeReq(url)
|
||||
if err != nil {
|
||||
glog.Error("Error getting market data: ", err)
|
||||
return false, err
|
||||
return nil, err
|
||||
}
|
||||
type FiatRatesResponse struct {
|
||||
MarketData struct {
|
||||
Prices map[string]interface{} `json:"current_price"`
|
||||
} `json:"market_data"`
|
||||
}
|
||||
var data FiatRatesResponse
|
||||
var data simpleSupportedVSCurrencies
|
||||
err = json.Unmarshal(resp, &data)
|
||||
if err != nil {
|
||||
glog.Errorf("Error parsing Coingecko response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// SimplePrice /simple/price Multiple ID and Currency (ids, vs_currencies)
|
||||
func (cg *Coingecko) simplePrice(ids []string, vsCurrencies []string) (*map[string]map[string]float32, error) {
|
||||
params := url.Values{}
|
||||
idsParam := strings.Join(ids, ",")
|
||||
vsCurrenciesParam := strings.Join(vsCurrencies, ",")
|
||||
|
||||
params.Add("ids", idsParam)
|
||||
params.Add("vs_currencies", vsCurrenciesParam)
|
||||
|
||||
url := fmt.Sprintf("%s/simple/price?%s", cg.url, params.Encode())
|
||||
resp, err := cg.makeReq(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := make(map[string]map[string]float32)
|
||||
err = json.Unmarshal(resp, &t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// CoinsList /coins/list
|
||||
func (cg *Coingecko) coinsList() (coinList, error) {
|
||||
params := url.Values{}
|
||||
platform := "false"
|
||||
if cg.platformIdentifier != "" {
|
||||
platform = "true"
|
||||
}
|
||||
params.Add("include_platform", platform)
|
||||
url := fmt.Sprintf("%s/coins/list?%s", cg.url, params.Encode())
|
||||
resp, err := cg.makeReq(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data coinList
|
||||
err = json.Unmarshal(resp, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if len(id) == 0 || len(vs_currency) == 0 || len(days) == 0 {
|
||||
return nil, fmt.Errorf("id, vs_currency, and days is required")
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("interval", "daily")
|
||||
params.Add("vs_currency", vs_currency)
|
||||
params.Add("days", days)
|
||||
|
||||
url := fmt.Sprintf("%s/coins/%s/market_chart?%s", cg.url, id, params.Encode())
|
||||
resp, err := cg.makeReq(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := marketChartPrices{}
|
||||
err = json.Unmarshal(resp, &m)
|
||||
if err != nil {
|
||||
return &m, err
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
var vsCurrencies []string
|
||||
var platformIds []string
|
||||
var platformIdsToTokens map[string]string
|
||||
|
||||
func (cg *Coingecko) platformIds() error {
|
||||
if cg.platformIdentifier == "" {
|
||||
return nil
|
||||
}
|
||||
cl, err := cg.coinsList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idsMap := make(map[string]string, 64)
|
||||
ids := make([]string, 0, 64)
|
||||
for i := range cl {
|
||||
id, found := cl[i].Platforms[cg.platformIdentifier]
|
||||
if found && id != "" {
|
||||
idsMap[cl[i].ID] = id
|
||||
ids = append(ids, cl[i].ID)
|
||||
}
|
||||
}
|
||||
platformIds = ids
|
||||
platformIdsToTokens = idsMap
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cg *Coingecko) CurrentTickers() (*db.CurrencyRatesTicker, error) {
|
||||
var newTickers = db.CurrencyRatesTicker{}
|
||||
|
||||
if vsCurrencies == nil {
|
||||
vs, err := cg.simpleSupportedVSCurrencies()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vsCurrencies = vs
|
||||
}
|
||||
prices, err := cg.simplePrice([]string{cg.coin}, vsCurrencies)
|
||||
if err != nil || prices == nil {
|
||||
return nil, err
|
||||
}
|
||||
newTickers.Rates = make(map[string]float32, len((*prices)[cg.coin]))
|
||||
for t, v := range (*prices)[cg.coin] {
|
||||
newTickers.Rates[t] = v
|
||||
}
|
||||
|
||||
if cg.platformIdentifier != "" && cg.platformVsCurrency != "" {
|
||||
if platformIdsToTokens == nil {
|
||||
err = cg.platformIds()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
newTickers.TokenRates = make(map[string]float32)
|
||||
const platformIdsGroup = 200
|
||||
for from := 0; from < len(platformIds); from += platformIdsGroup {
|
||||
to := from + platformIdsGroup
|
||||
if to > len(platformIds) {
|
||||
to = len(platformIds)
|
||||
}
|
||||
tokenPrices, err := cg.simplePrice(platformIds[from:to], []string{cg.platformVsCurrency})
|
||||
if err != nil || tokenPrices == nil {
|
||||
return nil, err
|
||||
}
|
||||
for id, v := range *tokenPrices {
|
||||
t, found := platformIdsToTokens[id]
|
||||
if found {
|
||||
newTickers.TokenRates[t] = v[cg.platformVsCurrency]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
newTickers.Timestamp = time.Now().UTC()
|
||||
return &newTickers, nil
|
||||
}
|
||||
|
||||
func (cg *Coingecko) getHistoricalTicker(tickersToUpdate map[uint]*db.CurrencyRatesTicker, coinId string, vsCurrency string, token string) (bool, error) {
|
||||
lastTicker, err := cg.db.FiatRatesFindLastTicker(vsCurrency, token)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return len(data.MarketData.Prices) != 0, nil
|
||||
var days string
|
||||
if lastTicker == nil {
|
||||
days = "max"
|
||||
} else {
|
||||
diff := time.Since(lastTicker.Timestamp)
|
||||
d := int(diff / (24 * 3600 * 1000000000))
|
||||
if d == 0 { // nothing to do, the last ticker exist
|
||||
return false, nil
|
||||
}
|
||||
days = strconv.Itoa(d)
|
||||
}
|
||||
mc, err := cg.coinMarketChart(coinId, vsCurrency, days)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
warningLogged := false
|
||||
for _, p := range mc.Prices {
|
||||
var timestamp uint
|
||||
if p[0] > 100000000000 {
|
||||
// convert timestamp from milliseconds to seconds
|
||||
timestamp = uint(p[0] / 1000)
|
||||
} else {
|
||||
timestamp = uint(p[0])
|
||||
}
|
||||
rate := p[1]
|
||||
if timestamp%(24*3600) == 0 && timestamp != 0 && rate != 0 { // process only tickers for the whole day with non 0 value
|
||||
var found bool
|
||||
var ticker *db.CurrencyRatesTicker
|
||||
if ticker, found = tickersToUpdate[timestamp]; !found {
|
||||
u := time.Unix(int64(timestamp), 0).UTC()
|
||||
ticker, err = cg.db.FiatRatesGetTicker(&u)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if ticker == nil {
|
||||
if token != "" { // if the base currency is not found in DB, do not create ticker for the token
|
||||
if !warningLogged {
|
||||
glog.Warningf("No base currency ticker for date %v for token %s", u, token)
|
||||
warningLogged = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
ticker = &db.CurrencyRatesTicker{
|
||||
Timestamp: u,
|
||||
Rates: make(map[string]float32),
|
||||
}
|
||||
}
|
||||
tickersToUpdate[timestamp] = ticker
|
||||
}
|
||||
if token == "" {
|
||||
ticker.Rates[vsCurrency] = rate
|
||||
} else {
|
||||
if ticker.TokenRates == nil {
|
||||
ticker.TokenRates = make(map[string]float32)
|
||||
}
|
||||
ticker.TokenRates[token] = rate
|
||||
}
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (cg *Coingecko) storeTickers(tickersToUpdate map[uint]*db.CurrencyRatesTicker) error {
|
||||
if len(tickersToUpdate) > 0 {
|
||||
wb := gorocksdb.NewWriteBatch()
|
||||
defer wb.Destroy()
|
||||
for _, v := range tickersToUpdate {
|
||||
if err := cg.db.FiatRatesStoreTicker(wb, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := cg.db.WriteBatch(wb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateHistoricalTickers gets historical tickers for the main crypto currency
|
||||
func (cg *Coingecko) UpdateHistoricalTickers() error {
|
||||
tickersToUpdate := make(map[uint]*db.CurrencyRatesTicker)
|
||||
|
||||
// reload vs_currencies
|
||||
vs, err := cg.simpleSupportedVSCurrencies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vsCurrencies = vs
|
||||
|
||||
for _, currency := range vsCurrencies {
|
||||
// get historical rates for each currency
|
||||
var err error
|
||||
var req bool
|
||||
if req, err = cg.getHistoricalTicker(tickersToUpdate, cg.coin, currency, ""); err != nil {
|
||||
// report error and continue, Coingecko may return error like "Could not find coin with the given id"
|
||||
// the rates will be updated next run
|
||||
glog.Errorf("getHistoricalTicker %s-%s %v", cg.coin, currency, err)
|
||||
}
|
||||
if req {
|
||||
time.Sleep(cg.throttlingDelay)
|
||||
}
|
||||
}
|
||||
|
||||
return cg.storeTickers(tickersToUpdate)
|
||||
}
|
||||
|
||||
// UpdateHistoricalTokenTickers gets historical tickers for the tokens
|
||||
func (cg *Coingecko) UpdateHistoricalTokenTickers() error {
|
||||
if cg.updatingTokens {
|
||||
return nil
|
||||
}
|
||||
cg.updatingTokens = true
|
||||
defer func() { cg.updatingTokens = false }()
|
||||
tickersToUpdate := make(map[uint]*db.CurrencyRatesTicker)
|
||||
|
||||
if cg.platformIdentifier != "" && cg.platformVsCurrency != "" {
|
||||
// reload platform ids
|
||||
if err := cg.platformIds(); err != nil {
|
||||
return err
|
||||
}
|
||||
glog.Infof("Coingecko returned %d %s tokens ", len(platformIds), cg.coin)
|
||||
count := 0
|
||||
// get token historical rates
|
||||
for tokenId, token := range platformIdsToTokens {
|
||||
var err error
|
||||
var req bool
|
||||
if req, err = cg.getHistoricalTicker(tickersToUpdate, tokenId, cg.platformVsCurrency, token); err != nil {
|
||||
// report error and continue, Coingecko may return error like "Could not find coin with the given id"
|
||||
// the rates will be updated next run
|
||||
glog.Errorf("getHistoricalTicker %s-%s %v", tokenId, cg.platformVsCurrency, err)
|
||||
}
|
||||
count++
|
||||
if count%100 == 0 {
|
||||
err := cg.storeTickers(tickersToUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tickersToUpdate = make(map[uint]*db.CurrencyRatesTicker)
|
||||
glog.Infof("Coingecko updated %d of %d token tickers", count, len(platformIds))
|
||||
}
|
||||
if req {
|
||||
// long delay next request to avoid throttling
|
||||
time.Sleep(cg.throttlingDelay * 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cg.storeTickers(tickersToUpdate)
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
@ -16,28 +16,29 @@ type OnNewFiatRatesTicker func(ticker *db.CurrencyRatesTicker)
|
||||
|
||||
// RatesDownloaderInterface provides method signatures for specific fiat rates downloaders
|
||||
type RatesDownloaderInterface interface {
|
||||
getTicker(timestamp *time.Time) (*db.CurrencyRatesTicker, error)
|
||||
marketDataExists(timestamp *time.Time) (bool, error)
|
||||
CurrentTickers() (*db.CurrencyRatesTicker, error)
|
||||
UpdateHistoricalTickers() error
|
||||
UpdateHistoricalTokenTickers() error
|
||||
}
|
||||
|
||||
// RatesDownloader stores FiatRates API parameters
|
||||
type RatesDownloader struct {
|
||||
periodSeconds time.Duration
|
||||
periodSeconds int64
|
||||
db *db.RocksDB
|
||||
startTime *time.Time // a starting timestamp for tests to be deterministic (time.Now() for production)
|
||||
timeFormat string
|
||||
callbackOnNewTicker OnNewFiatRatesTicker
|
||||
downloader RatesDownloaderInterface
|
||||
}
|
||||
|
||||
// NewFiatRatesDownloader initializes the downloader for FiatRates API.
|
||||
// If the startTime is nil, the downloader will start from the beginning.
|
||||
func NewFiatRatesDownloader(db *db.RocksDB, apiType string, params string, startTime *time.Time, callback OnNewFiatRatesTicker) (*RatesDownloader, error) {
|
||||
func NewFiatRatesDownloader(db *db.RocksDB, apiType string, params string, callback OnNewFiatRatesTicker) (*RatesDownloader, error) {
|
||||
var rd = &RatesDownloader{}
|
||||
type fiatRatesParams struct {
|
||||
URL string `json:"url"`
|
||||
Coin string `json:"coin"`
|
||||
PeriodSeconds int `json:"periodSeconds"`
|
||||
URL string `json:"url"`
|
||||
Coin string `json:"coin"`
|
||||
PlatformIdentifier string `json:"platformIdentifier"`
|
||||
PlatformVsCurrency string `json:"platformVsCurrency"`
|
||||
PeriodSeconds int64 `json:"periodSeconds"`
|
||||
}
|
||||
rdParams := &fiatRatesParams{}
|
||||
err := json.Unmarshal([]byte(params), &rdParams)
|
||||
@ -47,168 +48,62 @@ func NewFiatRatesDownloader(db *db.RocksDB, apiType string, params string, start
|
||||
if rdParams.URL == "" || rdParams.PeriodSeconds == 0 {
|
||||
return nil, errors.New("Missing parameters")
|
||||
}
|
||||
rd.timeFormat = "02-01-2006" // Layout string for FiatRates date formatting (DD-MM-YYYY)
|
||||
rd.periodSeconds = time.Duration(rdParams.PeriodSeconds) * time.Second // Time period for syncing the latest market data
|
||||
rd.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
|
||||
if rd.periodSeconds < 60 { // minimum is one minute
|
||||
rd.periodSeconds = 60
|
||||
}
|
||||
rd.db = db
|
||||
rd.callbackOnNewTicker = callback
|
||||
if startTime == nil {
|
||||
timeNow := time.Now().UTC()
|
||||
rd.startTime = &timeNow
|
||||
} else {
|
||||
rd.startTime = startTime // If startTime is nil, time.Now() will be used
|
||||
}
|
||||
if apiType == "coingecko" {
|
||||
rd.downloader = NewCoinGeckoDownloader(rdParams.URL, rdParams.Coin, rd.timeFormat)
|
||||
throttlingDelayMs := 50
|
||||
if callback == nil {
|
||||
// a small hack - in tests the callback is not used, therefore there is no delay slowing the test
|
||||
throttlingDelayMs = 0
|
||||
}
|
||||
rd.downloader = NewCoinGeckoDownloader(db, rdParams.URL, rdParams.Coin, rdParams.PlatformIdentifier, rdParams.PlatformVsCurrency, rd.timeFormat, throttlingDelayMs)
|
||||
} else {
|
||||
return nil, fmt.Errorf("NewFiatRatesDownloader: incorrect API type %q", apiType)
|
||||
}
|
||||
return rd, nil
|
||||
}
|
||||
|
||||
// Run starts the FiatRates downloader. If there are tickers available, it continues from the last record.
|
||||
// If there are no tickers, it finds the earliest market data available on API and downloads historical data.
|
||||
// When historical data is downloaded, it continues to fetch the latest ticker prices.
|
||||
// Run periodically downloads current (every 15 minutes) and historical (once a day) tickers
|
||||
func (rd *RatesDownloader) Run() error {
|
||||
var timestamp *time.Time
|
||||
var lastHistoricalTickers time.Time
|
||||
|
||||
// Check if there are any tickers stored in database
|
||||
glog.Infof("Finding last available ticker...")
|
||||
ticker, err := rd.db.FiatRatesFindLastTicker()
|
||||
if err != nil {
|
||||
glog.Errorf("RatesDownloader FindTicker error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if ticker == nil {
|
||||
// If no tickers found, start downloading from the beginning
|
||||
glog.Infof("No tickers found! Looking up the earliest market data available on API and downloading from there.")
|
||||
timestamp, err = rd.findEarliestMarketData()
|
||||
if err != nil {
|
||||
glog.Errorf("Error looking up earliest market data: %v", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// If found, continue downloading data from the next day of the last available record
|
||||
glog.Infof("Last available ticker: %v", ticker.Timestamp)
|
||||
timestamp = ticker.Timestamp
|
||||
}
|
||||
err = rd.syncHistorical(timestamp)
|
||||
if err != nil {
|
||||
glog.Errorf("RatesDownloader syncHistorical error: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := rd.syncLatest(); err != nil {
|
||||
glog.Errorf("RatesDownloader syncLatest error: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindEarliestMarketData uses binary search to find the oldest market data available on API.
|
||||
func (rd *RatesDownloader) findEarliestMarketData() (*time.Time, error) {
|
||||
minDateString := "03-01-2009"
|
||||
minDate, err := time.Parse(rd.timeFormat, minDateString)
|
||||
if err != nil {
|
||||
glog.Error("Error parsing date: ", err)
|
||||
return nil, err
|
||||
}
|
||||
maxDate := rd.startTime.Add(time.Duration(-24) * time.Hour) // today's historical tickers may not be ready yet, so set to yesterday
|
||||
currentDate := maxDate
|
||||
for {
|
||||
var dataExists bool = false
|
||||
for {
|
||||
dataExists, err = rd.downloader.marketDataExists(¤tDate)
|
||||
if err != nil {
|
||||
glog.Errorf("Error checking if market data exists for date %v. Error: %v. Retrying in %v seconds.", currentDate, err, rd.periodSeconds)
|
||||
timer := time.NewTimer(rd.periodSeconds)
|
||||
<-timer.C
|
||||
}
|
||||
break
|
||||
}
|
||||
dateDiff := currentDate.Sub(minDate)
|
||||
if dataExists {
|
||||
if dateDiff < time.Hour*24 {
|
||||
maxDate := time.Date(maxDate.Year(), maxDate.Month(), maxDate.Day(), 0, 0, 0, 0, maxDate.Location()) // truncate time to day
|
||||
return &maxDate, nil
|
||||
}
|
||||
maxDate = currentDate
|
||||
currentDate = currentDate.Add(-1 * dateDiff / 2)
|
||||
tickers, err := rd.downloader.CurrentTickers()
|
||||
if err != nil && tickers != nil {
|
||||
glog.Error("FiatRatesDownloader: CurrentTickers error ", err)
|
||||
} else {
|
||||
minDate = currentDate
|
||||
currentDate = currentDate.Add(maxDate.Sub(currentDate) / 2)
|
||||
rd.db.FiatRatesSetCurrentTicker(tickers)
|
||||
glog.Info("FiatRatesDownloader: CurrentTickers updated")
|
||||
}
|
||||
if time.Now().UTC().YearDay() != lastHistoricalTickers.YearDay() || time.Now().UTC().Year() != lastHistoricalTickers.Year() {
|
||||
err = rd.downloader.UpdateHistoricalTickers()
|
||||
if err != nil {
|
||||
glog.Error("FiatRatesDownloader: UpdateHistoricalTickers error ", err)
|
||||
} else {
|
||||
lastHistoricalTickers = time.Now().UTC()
|
||||
glog.Info("FiatRatesDownloader: UpdateHistoricalTickers finished")
|
||||
}
|
||||
// UpdateHistoricalTokenTickers in a goroutine, it can take quite some time as there may be many tokens
|
||||
go func() {
|
||||
err := rd.downloader.UpdateHistoricalTokenTickers()
|
||||
if err != nil {
|
||||
glog.Error("FiatRatesDownloader: UpdateHistoricalTokenTickers error ", err)
|
||||
} else {
|
||||
lastHistoricalTickers = time.Now().UTC()
|
||||
glog.Info("FiatRatesDownloader: UpdateHistoricalTokenTickers finished")
|
||||
}
|
||||
}()
|
||||
}
|
||||
// next run on the
|
||||
now := time.Now().Unix()
|
||||
next := now + rd.periodSeconds
|
||||
next -= next % rd.periodSeconds
|
||||
next += int64(rand.Intn(12))
|
||||
time.Sleep(time.Duration(next-now) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// syncLatest downloads the latest FiatRates data every rd.PeriodSeconds
|
||||
func (rd *RatesDownloader) syncLatest() error {
|
||||
timer := time.NewTimer(rd.periodSeconds)
|
||||
var lastTickerRates map[string]float64
|
||||
sameTickerCounter := 0
|
||||
for {
|
||||
ticker, err := rd.downloader.getTicker(nil)
|
||||
if err != nil {
|
||||
// Do not exit on GET error, log it, wait and try again
|
||||
glog.Errorf("syncLatest GetData error: %v", err)
|
||||
<-timer.C
|
||||
timer.Reset(rd.periodSeconds)
|
||||
continue
|
||||
}
|
||||
|
||||
if sameTickerCounter < 5 && reflect.DeepEqual(ticker.Rates, lastTickerRates) {
|
||||
// If rates are the same as previous, do not store them
|
||||
glog.Infof("syncLatest: ticker rates for %v are the same as previous, skipping...", ticker.Timestamp)
|
||||
<-timer.C
|
||||
timer.Reset(rd.periodSeconds)
|
||||
sameTickerCounter++
|
||||
continue
|
||||
}
|
||||
lastTickerRates = ticker.Rates
|
||||
sameTickerCounter = 0
|
||||
|
||||
glog.Infof("syncLatest: storing ticker for %v", ticker.Timestamp)
|
||||
err = rd.db.FiatRatesStoreTicker(ticker)
|
||||
if err != nil {
|
||||
// If there's an error storing ticker (like missing rates), log it, wait and try again
|
||||
glog.Errorf("syncLatest StoreTicker error: %v", err)
|
||||
} else if rd.callbackOnNewTicker != nil {
|
||||
rd.callbackOnNewTicker(ticker)
|
||||
}
|
||||
<-timer.C
|
||||
timer.Reset(rd.periodSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
// syncHistorical downloads all the historical data since the specified timestamp till today,
|
||||
// then continues to download the latest rates
|
||||
func (rd *RatesDownloader) syncHistorical(timestamp *time.Time) error {
|
||||
period := time.Duration(1) * time.Second
|
||||
timer := time.NewTimer(period)
|
||||
for {
|
||||
if rd.startTime.Sub(*timestamp) < time.Duration(time.Hour*24) {
|
||||
break
|
||||
}
|
||||
|
||||
ticker, err := rd.downloader.getTicker(timestamp)
|
||||
if err != nil {
|
||||
// Do not exit on GET error, log it, wait and try again
|
||||
glog.Errorf("syncHistorical GetData error: %v", err)
|
||||
<-timer.C
|
||||
timer.Reset(rd.periodSeconds)
|
||||
continue
|
||||
}
|
||||
|
||||
glog.Infof("syncHistorical: storing ticker for %v", ticker.Timestamp)
|
||||
err = rd.db.FiatRatesStoreTicker(ticker)
|
||||
if err != nil {
|
||||
// If there's an error storing ticker (like missing rates), log it and continue to the next day
|
||||
glog.Errorf("syncHistorical error storing ticker for %v: %v", timestamp, err)
|
||||
}
|
||||
|
||||
*timestamp = timestamp.Add(time.Hour * 24) // go to the next day
|
||||
|
||||
<-timer.C
|
||||
timer.Reset(period)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -66,13 +67,9 @@ func bitcoinTestnetParser() *btc.BitcoinParser {
|
||||
}
|
||||
|
||||
// getFiatRatesMockData reads a stub JSON response from a file and returns its content as string
|
||||
func getFiatRatesMockData(dateParam string) (string, error) {
|
||||
func getFiatRatesMockData(name string) (string, error) {
|
||||
var filename string
|
||||
if dateParam == "current" {
|
||||
filename = "fiat/mock_data/current.json"
|
||||
} else {
|
||||
filename = "fiat/mock_data/" + dateParam + ".json"
|
||||
}
|
||||
filename = "fiat/mock_data/" + name + ".json"
|
||||
mockFile, err := os.Open(filename)
|
||||
if err != nil {
|
||||
glog.Errorf("Cannot open file %v", filename)
|
||||
@ -98,27 +95,43 @@ func TestFiatRates(t *testing.T) {
|
||||
|
||||
if r.URL.Path == "/ping" {
|
||||
w.WriteHeader(200)
|
||||
} else if r.URL.Path == "/coins/bitcoin/history" {
|
||||
date := r.URL.Query()["date"][0]
|
||||
mockData, err = getFiatRatesMockData(date) // get stub rates by date
|
||||
} else if r.URL.Path == "/coins/bitcoin" {
|
||||
mockData, err = getFiatRatesMockData("current") // get "latest" stub rates
|
||||
} else if r.URL.Path == "/coins/list" {
|
||||
mockData, err = getFiatRatesMockData("coinlist")
|
||||
} else if r.URL.Path == "/simple/supported_vs_currencies" {
|
||||
mockData, err = getFiatRatesMockData("vs_currencies")
|
||||
} else if r.URL.Path == "/simple/price" {
|
||||
if r.URL.Query().Get("ids") == "ethereum" {
|
||||
mockData, err = getFiatRatesMockData("simpleprice_base")
|
||||
} else {
|
||||
mockData, err = getFiatRatesMockData("simpleprice_tokens")
|
||||
}
|
||||
} else if r.URL.Path == "/coins/ethereum/market_chart" {
|
||||
vsCurrency := r.URL.Query().Get("vs_currency")
|
||||
if vsCurrency == "usd" {
|
||||
days := r.URL.Query().Get("days")
|
||||
if days == "max" {
|
||||
mockData, err = getFiatRatesMockData("market_chart_eth_usd_max")
|
||||
} else {
|
||||
mockData, err = getFiatRatesMockData("market_chart_eth_usd_1")
|
||||
}
|
||||
} else {
|
||||
mockData, err = getFiatRatesMockData("market_chart_eth_other")
|
||||
}
|
||||
} else if r.URL.Path == "/coins/vendit/market_chart" || r.URL.Path == "/coins/ethereum-cash-token/market_chart" {
|
||||
mockData, err = getFiatRatesMockData("market_chart_token_other")
|
||||
} else {
|
||||
t.Errorf("Unknown URL path: %v", r.URL.Path)
|
||||
t.Fatalf("Unknown URL path: %v", r.URL.Path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error loading stub data: %v", err)
|
||||
t.Fatalf("Error loading stub data: %v", err)
|
||||
}
|
||||
fmt.Fprintln(w, mockData)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
// real CoinGecko API
|
||||
//configJSON := `{"fiat_rates": "coingecko", "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}"}`
|
||||
|
||||
// mocked CoinGecko API
|
||||
configJSON := `{"fiat_rates": "coingecko", "fiat_rates_params": "{\"url\": \"` + mockServer.URL + `\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}"}`
|
||||
configJSON := `{"fiat_rates": "coingecko", "fiat_rates_params": "{\"url\": \"` + mockServer.URL + `\", \"coin\": \"ethereum\",\"platformIdentifier\":\"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 60}"}`
|
||||
|
||||
type fiatRatesConfig struct {
|
||||
FiatRates string `json:"fiat_rates"`
|
||||
@ -128,49 +141,157 @@ func TestFiatRates(t *testing.T) {
|
||||
var config fiatRatesConfig
|
||||
err := json.Unmarshal([]byte(configJSON), &config)
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing config: %v", err)
|
||||
t.Fatalf("Error parsing config: %v", err)
|
||||
}
|
||||
|
||||
if config.FiatRates == "" || config.FiatRatesParams == "" {
|
||||
t.Errorf("Error parsing FiatRates config - empty parameter")
|
||||
t.Fatalf("Error parsing FiatRates config - empty parameter")
|
||||
return
|
||||
}
|
||||
testStartTime := time.Date(2019, 11, 22, 16, 0, 0, 0, time.UTC)
|
||||
fiatRates, err := NewFiatRatesDownloader(d, config.FiatRates, config.FiatRatesParams, &testStartTime, nil)
|
||||
fiatRates, err := NewFiatRatesDownloader(d, config.FiatRates, config.FiatRatesParams, nil)
|
||||
if err != nil {
|
||||
t.Errorf("FiatRates init error: %v\n", err)
|
||||
t.Fatalf("FiatRates init error: %v", err)
|
||||
}
|
||||
if config.FiatRates == "coingecko" {
|
||||
timestamp, err := fiatRates.findEarliestMarketData()
|
||||
|
||||
// get current tickers
|
||||
currentTickers, err := fiatRates.downloader.CurrentTickers()
|
||||
if err != nil {
|
||||
t.Errorf("Error looking up earliest market data: %v", err)
|
||||
t.Fatalf("Error in CurrentTickers: %v", err)
|
||||
return
|
||||
}
|
||||
earliestTimestamp, _ := time.Parse(db.FiatRatesTimeFormat, "20130429000000")
|
||||
if *timestamp != earliestTimestamp {
|
||||
t.Errorf("Incorrect earliest available timestamp found. Wanted: %v, got: %v", earliestTimestamp, timestamp)
|
||||
if currentTickers == nil {
|
||||
t.Fatalf("CurrentTickers returned nil value")
|
||||
return
|
||||
}
|
||||
|
||||
// After verifying that findEarliestMarketData works correctly,
|
||||
// set the earliest available timestamp to 2 days ago for easier testing
|
||||
*timestamp = fiatRates.startTime.Add(time.Duration(-24*2) * time.Hour)
|
||||
wantCurrentTickers := db.CurrencyRatesTicker{
|
||||
Rates: map[string]float32{
|
||||
"aed": 8447.1,
|
||||
"ars": 268901,
|
||||
"aud": 3314.36,
|
||||
"btc": 0.07531005,
|
||||
"eth": 1,
|
||||
"eur": 2182.99,
|
||||
"ltc": 29.097696,
|
||||
"usd": 2299.72,
|
||||
},
|
||||
TokenRates: map[string]float32{
|
||||
"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 5.58195e-07,
|
||||
"0x906710835d1ae85275eb770f06873340ca54274b": 1.39852e-10,
|
||||
},
|
||||
Timestamp: currentTickers.Timestamp,
|
||||
}
|
||||
if !reflect.DeepEqual(currentTickers, &wantCurrentTickers) {
|
||||
t.Fatalf("CurrentTickers() = %v, want %v", *currentTickers, wantCurrentTickers)
|
||||
}
|
||||
|
||||
err = fiatRates.syncHistorical(timestamp)
|
||||
ticker, err := fiatRates.db.FiatRatesFindLastTicker("usd", "")
|
||||
if err != nil {
|
||||
t.Errorf("RatesDownloader syncHistorical error: %v", err)
|
||||
return
|
||||
t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
|
||||
}
|
||||
ticker, err := fiatRates.downloader.getTicker(fiatRates.startTime)
|
||||
if err != nil {
|
||||
// Do not exit on GET error, log it, wait and try again
|
||||
glog.Errorf("Sync GetData error: %v", err)
|
||||
return
|
||||
if ticker != nil {
|
||||
t.Fatalf("FiatRatesFindLastTicker found unexpected data")
|
||||
}
|
||||
err = fiatRates.db.FiatRatesStoreTicker(ticker)
|
||||
|
||||
// update historical tickers for the first time
|
||||
err = fiatRates.downloader.UpdateHistoricalTickers()
|
||||
if err != nil {
|
||||
glog.Errorf("Sync StoreTicker error %v", err)
|
||||
return
|
||||
t.Fatalf("UpdateHistoricalTickers 1st pass failed with error: %v", err)
|
||||
}
|
||||
err = fiatRates.downloader.UpdateHistoricalTokenTickers()
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateHistoricalTokenTickers 1st pass failed with error: %v", err)
|
||||
}
|
||||
|
||||
ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "")
|
||||
if err != nil || ticker == nil {
|
||||
t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
|
||||
}
|
||||
wantTicker := db.CurrencyRatesTicker{
|
||||
Rates: map[string]float32{
|
||||
"aed": 241272.48,
|
||||
"ars": 241272.48,
|
||||
"aud": 241272.48,
|
||||
"btc": 241272.48,
|
||||
"eth": 241272.48,
|
||||
"eur": 241272.48,
|
||||
"ltc": 241272.48,
|
||||
"usd": 1794.5397,
|
||||
},
|
||||
TokenRates: map[string]float32{
|
||||
"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.161734e+07,
|
||||
"0x906710835d1ae85275eb770f06873340ca54274b": 4.161734e+07,
|
||||
},
|
||||
Timestamp: time.Unix(1654732800, 0).UTC(),
|
||||
}
|
||||
if !reflect.DeepEqual(ticker, &wantTicker) {
|
||||
t.Fatalf("UpdateHistoricalTickers(usd) 1st pass = %v, want %v", *ticker, wantTicker)
|
||||
}
|
||||
|
||||
ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "")
|
||||
if err != nil || ticker == nil {
|
||||
t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
|
||||
}
|
||||
wantTicker = db.CurrencyRatesTicker{
|
||||
Rates: map[string]float32{
|
||||
"aed": 240402.97,
|
||||
"ars": 240402.97,
|
||||
"aud": 240402.97,
|
||||
"btc": 240402.97,
|
||||
"eth": 240402.97,
|
||||
"eur": 240402.97,
|
||||
"ltc": 240402.97,
|
||||
},
|
||||
TokenRates: map[string]float32{
|
||||
"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07,
|
||||
"0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07,
|
||||
},
|
||||
Timestamp: time.Unix(1654819200, 0).UTC(),
|
||||
}
|
||||
if !reflect.DeepEqual(ticker, &wantTicker) {
|
||||
t.Fatalf("UpdateHistoricalTickers(eur) 1st pass = %v, want %v", *ticker, wantTicker)
|
||||
}
|
||||
|
||||
// update historical tickers for the second time
|
||||
err = fiatRates.downloader.UpdateHistoricalTickers()
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateHistoricalTickers 2nd pass failed with error: %v", err)
|
||||
}
|
||||
err = fiatRates.downloader.UpdateHistoricalTokenTickers()
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateHistoricalTokenTickers 2nd pass failed with error: %v", err)
|
||||
}
|
||||
ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "")
|
||||
if err != nil || ticker == nil {
|
||||
t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
|
||||
}
|
||||
wantTicker = db.CurrencyRatesTicker{
|
||||
Rates: map[string]float32{
|
||||
"aed": 240402.97,
|
||||
"ars": 240402.97,
|
||||
"aud": 240402.97,
|
||||
"btc": 240402.97,
|
||||
"eth": 240402.97,
|
||||
"eur": 240402.97,
|
||||
"ltc": 240402.97,
|
||||
"usd": 1788.4183,
|
||||
},
|
||||
TokenRates: map[string]float32{
|
||||
"0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07,
|
||||
"0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07,
|
||||
},
|
||||
Timestamp: time.Unix(1654819200, 0).UTC(),
|
||||
}
|
||||
if !reflect.DeepEqual(ticker, &wantTicker) {
|
||||
t.Fatalf("UpdateHistoricalTickers(usd) 2nd pass = %v, want %v", *ticker, wantTicker)
|
||||
}
|
||||
ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "")
|
||||
if err != nil || ticker == nil {
|
||||
t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(ticker, &wantTicker) {
|
||||
t.Fatalf("UpdateHistoricalTickers(eur) 2nd pass = %v, want %v", *ticker, wantTicker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"}}
|
||||
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"},"market_data":{"current_price":{"aud":112.481,"brl":232.8687,"btc":1.0,"cad":117.617,"chf":108.7145,"cny":718.7368,"dkk":661.3731,"eur":88.6291,"gbp":74.9767,"hkd":903.2559,"idr":1130568.3956,"inr":6274.6092,"jpy":11364.3607,"krw":128625.969,"mxn":1412.9046,"myr":353.6681,"nzd":136.2101,"php":4792.6186,"pln":368.8928,"rub":3623.3519,"sek":758.5144,"sgd":143.534,"twd":3433.0342,"usd":117.0,"xag":4.9088,"xau":0.0808,"xdr":76.8864,"zar":1049.0856},"market_cap":{"aud":1248780934.15,"brl":2585343237.705,"btc":11102150.0,"cad":1305801576.55,"chf":1206964686.175,"cny":7979523764.12,"dkk":7342663362.165,"eur":983973562.5649999,"gbp":832402569.9049999,"hkd":10028082490.185,"idr":12551739913210.54,"inr":69661652529.78,"jpy":126168837145.505,"krw":1428024801733.35,"mxn":15686278804.89,"myr":3926476296.415,"nzd":1512224961.715,"php":53208370589.99,"pln":4095503199.52,"rub":40226996296.585,"sek":8421140645.96,"sgd":1593535998.1,"twd":38114060643.53,"usd":1298951550.0,"xag":54498233.92,"xau":897053.72,"xdr":853604345.7599999,"zar":11647105694.04},"total_volume":{"aud":0.0,"brl":0.0,"btc":0.0,"cad":0.0,"chf":0.0,"cny":0.0,"dkk":0.0,"eur":0.0,"gbp":0.0,"hkd":0.0,"idr":0.0,"inr":0.0,"jpy":0.0,"krw":0.0,"mxn":0.0,"myr":0.0,"nzd":0.0,"php":0.0,"pln":0.0,"rub":0.0,"sek":0.0,"sgd":0.0,"twd":0.0,"usd":0.0,"xag":0.0,"xau":0.0,"xdr":0.0,"zar":0.0}},"community_data":{"facebook_likes":null,"twitter_followers":null,"reddit_average_posts_48h":0.0,"reddit_average_comments_48h":0.0,"reddit_subscribers":null,"reddit_accounts_active_48h":null},"developer_data":{"forks":null,"stars":null,"subscribers":null,"total_issues":null,"closed_issues":null,"pull_requests_merged":null,"pull_request_contributors":null,"code_additions_deletions_4_weeks":{"additions":null,"deletions":null},"commit_count_4_weeks":null},"public_interest_stats":{"alexa_rank":null,"bing_matches":null}}
|
||||
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"}}
|
||||
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"},"market_data":{"current_price":{"aud":112.4208,"brl":233.1081,"btc":1.0,"cad":117.0104,"chf":108.4737,"cny":715.1351,"dkk":659.3659,"eur":88.4403,"gbp":74.4895,"hkd":899.8427,"idr":1128375.8759,"inr":6235.1253,"jpy":11470.4956,"krw":127344.157,"mxn":1401.3929,"myr":352.0832,"nzd":135.8774,"php":4740.2255,"pln":366.461,"rub":3602.9548,"sek":754.5925,"sgd":143.0844,"twd":3426.0044,"usd":116.79,"xag":4.9089,"xau":0.0807,"xdr":76.8136,"zar":1033.9804},"market_cap":{"aud":1249804517.76,"brl":2591509369.32,"btc":11117200.0,"cad":1300828018.88,"chf":1205923817.64,"cny":7950299933.72,"dkk":7330302583.48,"eur":983208503.1599998,"gbp":828114669.4000001,"hkd":10003731264.44,"idr":12544380287555.48,"inr":69317134985.16,"jpy":127519793684.32,"krw":1415710462200.4,"mxn":15579565147.88,"myr":3914179351.04,"nzd":1510576231.28,"php":52698034928.6,"pln":4074020229.2,"rub":40054769102.56,"sek":8388955741.0,"sgd":1590697891.68,"twd":38087576115.68,"usd":1298377788.0,"xag":54573223.08,"xau":897158.0399999999,"xdr":853952153.9199998,"zar":11494966902.88},"total_volume":{"aud":0.0,"brl":0.0,"btc":0.0,"cad":0.0,"chf":0.0,"cny":0.0,"dkk":0.0,"eur":0.0,"gbp":0.0,"hkd":0.0,"idr":0.0,"inr":0.0,"jpy":0.0,"krw":0.0,"mxn":0.0,"myr":0.0,"nzd":0.0,"php":0.0,"pln":0.0,"rub":0.0,"sek":0.0,"sgd":0.0,"twd":0.0,"usd":0.0,"xag":0.0,"xau":0.0,"xdr":0.0,"zar":0.0}},"community_data":{"facebook_likes":null,"twitter_followers":null,"reddit_average_posts_48h":0.0,"reddit_average_comments_48h":0.0,"reddit_subscribers":null,"reddit_accounts_active_48h":null},"developer_data":{"forks":null,"stars":null,"subscribers":null,"total_issues":null,"closed_issues":null,"pull_requests_merged":null,"pull_request_contributors":null,"code_additions_deletions_4_weeks":{"additions":null,"deletions":null},"commit_count_4_weeks":null},"public_interest_stats":{"alexa_rank":null,"bing_matches":null}}
|
||||
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"},"market_data":{"current_price":{"aud":126.3133,"brl":259.5964,"btc":1.0,"cad":126.0278,"chf":115.6713,"cny":748.4143,"dkk":695.3988,"eur":93.2043,"gbp":79.6307,"hkd":946.0816,"idr":1196029.7068,"inr":6892.8316,"jpy":12203.5904,"krw":137012.0733,"mxn":1551.7041,"myr":377.299,"nzd":152.0791,"php":5112.2715,"pln":396.1512,"rub":3891.7674,"sek":800.3999,"sgd":152.8465,"twd":3646.9125,"uah":987.805115156,"usd":121.309,"xag":5.3382,"xau":0.0868,"xdr":81.033,"zar":1200.5467},"market_cap":{"aud":1420386743.3035636,"brl":2919148539.142982,"btc":11244950.003709536,"cad":1417176310.0775046,"chf":1300717985.3640869,"cny":8415881385.561269,"dkk":7819724738.639607,"eur":1048077693.6307446,"gbp":895443240.2603929,"hkd":10638640291.429523,"idr":13449294255917.375,"inr":77509546725.9892,"jpy":137228763913.74965,"krw":1540693914163.0862,"mxn":17448835025.0511,"myr":4242708391.449604,"nzd":1710121876.1091428,"php":57487237422.88915,"pln":4454700437.909536,"rub":43762729839.06665,"sek":9000456858.474112,"sgd":1718751250.7419894,"twd":41009348730.40335,"uah":11107819133.33776,"usd":1364113640.0,"xag":60027792.10980224,"xau":976061.6603219877,"xdr":911212033.6505947,"zar":13500087618.61847},"total_volume":{"aud":0.0,"brl":0.0,"btc":0.0,"cad":0.0,"chf":0.0,"cny":0.0,"dkk":0.0,"eur":0.0,"gbp":0.0,"hkd":0.0,"idr":0.0,"inr":0.0,"jpy":0.0,"krw":0.0,"mxn":0.0,"myr":0.0,"nzd":0.0,"php":0.0,"pln":0.0,"rub":0.0,"sek":0.0,"sgd":0.0,"twd":0.0,"uah":0.0,"usd":0.0,"xag":0.0,"xau":0.0,"xdr":0.0,"zar":0.0}},"community_data":{"facebook_likes":null,"twitter_followers":null,"reddit_average_posts_48h":0.0,"reddit_average_comments_48h":0.0,"reddit_subscribers":null,"reddit_accounts_active_48h":null},"developer_data":{"forks":null,"stars":null,"subscribers":null,"total_issues":null,"closed_issues":null,"pull_requests_merged":null,"pull_request_contributors":null,"code_additions_deletions_4_weeks":{"additions":null,"deletions":null},"commit_count_4_weeks":null},"public_interest_stats":{"alexa_rank":null,"bing_matches":null}}
|
||||
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"},"market_data":{"current_price":{"aud":130.8239,"bdt":9961.3816372,"bhd":48.39643563999999,"bmd":128.38,"brl":272.2555,"btc":1.0,"cad":127.0763,"chf":111.3766,"cny":754.702,"dkk":677.221,"eur":90.7575,"gbp":76.6108,"hkd":955.6959,"idr":1401976.6268,"inr":7601.0098,"jpy":11938.215,"krw":132238.2292,"ltc":59.84440454817475,"mmk":124564.42058759999,"mxn":1619.1347,"myr":393.0957,"nzd":148.4503,"php":5315.0149,"pln":381.3587,"rub":3973.0086,"sek":791.16,"sgd":153.7814,"twd":3623.6843,"uah":1051.00353918,"usd":128.38,"vef":807.6801751200001,"xag":5.5156,"xau":0.0932,"xdr":80.1381,"zar":1233.5253},"market_cap":{"aud":1544578916.545,"bdt":117609550368.68365,"bhd":571394937.205442,"bmd":1515724889.0,"brl":3214398173.525,"btc":11806550.0,"cad":1500332689.765,"chf":1314973396.73,"cny":8910426898.1,"dkk":7995643597.55,"eur":1071532961.6249999,"gbp":904509240.74,"hkd":11283471428.145,"idr":16552507143145.54,"inr":89741702254.19,"jpy":140949132308.25,"krw":1561277264961.26,"ltc":706555954.5182526,"mmk":1470676059888.5288,"mxn":19116394792.285,"myr":4641104036.835,"nzd":1752685889.465,"php":62751989167.595,"pln":4502530559.485,"rub":46907524686.33,"sek":9340870098.0,"sgd":1815627788.17,"twd":42783209872.165,"uah":12408725835.50563,"usd":1515724889.0,"vef":9535916371.563036,"xag":65120207.18,"xau":1100370.4600000002,"xdr":946154484.5549998,"zar":14563678130.715},"total_volume":{"aud":0.0,"bdt":0.0,"bhd":0.0,"bmd":0.0,"brl":0.0,"btc":0.0,"cad":0.0,"chf":0.0,"cny":0.0,"dkk":0.0,"eur":0.0,"gbp":0.0,"hkd":0.0,"idr":0.0,"inr":0.0,"jpy":0.0,"krw":0.0,"ltc":0.0,"mmk":0.0,"mxn":0.0,"myr":0.0,"nzd":0.0,"php":0.0,"pln":0.0,"rub":0.0,"sek":0.0,"sgd":0.0,"twd":0.0,"uah":0.0,"usd":0.0,"vef":0.0,"xag":0.0,"xau":0.0,"xdr":0.0,"zar":0.0}},"community_data":{"facebook_likes":null,"twitter_followers":null,"reddit_average_posts_48h":0.0,"reddit_average_comments_48h":0.0,"reddit_subscribers":null,"reddit_accounts_active_48h":null},"developer_data":{"forks":null,"stars":null,"subscribers":null,"total_issues":null,"closed_issues":null,"pull_requests_merged":null,"pull_request_contributors":null,"code_additions_deletions_4_weeks":{"additions":null,"deletions":null},"commit_count_4_weeks":null},"public_interest_stats":{"alexa_rank":null,"bing_matches":null}}
|
||||
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"},"market_data":{"current_price":{"aud":635.4009,"bdt":46187.0412867627,"bhd":224.2182020654,"bmd":594.6833,"brl":1330.3026,"btc":1.0,"cad":645.6162,"chf":537.5144,"cny":3818.09,"dkk":3291.0056,"eur":439.2353,"gbp":350.5075,"hkd":4609.773,"idr":7056654.8237,"inr":35623.7845,"jpy":60664.7779,"krw":605273.662,"ltc":58.68544600938967,"mmk":576316.9820261401,"mxn":7774.4393,"myr":1921.5065,"nzd":689.3958,"php":26182.8705,"pln":1816.6144,"rub":20449.5057,"sek":3957.6568,"sgd":743.7241,"twd":17936.3326,"uah":6989.384186896,"usd":594.6833,"vef":3749.9319498579002,"vnd":12616057.538675,"xag":30.3811,"xau":0.4678,"xdr":388.0442,"zar":6383.9883},"market_cap":{"aud":8191238932.305,"bdt":595417933396.2369,"bhd":2890497741.0160007,"bmd":7666330027.785,"brl":17149529452.77,"btc":12891450.0,"cad":8322928961.49,"chf":6929340011.88,"cny":49220716330.5,"dkk":42425834142.12,"eur":5662379908.185,"gbp":4518549910.875,"hkd":59426658140.85,"idr":90970512826987.36,"inr":459242236692.525,"jpy":782056951058.955,"krw":7802855149989.9,"ltc":756540492.9577465,"mmk":7429561557940.883,"mxn":100223795513.985,"myr":24771004969.425,"nzd":8887311485.91,"php":337535165907.225,"pln":23418793706.88,"rub":263623780256.265,"sek":51019934754.36,"sgd":9587682048.945,"twd":231225334896.27,"uah":90103296776.16043,"usd":7666330027.785,"vef":48342060234.99562,"vnd":162639274956951.8,"xag":391656431.595,"xau":6030620.31,"xdr":5002452402.09,"zar":82298865970.035},"total_volume":{"aud":40549103.56573137,"bdt":2947498375.4849124,"bhd":14308835.723827252,"bmd":37950646.151919045,"brl":84895343.87055749,"btc":63816.5661486022,"cad":41201008.933909185,"chf":34302323.26342622,"cny":243657393.04631656,"dkk":210020676.56782028,"eur":28030488.577251133,"gbp":22368185.059331186,"hkd":294179883.5845404,"idr":450331479344.50385,"inr":2273387600.0077996,"jpy":3871417811.7456107,"krw":38626486689.029686,"ltc":3745103.647218439,"mmk":36778570806.03395,"mxn":496138019.85674256,"myr":122623946.66221909,"nzd":43994872.673268534,"php":1670900887.223535,"pln":115930093.0241033,"rub":1305017233.2102678,"sek":252564066.9706653,"sgd":47461918.22395964,"twd":1144635155.8312302,"uah":446038498.30104274,"usd":37950646.151919045,"vef":239307780.33086348,"vnd":805113470451.4246,"xag":1938817.4778172984,"xau":29853.38964431611,"xdr":24763648.357881423,"zar":407404211.6388525}},"community_data":{"facebook_likes":22450,"twitter_followers":54747,"reddit_average_posts_48h":2.449,"reddit_average_comments_48h":266.163,"reddit_subscribers":122886,"reddit_accounts_active_48h":"957.0"},"developer_data":{"forks":3894,"stars":5469,"subscribers":757,"total_issues":4332,"closed_issues":3943,"pull_requests_merged":1950,"pull_request_contributors":201,"code_additions_deletions_4_weeks":{"additions":null,"deletions":null},"commit_count_4_weeks":null},"public_interest_stats":{"alexa_rank":null,"bing_matches":null}}
|
||||
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"}}
|
||||
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"}}
|
||||
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"},"market_data":{"current_price":{"aud":130.7952,"brl":268.8555,"btc":1.0,"cad":136.8008,"chf":126.7471,"cny":830.3415,"dkk":769.4261,"eur":103.1862,"gbp":86.889,"hkd":1043.747,"idr":1306348.9692,"inr":7304.2353,"jpy":13203.1967,"krw":149390.4586,"mxn":1633.6086,"myr":407.992,"nzd":158.5211,"php":5543.837,"pln":429.2283,"rub":4203.4233,"sek":884.1254,"sgd":166.2931,"usd":135.3,"xag":5.716,"xau":0.0938,"zar":1223.2239},"market_cap":{"aud":1450558006.5599997,"brl":2981688151.6499996,"btc":11090299.999999998,"cad":1517161912.2399998,"chf":1405663363.1299999,"cny":9208736337.449999,"dkk":8533166276.829999,"eur":1144365913.86,"gbp":963625076.6999998,"hkd":11575467354.099998,"idr":14487801973118.756,"inr":81006160747.59,"jpy":146427412362.00998,"krw":1656785003011.5798,"mxn":18117209456.579998,"myr":4524753677.599999,"nzd":1758046555.3299997,"php":61482815481.09999,"pln":4760270615.489999,"rub":46617225423.99,"sek":9805215923.619999,"sgd":1844240366.9299998,"usd":1500517590,"xag":63392154.79999999,"xau":1040270.1399999998,"zar":13565920018.169998},"total_volume":{"aud":0.0,"brl":0.0,"btc":0.0,"cad":0.0,"chf":0.0,"cny":0.0,"dkk":0.0,"eur":0.0,"gbp":0.0,"hkd":0.0,"idr":0.0,"inr":0.0,"jpy":0.0,"krw":0.0,"mxn":0.0,"myr":0.0,"nzd":0.0,"php":0.0,"pln":0.0,"rub":0.0,"sek":0.0,"sgd":0.0,"usd":0,"xag":0.0,"xau":0.0,"zar":0.0}},"community_data":{"facebook_likes":null,"twitter_followers":null,"reddit_average_posts_48h":0.0,"reddit_average_comments_48h":0.0,"reddit_subscribers":null,"reddit_accounts_active_48h":null},"developer_data":{"forks":null,"stars":null,"subscribers":null,"total_issues":null,"closed_issues":null,"pull_requests_merged":null,"pull_request_contributors":null,"code_additions_deletions_4_weeks":{"additions":null,"deletions":null},"commit_count_4_weeks":null},"public_interest_stats":{"alexa_rank":null,"bing_matches":null}}
|
||||
@ -1 +0,0 @@
|
||||
{"id":"bitcoin","symbol":"btc","name":"Bitcoin","localization":{"en":"Bitcoin","de":"Bitcoin","es":"Bitcoin","fr":"Bitcoin","it":"Bitcoin","pl":"Bitcoin","ro":"Bitcoin","hu":"Bitcoin","nl":"Bitcoin","pt":"Bitcoin","sv":"Bitcoin","vi":"Bitcoin","tr":"Bitcoin","ru":"биткоина","ja":"ビットコイン","zh":"比特币","zh-tw":"比特幣","ko":"비트코인","ar":"بيتكوين","th":"บิตคอยน์","id":"Bitcoin"},"image":{"thumb":"https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579","small":"https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579"},"market_data":{"current_price":{"aud":140.0534,"brl":287.7259,"btc":1.0,"cad":146.3907,"chf":135.5619,"cny":889.0842,"dkk":822.6608,"eur":110.3745,"gbp":93.0697,"hkd":1117.9148,"idr":1394387.3129,"inr":7822.3338,"jpy":14108.4087,"krw":159839.6429,"mxn":1748.706,"myr":436.5932,"nzd":169.7084,"php":5937.7695,"pln":459.2644,"rub":4501.5503,"sek":944.2334,"sgd":178.0707,"twd":4262.3287,"usd":141.96,"xag":6.1223,"xau":0.1005,"xdr":95.6015,"zar":1309.9167},"market_cap":{"aud":1553878467.66,"brl":3192290087.91,"btc":11094900.0,"cad":1624190177.43,"chf":1504045724.31,"cny":9864300290.58,"dkk":9127339309.92,"eur":1224594040.05,"gbp":1032599014.53,"hkd":12403152914.52,"idr":15470587797894.21,"inr":86788011277.62,"jpy":156531383685.63,"krw":1773404854011.21,"mxn":19401718199.4,"myr":4843957894.68,"nzd":1882897727.16,"php":65878958825.55,"pln":5095492591.56,"rub":49944250423.47,"sek":10476175149.66,"sgd":1975676609.43,"twd":47290110693.63,"usd":1575032004.0,"xag":67926306.27,"xau":1115037.45,"xdr":1060689082.35,"zar":14533394794.83},"total_volume":{"aud":0.0,"brl":0.0,"btc":0.0,"cad":0.0,"chf":0.0,"cny":0.0,"dkk":0.0,"eur":0.0,"gbp":0.0,"hkd":0.0,"idr":0.0,"inr":0.0,"jpy":0.0,"krw":0.0,"mxn":0.0,"myr":0.0,"nzd":0.0,"php":0.0,"pln":0.0,"rub":0.0,"sek":0.0,"sgd":0.0,"twd":0.0,"usd":0.0,"xag":0.0,"xau":0.0,"xdr":0.0,"zar":0.0}},"community_data":{"facebook_likes":null,"twitter_followers":null,"reddit_average_posts_48h":0.0,"reddit_average_comments_48h":0.0,"reddit_subscribers":null,"reddit_accounts_active_48h":null},"developer_data":{"forks":null,"stars":null,"subscribers":null,"total_issues":null,"closed_issues":null,"pull_requests_merged":null,"pull_request_contributors":null,"code_additions_deletions_4_weeks":{"additions":null,"deletions":null},"commit_count_4_weeks":null},"public_interest_stats":{"alexa_rank":null,"bing_matches":null}}
|
||||
30
fiat/mock_data/coinlist.json
Normal file
30
fiat/mock_data/coinlist.json
Normal file
@ -0,0 +1,30 @@
|
||||
[
|
||||
{ "id": "01coin", "symbol": "zoc", "name": "01coin", "platforms": {} },
|
||||
{
|
||||
"id": "0-5x-long-algorand-token",
|
||||
"symbol": "algohalf",
|
||||
"name": "0.5X Long Algorand Token",
|
||||
"platforms": { "ethereum": "" }
|
||||
},
|
||||
{ "id": "ethereum", "symbol": "eth", "name": "Ethereum", "platforms": {} },
|
||||
{
|
||||
"id": "ethereum-cash-token",
|
||||
"symbol": "ecash",
|
||||
"name": "Ethereum Cash Token",
|
||||
"platforms": { "ethereum": "0x906710835d1ae85275eb770f06873340ca54274b" }
|
||||
},
|
||||
{
|
||||
"id": "santa-shiba",
|
||||
"symbol": "santashib",
|
||||
"name": "Santa Shiba",
|
||||
"platforms": {
|
||||
"binance-smart-chain": "0x74c609b16512869b1873f5a9d7999deee386e740"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "vendit",
|
||||
"symbol": "vndt",
|
||||
"name": "Vendit",
|
||||
"platforms": { "ethereum": "0x5e9997684d061269564f94e5d11ba6ce6fa9528c" }
|
||||
}
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
23
fiat/mock_data/market_chart_eth_other.json
Normal file
23
fiat/mock_data/market_chart_eth_other.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"prices": [
|
||||
[1654560000000, 245991.30610738738],
|
||||
[1654646400000, 241439.61063702923],
|
||||
[1654732800000, 241272.47868584536],
|
||||
[1654819200000, 240402.9616407818],
|
||||
[1654874261000, 232687.7973743471]
|
||||
],
|
||||
"market_caps": [
|
||||
[1654560000000, 29783749062026.934],
|
||||
[1654646400000, 29309140822797.383],
|
||||
[1654732800000, 29218371977967.83],
|
||||
[1654819200000, 29135342816603.11],
|
||||
[1654874261000, 28322159926577.836]
|
||||
],
|
||||
"total_volumes": [
|
||||
[1654560000000, 2198234703995.5186],
|
||||
[1654646400000, 3139844528072.9595],
|
||||
[1654732800000, 2381462737920.105],
|
||||
[1654819200000, 1407275835572.8992],
|
||||
[1654874261000, 1875231811513.972]
|
||||
]
|
||||
}
|
||||
14
fiat/mock_data/market_chart_eth_usd_1.json
Normal file
14
fiat/mock_data/market_chart_eth_usd_1.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"prices": [
|
||||
[1654819200000, 1788.4182866616045],
|
||||
[1654871975000, 1741.4106052586249]
|
||||
],
|
||||
"market_caps": [
|
||||
[1654819200000, 216720355679.05618],
|
||||
[1654871975000, 210920939953.81134]
|
||||
],
|
||||
"total_volumes": [
|
||||
[1654819200000, 10469080004.414614],
|
||||
[1654871975000, 13875498345.972267]
|
||||
]
|
||||
}
|
||||
17
fiat/mock_data/market_chart_eth_usd_max.json
Normal file
17
fiat/mock_data/market_chart_eth_usd_max.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"prices": [
|
||||
[1654560000000, 1860.1813068416047],
|
||||
[1654646400000, 1818.3877119829308],
|
||||
[1654732800000, 1794.539625671828]
|
||||
],
|
||||
"market_caps": [
|
||||
[1654560000000, 225224111085.68793],
|
||||
[1654646400000, 220727955347.00992],
|
||||
[1654732800000, 217320792647.69748]
|
||||
],
|
||||
"total_volumes": [
|
||||
[1654560000000, 16623006597.793545],
|
||||
[1654646400000, 23647547692.445885],
|
||||
[1654732800000, 17712874976.607395]
|
||||
]
|
||||
}
|
||||
23
fiat/mock_data/market_chart_token_other.json
Normal file
23
fiat/mock_data/market_chart_token_other.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"prices": [
|
||||
[1654560000000, 43129640.779293984],
|
||||
[1654646400000, 42170403.75197084],
|
||||
[1654732800000, 41617340.4960857],
|
||||
[1654819200000, 41464477.97624925],
|
||||
[1654893557000, 39012012.89610346]
|
||||
],
|
||||
"market_caps": [
|
||||
[1654560000000, 5221982916522588],
|
||||
[1654646400000, 5118923172979404],
|
||||
[1654732800000, 5039907336185186],
|
||||
[1654819200000, 5024661446418917],
|
||||
[1654893557000, 4722632860950729]
|
||||
],
|
||||
"total_volumes": [
|
||||
[1654560000000, 385416357318398.5],
|
||||
[1654646400000, 548412545554966],
|
||||
[1654732800000, 410780981662688],
|
||||
[1654819200000, 242725619902352.5],
|
||||
[1654893557000, 395315245827820.75]
|
||||
]
|
||||
}
|
||||
12
fiat/mock_data/simpleprice_base.json
Normal file
12
fiat/mock_data/simpleprice_base.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"ethereum": {
|
||||
"btc": 0.07531005,
|
||||
"eth": 1.0,
|
||||
"ltc": 29.097696,
|
||||
"usd": 2299.72,
|
||||
"eur": 2182.99,
|
||||
"aed": 8447.1,
|
||||
"ars": 268901,
|
||||
"aud": 3314.36
|
||||
}
|
||||
}
|
||||
8
fiat/mock_data/simpleprice_tokens.json
Normal file
8
fiat/mock_data/simpleprice_tokens.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"ethereum-cash-token": {
|
||||
"eth": 1.39852e-10
|
||||
},
|
||||
"vendit": {
|
||||
"eth": 5.58195e-07
|
||||
}
|
||||
}
|
||||
10
fiat/mock_data/vs_currencies.json
Normal file
10
fiat/mock_data/vs_currencies.json
Normal file
@ -0,0 +1,10 @@
|
||||
[
|
||||
"btc",
|
||||
"eth",
|
||||
"ltc",
|
||||
"usd",
|
||||
"eur",
|
||||
"aed",
|
||||
"ars",
|
||||
"aud"
|
||||
]
|
||||
@ -193,7 +193,7 @@ func (s *PublicServer) ConnectFullPublicInterface() {
|
||||
serveMux.HandleFunc(path+"api/v2/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault))
|
||||
serveMux.HandleFunc(path+"api/v2/tickers/", s.jsonHandler(s.apiTickers, apiV2))
|
||||
serveMux.HandleFunc(path+"api/v2/multi-tickers/", s.jsonHandler(s.apiMultiTickers, apiV2))
|
||||
serveMux.HandleFunc(path+"api/v2/tickers-list/", s.jsonHandler(s.apiTickersList, apiV2))
|
||||
serveMux.HandleFunc(path+"api/v2/tickers-list/", s.jsonHandler(s.apiAvailableVsCurrencies, apiV2))
|
||||
// socket.io interface
|
||||
serveMux.Handle(path+"socket.io/", s.socketio.GetHandler())
|
||||
// websocket interface
|
||||
@ -1197,21 +1197,21 @@ func (s *PublicServer) apiSendTx(r *http.Request, apiVersion int) (interface{},
|
||||
return nil, api.NewAPIError("Missing tx blob", true)
|
||||
}
|
||||
|
||||
// apiTickersList returns a list of available FiatRates currencies
|
||||
func (s *PublicServer) apiTickersList(r *http.Request, apiVersion int) (interface{}, error) {
|
||||
// apiAvailableVsCurrencies returns a list of available versus currencies
|
||||
func (s *PublicServer) apiAvailableVsCurrencies(r *http.Request, apiVersion int) (interface{}, error) {
|
||||
s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-list"}).Inc()
|
||||
timestampString := strings.ToLower(r.URL.Query().Get("timestamp"))
|
||||
timestamp, err := strconv.ParseInt(timestampString, 10, 64)
|
||||
if err != nil {
|
||||
return nil, api.NewAPIError("Parameter \"timestamp\" is not a valid Unix timestamp.", true)
|
||||
}
|
||||
result, err := s.api.GetFiatRatesTickersList(timestamp)
|
||||
result, err := s.api.GetAvailableVsCurrencies(timestamp)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// apiTickers returns FiatRates ticker prices for the specified block or timestamp.
|
||||
func (s *PublicServer) apiTickers(r *http.Request, apiVersion int) (interface{}, error) {
|
||||
var result *db.ResultTickerAsString
|
||||
var result *api.FiatTicker
|
||||
var err error
|
||||
|
||||
currency := strings.ToLower(r.URL.Query().Get("currency"))
|
||||
@ -1251,7 +1251,7 @@ func (s *PublicServer) apiTickers(r *http.Request, apiVersion int) (interface{},
|
||||
|
||||
// apiMultiTickers returns FiatRates ticker prices for the specified comma separated list of timestamps.
|
||||
func (s *PublicServer) apiMultiTickers(r *http.Request, apiVersion int) (interface{}, error) {
|
||||
var result []db.ResultTickerAsString
|
||||
var result []api.FiatTicker
|
||||
var err error
|
||||
|
||||
currency := strings.ToLower(r.URL.Query().Get("currency"))
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/flier/gorocksdb"
|
||||
"github.com/golang/glog"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/martinboehm/btcutil/chaincfg"
|
||||
@ -161,51 +162,56 @@ func newPostRequest(u string, body string) *http.Request {
|
||||
return r
|
||||
}
|
||||
|
||||
func insertFiatRate(date string, rates map[string]float64, d *db.RocksDB) error {
|
||||
func insertFiatRate(date string, rates map[string]float32, d *db.RocksDB) error {
|
||||
convertedDate, err := db.FiatRatesConvertDate(date)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ticker := &db.CurrencyRatesTicker{
|
||||
Timestamp: convertedDate,
|
||||
Timestamp: *convertedDate,
|
||||
Rates: rates,
|
||||
}
|
||||
return d.FiatRatesStoreTicker(ticker)
|
||||
wb := gorocksdb.NewWriteBatch()
|
||||
defer wb.Destroy()
|
||||
if err := d.FiatRatesStoreTicker(wb, ticker); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.WriteBatch(wb)
|
||||
}
|
||||
|
||||
// initTestFiatRates initializes test data for /api/v2/tickers endpoint
|
||||
func initTestFiatRates(d *db.RocksDB) error {
|
||||
if err := insertFiatRate("20180320020000", map[string]float64{
|
||||
if err := insertFiatRate("20180320020000", map[string]float32{
|
||||
"usd": 2000.0,
|
||||
"eur": 1300.0,
|
||||
}, d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := insertFiatRate("20180320030000", map[string]float64{
|
||||
if err := insertFiatRate("20180320030000", map[string]float32{
|
||||
"usd": 2001.0,
|
||||
"eur": 1301.0,
|
||||
}, d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := insertFiatRate("20180320040000", map[string]float64{
|
||||
if err := insertFiatRate("20180320040000", map[string]float32{
|
||||
"usd": 2002.0,
|
||||
"eur": 1302.0,
|
||||
}, d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := insertFiatRate("20180321055521", map[string]float64{
|
||||
if err := insertFiatRate("20180321055521", map[string]float32{
|
||||
"usd": 2003.0,
|
||||
"eur": 1303.0,
|
||||
}, d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := insertFiatRate("20191121140000", map[string]float64{
|
||||
if err := insertFiatRate("20191121140000", map[string]float32{
|
||||
"usd": 7814.5,
|
||||
"eur": 7100.0,
|
||||
}, d); err != nil {
|
||||
return err
|
||||
}
|
||||
return insertFiatRate("20191121143015", map[string]float64{
|
||||
return insertFiatRate("20191121143015", map[string]float32{
|
||||
"usd": 7914.5,
|
||||
"eur": 7134.1,
|
||||
}, d)
|
||||
|
||||
@ -421,7 +421,7 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs
|
||||
}{}
|
||||
err = json.Unmarshal(req.Params, &r)
|
||||
if err == nil {
|
||||
rv, err = s.getFiatRatesTickersList(r.Timestamp)
|
||||
rv, err = s.getAvailableVsCurrencies(r.Timestamp)
|
||||
}
|
||||
return
|
||||
},
|
||||
@ -960,7 +960,7 @@ func (s *WebsocketServer) OnNewTx(tx *bchain.MempoolTx) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) broadcastTicker(currency string, rates map[string]float64) {
|
||||
func (s *WebsocketServer) broadcastTicker(currency string, rates map[string]float32) {
|
||||
as, ok := s.fiatRatesSubscriptions[currency]
|
||||
if ok && len(as) > 0 {
|
||||
data := struct {
|
||||
@ -983,7 +983,7 @@ func (s *WebsocketServer) OnNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) {
|
||||
s.fiatRatesSubscriptionsLock.Lock()
|
||||
defer s.fiatRatesSubscriptionsLock.Unlock()
|
||||
for currency, rate := range ticker.Rates {
|
||||
s.broadcastTicker(currency, map[string]float64{currency: rate})
|
||||
s.broadcastTicker(currency, map[string]float32{currency: rate})
|
||||
}
|
||||
s.broadcastTicker(allFiatRates, ticker.Rates)
|
||||
}
|
||||
@ -998,7 +998,7 @@ func (s *WebsocketServer) getFiatRatesForTimestamps(timestamps []int64, currenci
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) getFiatRatesTickersList(timestamp int64) (interface{}, error) {
|
||||
ret, err := s.api.GetFiatRatesTickersList(timestamp)
|
||||
func (s *WebsocketServer) getAvailableVsCurrencies(timestamp int64) (interface{}, error) {
|
||||
ret, err := s.api.GetAvailableVsCurrencies(timestamp)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user