Fiat rates refactor, fetch rates for tokens

This commit is contained in:
Martin Boehm 2022-06-15 09:32:48 +02:00 committed by Martin
parent db91824dc3
commit e0be8aa400
36 changed files with 1118 additions and 432 deletions

View File

@ -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"`
}

View File

@ -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

View File

@ -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 {

View File

@ -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/"
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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)
}

View File

@ -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(&currentDate)
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
}

View File

@ -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)
}
}
}

View File

@ -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"}}

View File

@ -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}}

View File

@ -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"}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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

View File

@ -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"}}

View File

@ -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"}}

View File

@ -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}}

View File

@ -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}}

View 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

View 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]
]
}

View 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]
]
}

View 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]
]
}

View 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]
]
}

View 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
}
}

View File

@ -0,0 +1,8 @@
{
"ethereum-cash-token": {
"eth": 1.39852e-10
},
"vendit": {
"eth": 5.58195e-07
}
}

View File

@ -0,0 +1,10 @@
[
"btc",
"eth",
"ltc",
"usd",
"eur",
"aed",
"ars",
"aud"
]

View File

@ -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"))

View File

@ -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)

View File

@ -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
}