diff --git a/api/worker.go b/api/worker.go index b2032c58..b2fe1fdd 100644 --- a/api/worker.go +++ b/api/worker.go @@ -13,6 +13,7 @@ import ( "os" "sort" "strconv" + "strings" "time" "github.com/golang/glog" @@ -972,6 +973,146 @@ func (w *Worker) GetBlocks(page int, blocksOnPage int) (*Blocks, error) { return r, nil } +// getFiatRatesResult checks if CurrencyRatesTicker contains all necessary data and returns formatted result +func (w *Worker) getFiatRatesResult(currency string, ticker *db.CurrencyRatesTicker) (*db.ResultTickerAsString, error) { + resultRates := make(map[string]json.Number) + timeFormatted := ticker.Timestamp.Format(db.FiatRatesTimeFormat) + + // Check if both USD rate and the desired currency rate exist in the result + for _, currencySymbol := range []string{"usd", currency} { + if _, found := ticker.Rates[currencySymbol]; !found { + availableCurrencies := make([]string, 0, len(ticker.Rates)) + for availableCurrency := range ticker.Rates { + availableCurrencies = append(availableCurrencies, string(availableCurrency)) + } + sort.Strings(availableCurrencies) // sort to get deterministic results + availableCurrenciesString := strings.Join(availableCurrencies, ", ") + return nil, NewAPIError(fmt.Sprintf("Currency %q is not available for timestamp %s. Available currencies are: %s.", currency, timeFormatted, availableCurrenciesString), true) + } + resultRates[currencySymbol] = ticker.Rates[currencySymbol] + if currencySymbol == "usd" && currency == "usd" { + break + } + } + + result := &db.ResultTickerAsString{ + Timestamp: timeFormatted, + Rates: resultRates, + } + return result, nil +} + +// GetFiatRatesForBlockID returns fiat rates for block height or block hash +func (w *Worker) GetFiatRatesForBlockID(bid string, currency string) (*db.ResultTickerAsString, error) { + if currency == "" { + return nil, NewAPIError("Missing or empty \"currency\" parameter", true) + } + ticker := &db.CurrencyRatesTicker{} + bi, err := w.getBlockInfoFromBlockID(bid) + 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, error: %v", bid, err), false) + } + dbi := &db.BlockInfo{Time: bi.Time} // get timestamp from block + tm := time.Unix(dbi.Time, 0) // convert it to Time object + ticker, err = w.db.FiatRatesFindTicker(&tm) + if err != nil { + return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false) + } else if ticker == nil { + return nil, NewAPIError(fmt.Sprintf("No tickers available for %s (%s)", tm, currency), true) + } + result, err := w.getFiatRatesResult(currency, ticker) + if err != nil { + return nil, err + } + return result, nil +} + +// GetCurrentFiatRates returns current fiat rates +func (w *Worker) GetCurrentFiatRates(currency string) (*db.ResultTickerAsString, error) { + if currency == "" { + return nil, NewAPIError("Missing or empty \"currency\" parameter", true) + } + ticker, err := w.db.FiatRatesFindLastTicker() + if err != nil { + return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false) + } else if ticker == nil { + return nil, NewAPIError(fmt.Sprintf("No tickers found!"), true) + } + result, err := w.getFiatRatesResult(currency, ticker) + if err != nil { + return nil, err + } + return result, nil +} + +// GetFiatRatesForDates returns fiat rates for each of the provided dates +func (w *Worker) GetFiatRatesForDates(dateStrings []string, currency string) (*db.ResultTickersAsString, error) { + if currency == "" { + return nil, NewAPIError("Missing or empty \"currency\" parameter", true) + } else if len(dateStrings) == 0 { + return nil, NewAPIError("No dates provided", true) + } + + ret := &db.ResultTickersAsString{} + for _, dateString := range dateStrings { + date, err := db.FiatRatesConvertDate(dateString) + if err != nil { + ret.Tickers = append(ret.Tickers, db.ResultTickerAsString{Error: fmt.Sprintf("%v", err)}) + continue + } + ticker, err := w.db.FiatRatesFindTicker(date) + if err != nil { + glog.Errorf("Error finding ticker by date %v. Error: %v", dateString, err) + ret.Tickers = append(ret.Tickers, db.ResultTickerAsString{Error: "Ticker not found."}) + continue + } else if ticker == nil { + ret.Tickers = append(ret.Tickers, db.ResultTickerAsString{Error: fmt.Sprintf("No tickers available for %s (%s)", date, currency)}) + continue + } + result, err := w.getFiatRatesResult(currency, ticker) + if err != nil { + ret.Tickers = append(ret.Tickers, db.ResultTickerAsString{Error: fmt.Sprintf("%v", err)}) + continue + } + ret.Tickers = append(ret.Tickers, *result) + } + return ret, nil +} + +// GetFiatRatesTickersList returns the list of available fiatRates tickers +func (w *Worker) GetFiatRatesTickersList(dateString string) (*db.ResultTickerListAsString, error) { + if dateString == "" { + return nil, NewAPIError("Missing or empty \"date\" parameter", true) + } + date, err := db.FiatRatesConvertDate(dateString) + if err != nil { + return nil, err + } + + ticker, err := w.db.FiatRatesFindTicker(date) + if err != nil { + return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false) + } else if ticker == nil { + return nil, NewAPIError(fmt.Sprintf("No tickers found for date %v.", date), true) + } + + keys := make([]string, 0, len(ticker.Rates)) + for k := range ticker.Rates { + keys = append(keys, k) + } + sort.Strings(keys) // sort to get deterministic results + timeFormatted := ticker.Timestamp.Format(db.FiatRatesTimeFormat) + + return &db.ResultTickerListAsString{ + Timestamp: timeFormatted, + Tickers: keys, + }, nil +} + +// getBlockInfoFromBlockID returns block info from block height or block hash func (w *Worker) getBlockInfoFromBlockID(bid string) (*bchain.BlockInfo, error) { // try to decide if passed string (bid) is block height or block hash // if it's a number, must be less than int32 diff --git a/bchain/coins/btc/bitcoinrpc.go b/bchain/coins/btc/bitcoinrpc.go index ef50c710..2a66cb21 100644 --- a/bchain/coins/btc/bitcoinrpc.go +++ b/bchain/coins/btc/bitcoinrpc.go @@ -55,8 +55,8 @@ type Configuration struct { XPubMagicSegwitP2sh uint32 `json:"xpub_magic_segwit_p2sh,omitempty"` XPubMagicSegwitNative uint32 `json:"xpub_magic_segwit_native,omitempty"` Slip44 uint32 `json:"slip44,omitempty"` - AlternativeEstimateFee string `json:"alternativeEstimateFee,omitempty"` - AlternativeEstimateFeeParams string `json:"alternativeEstimateFeeParams,omitempty"` + AlternativeEstimateFee string `json:"alternative_estimate_fee,omitempty"` + AlternativeEstimateFeeParams string `json:"alternative_estimate_fee_params,omitempty"` MinimumCoinbaseConfirmations int `json:"minimumCoinbaseConfirmations,omitempty"` } diff --git a/blockbook.go b/blockbook.go index 444f60f4..08268081 100644 --- a/blockbook.go +++ b/blockbook.go @@ -6,9 +6,12 @@ import ( "blockbook/bchain/coins" "blockbook/common" "blockbook/db" + "blockbook/fiat" "blockbook/server" "context" + "encoding/json" "flag" + "io/ioutil" "log" "math/rand" "net/http" @@ -81,23 +84,24 @@ var ( ) var ( - chanSyncIndex = make(chan struct{}) - chanSyncMempool = make(chan struct{}) - chanStoreInternalState = make(chan struct{}) - chanSyncIndexDone = make(chan struct{}) - chanSyncMempoolDone = make(chan struct{}) - chanStoreInternalStateDone = make(chan struct{}) - chain bchain.BlockChain - mempool bchain.Mempool - index *db.RocksDB - txCache *db.TxCache - metrics *common.Metrics - syncWorker *db.SyncWorker - internalState *common.InternalState - callbacksOnNewBlock []bchain.OnNewBlockFunc - callbacksOnNewTxAddr []bchain.OnNewTxAddrFunc - chanOsSignal chan os.Signal - inShutdown int32 + chanSyncIndex = make(chan struct{}) + chanSyncMempool = make(chan struct{}) + chanStoreInternalState = make(chan struct{}) + chanSyncIndexDone = make(chan struct{}) + chanSyncMempoolDone = make(chan struct{}) + chanStoreInternalStateDone = make(chan struct{}) + chain bchain.BlockChain + mempool bchain.Mempool + index *db.RocksDB + txCache *db.TxCache + metrics *common.Metrics + syncWorker *db.SyncWorker + internalState *common.InternalState + callbacksOnNewBlock []bchain.OnNewBlockFunc + callbacksOnNewTxAddr []bchain.OnNewTxAddrFunc + callbacksOnNewFiatRatesTicker []fiat.OnNewFiatRatesTicker + chanOsSignal chan os.Signal + inShutdown int32 ) func init() { @@ -295,6 +299,7 @@ func mainWithExitCode() int { // start full public interface callbacksOnNewBlock = append(callbacksOnNewBlock, publicServer.OnNewBlock) callbacksOnNewTxAddr = append(callbacksOnNewTxAddr, publicServer.OnNewTxAddr) + callbacksOnNewFiatRatesTicker = append(callbacksOnNewFiatRatesTicker, publicServer.OnNewFiatRatesTicker) publicServer.ConnectFullPublicInterface() } @@ -317,6 +322,8 @@ func mainWithExitCode() int { } if internalServer != nil || publicServer != nil || chain != nil { + // start fiat rates downloader only if not shutting down immediately + initFiatRatesDownloader(index, *blockchain) waitForSignalAndShutdown(internalServer, publicServer, chain, 10*time.Second) } @@ -521,6 +528,12 @@ func onNewBlockHash(hash string, height uint32) { } } +func onNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) { + for _, c := range callbacksOnNewFiatRatesTicker { + c(ticker) + } +} + func syncMempoolLoop() { defer close(chanSyncMempoolDone) glog.Info("syncMempoolLoop starting") @@ -650,3 +663,34 @@ func computeFeeStats(stopCompute chan os.Signal, blockFrom, blockTo int, db *db. glog.Info("computeFeeStats finished in ", time.Since(start)) return err } + +func initFiatRatesDownloader(db *db.RocksDB, configfile string) { + data, err := ioutil.ReadFile(configfile) + if err != nil { + glog.Errorf("Error reading file %v, %v", configfile, err) + return + } + + var config struct { + FiatRates string `json:"fiat_rates"` + FiatRatesParams string `json:"fiat_rates_params"` + } + + err = json.Unmarshal(data, &config) + if err != nil { + glog.Errorf("Error parsing config file %v, %v", configfile, err) + return + } + + if config.FiatRates == "" || config.FiatRatesParams == "" { + glog.Infof("FiatRates config (%v) is empty, so the functionality is disabled.", configfile) + } else { + fiatRates, err := fiat.NewFiatRatesDownloader(db, config.FiatRates, config.FiatRatesParams, nil, onNewFiatRatesTicker) + if err != nil { + glog.Errorf("NewFiatRatesDownloader Init error: %v", err) + return + } + glog.Infof("Starting %v FiatRates downloader...", config.FiatRates) + go fiatRates.Run() + } +} diff --git a/build/tools/templates.go b/build/tools/templates.go index b8101ada..3e58aaa8 100644 --- a/build/tools/templates.go +++ b/build/tools/templates.go @@ -98,7 +98,7 @@ func jsonToString(msg json.RawMessage) (string, error) { } func generateRPCAuth(user, pass string) (string, error) { - cmd := exec.Command("/bin/bash", "-c", "build/scripts/rpcauth.py \"$0\" \"$1\" | sed -n -e 2p", user, pass) + cmd := exec.Command("/usr/bin/env", "bash", "-c", "build/scripts/rpcauth.py \"$0\" \"$1\" | sed -n -e 2p", user, pass) var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() diff --git a/configs/coins/bcash.json b/configs/coins/bcash.json index 99cb6599..440d3467 100644 --- a/configs/coins/bcash.json +++ b/configs/coins/bcash.json @@ -56,11 +56,14 @@ "block_addresses_to_keep": 300, "xpub_magic": 76067358, "slip44": 145, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin-cash\", \"periodSeconds\": 60}" + } } }, "meta": { "package_maintainer": "IT", "package_maintainer_email": "it@satoshilabs.com" } -} \ No newline at end of file +} diff --git a/configs/coins/bgold.json b/configs/coins/bgold.json index e518024c..8807de5f 100644 --- a/configs/coins/bgold.json +++ b/configs/coins/bgold.json @@ -252,11 +252,14 @@ "xpub_magic_segwit_p2sh": 77429938, "xpub_magic_segwit_native": 78792518, "slip44": 156, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin-gold\", \"periodSeconds\": 60}" + } } }, "meta": { "package_maintainer": "Jakub Matys", "package_maintainer_email": "jakub.matys@satoshilabs.com" } -} \ No newline at end of file +} diff --git a/configs/coins/bitcoin.json b/configs/coins/bitcoin.json index c9096f75..3362d51e 100644 --- a/configs/coins/bitcoin.json +++ b/configs/coins/bitcoin.json @@ -59,8 +59,10 @@ "xpub_magic_segwit_p2sh": 77429938, "xpub_magic_segwit_native": 78792518, "additional_params": { - "alternativeEstimateFee": "whatthefee-disabled", - "alternativeEstimateFeeParams": "{\"url\": \"https://whatthefee.io/data.json\", \"periodSeconds\": 60}" + "alternative_estimate_fee": "whatthefee-disabled", + "alternative_estimate_fee_params": "{\"url\": \"https://whatthefee.io/data.json\", \"periodSeconds\": 60}", + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}" } } }, diff --git a/configs/coins/bitcoin_testnet.json b/configs/coins/bitcoin_testnet.json index 2fa70af7..de057cf5 100644 --- a/configs/coins/bitcoin_testnet.json +++ b/configs/coins/bitcoin_testnet.json @@ -59,7 +59,10 @@ "xpub_magic_segwit_p2sh": 71979618, "xpub_magic_segwit_native": 73342198, "slip44": 1, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}" + } } }, "meta": { diff --git a/configs/coins/dash.json b/configs/coins/dash.json index b4e8ba65..fd8b7537 100644 --- a/configs/coins/dash.json +++ b/configs/coins/dash.json @@ -58,11 +58,14 @@ "block_addresses_to_keep": 300, "xpub_magic": 50221772, "slip44": 5, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"dash\", \"periodSeconds\": 60}" + } } }, "meta": { "package_maintainer": "IT Admin", "package_maintainer_email": "it@satoshilabs.com" } -} \ No newline at end of file +} diff --git a/configs/coins/digibyte.json b/configs/coins/digibyte.json index e9e01c3a..97f3d119 100644 --- a/configs/coins/digibyte.json +++ b/configs/coins/digibyte.json @@ -59,11 +59,14 @@ "xpub_magic_segwit_p2sh": 77429938, "xpub_magic_segwit_native": 78792518, "slip44": 20, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"digibyte\", \"periodSeconds\": 60}" + } } }, "meta": { "package_maintainer": "Martin Bohm", "package_maintainer_email": "martin.bohm@satoshilabs.com" } -} \ No newline at end of file +} diff --git a/configs/coins/dogecoin.json b/configs/coins/dogecoin.json index 7ed865c6..cd42ca65 100644 --- a/configs/coins/dogecoin.json +++ b/configs/coins/dogecoin.json @@ -60,11 +60,14 @@ "block_addresses_to_keep": 300, "xpub_magic": 49990397, "slip44": 3, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"dogecoin\", \"periodSeconds\": 60}" + } } }, "meta": { "package_maintainer": "IT Admin", "package_maintainer_email": "it@satoshilabs.com" } -} \ No newline at end of file +} diff --git a/configs/coins/ethereum-classic.json b/configs/coins/ethereum-classic.json index 77a7c8da..0e72b03d 100644 --- a/configs/coins/ethereum-classic.json +++ b/configs/coins/ethereum-classic.json @@ -63,7 +63,9 @@ "block_addresses_to_keep": 300, "additional_params": { "mempoolTxTimeoutHours": 48, - "queryBackendOnMempoolResync": true + "queryBackendOnMempoolResync": true, + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ethereum-classic\", \"periodSeconds\": 60}" } } }, @@ -71,4 +73,4 @@ "package_maintainer": "IT", "package_maintainer_email": "it@satoshilabs.com" } -} \ No newline at end of file +} diff --git a/configs/coins/ethereum.json b/configs/coins/ethereum.json index cafac864..9c530a2c 100644 --- a/configs/coins/ethereum.json +++ b/configs/coins/ethereum.json @@ -51,7 +51,9 @@ "block_addresses_to_keep": 300, "additional_params": { "mempoolTxTimeoutHours": 48, - "queryBackendOnMempoolResync": false + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ethereum\", \"periodSeconds\": 60}" } } }, diff --git a/configs/coins/ethereum_testnet_ropsten.json b/configs/coins/ethereum_testnet_ropsten.json index 9ed31ec8..27aa6feb 100644 --- a/configs/coins/ethereum_testnet_ropsten.json +++ b/configs/coins/ethereum_testnet_ropsten.json @@ -50,7 +50,9 @@ "block_addresses_to_keep": 300, "additional_params": { "mempoolTxTimeoutHours": 12, - "queryBackendOnMempoolResync": false + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ethereum\", \"periodSeconds\": 60}" } } }, diff --git a/configs/coins/litecoin.json b/configs/coins/litecoin.json index cfe51de5..121c5093 100644 --- a/configs/coins/litecoin.json +++ b/configs/coins/litecoin.json @@ -59,11 +59,14 @@ "xpub_magic_segwit_p2sh": 28471030, "xpub_magic_segwit_native": 78792518, "slip44": 2, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"litecoin\", \"periodSeconds\": 60}" + } } }, "meta": { "package_maintainer": "IT", "package_maintainer_email": "it@satoshilabs.com" } -} \ No newline at end of file +} diff --git a/configs/coins/namecoin.json b/configs/coins/namecoin.json index 7c2bc93b..441777b3 100644 --- a/configs/coins/namecoin.json +++ b/configs/coins/namecoin.json @@ -64,11 +64,14 @@ "block_addresses_to_keep": 300, "xpub_magic": 76067358, "slip44": 7, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"namecoin\", \"periodSeconds\": 60}" + } } }, "meta": { "package_maintainer": "IT Admin", "package_maintainer_email": "it@satoshilabs.com" } -} \ No newline at end of file +} diff --git a/configs/coins/vertcoin.json b/configs/coins/vertcoin.json index 1ce7fa77..216c744c 100644 --- a/configs/coins/vertcoin.json +++ b/configs/coins/vertcoin.json @@ -57,11 +57,14 @@ "xpub_magic_segwit_p2sh": 77429938, "xpub_magic_segwit_native": 78792518, "slip44": 28, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"vertcoin\", \"periodSeconds\": 60}" + } } }, "meta": { "package_maintainer": "Petr Kracik", "package_maintainer_email": "petr.kracik@satoshilabs.com" } -} \ No newline at end of file +} diff --git a/configs/coins/zcash.json b/configs/coins/zcash.json index fecc9586..4845c822 100644 --- a/configs/coins/zcash.json +++ b/configs/coins/zcash.json @@ -57,11 +57,14 @@ "block_addresses_to_keep": 300, "xpub_magic": 76067358, "slip44": 133, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"zcash\", \"periodSeconds\": 60}" + } } }, "meta": { "package_maintainer": "IT Admin", "package_maintainer_email": "it@satoshilabs.com" } -} \ No newline at end of file +} diff --git a/db/rocksdb.go b/db/rocksdb.go index 64a8815f..14438096 100644 --- a/db/rocksdb.go +++ b/db/rocksdb.go @@ -6,6 +6,7 @@ import ( "bytes" "encoding/binary" "encoding/hex" + "encoding/json" "fmt" "math/big" "os" @@ -30,6 +31,34 @@ const maxAddrDescLen = 1024 // when doing huge scan, it is better to close it and reopen from time to time to free the resources const refreshIterator = 5000000 +// FiatRatesTimeFormat is a format string for storing FiatRates timestamps in rocksdb +const FiatRatesTimeFormat = "20060102150405" // YYYYMMDDhhmmss + +// CurrencyRatesTicker contains coin ticker data fetched from API +type CurrencyRatesTicker struct { + Timestamp *time.Time // return as unix timestamp in API + Rates map[string]json.Number +} + +// ResultTickerAsString contains formatted CurrencyRatesTicker data +type ResultTickerAsString struct { + Timestamp string `json:"data_timestamp,omitempty"` + Rates map[string]json.Number `json:"rates,omitempty"` + Error string `json:"error,omitempty"` +} + +// ResultTickersAsString contains a formatted CurrencyRatesTicker list +type ResultTickersAsString struct { + Tickers []ResultTickerAsString `json:"tickers"` +} + +// ResultTickerListAsString contains formatted data about available currency tickers +type ResultTickerListAsString struct { + Timestamp string `json:"data_timestamp,omitempty"` + Tickers []string `json:"available_currencies"` + Error string `json:"error,omitempty"` +} + // RepairRocksDB calls RocksDb db repair function func RepairRocksDB(name string) error { glog.Infof("rocksdb: repair") @@ -77,6 +106,7 @@ const ( cfAddresses cfBlockTxs cfTransactions + cfFiatRates // BitcoinType cfAddressBalance cfTxAddresses @@ -86,7 +116,7 @@ const ( // common columns var cfNames []string -var cfBaseNames = []string{"default", "height", "addresses", "blockTxs", "transactions"} +var cfBaseNames = []string{"default", "height", "addresses", "blockTxs", "transactions", "fiatRates"} // type specific columns var cfNamesBitcoinType = []string{"addressBalance", "txAddresses"} @@ -99,7 +129,7 @@ func openDB(path string, c *gorocksdb.Cache, openFiles int) (*gorocksdb.DB, []*g // from documentation: if most of your queries are executed using iterators, you shouldn't set bloom filter optsAddresses := createAndSetDBOptions(0, c, openFiles) // default, height, addresses, blockTxids, transactions - cfOptions := []*gorocksdb.Options{opts, opts, optsAddresses, opts, opts} + cfOptions := []*gorocksdb.Options{opts, opts, optsAddresses, opts, opts, opts} // append type specific options count := len(cfNames) - len(cfOptions) for i := 0; i < count; i++ { @@ -146,6 +176,104 @@ func (d *RocksDB) closeDB() error { return nil } +// FiatRatesConvertDate checks if the date is in correct format and returns the Time object. +// Possible formats are: YYYYMMDDhhmmss, YYYYMMDDhhmm, YYYYMMDDhh, YYYYMMDD +func FiatRatesConvertDate(date string) (*time.Time, error) { + for format := FiatRatesTimeFormat; len(format) >= 8; format = format[:len(format)-2] { + convertedDate, err := time.Parse(format, date) + if err == nil { + return &convertedDate, nil + } + } + msg := "Date \"" + date + "\" does not match any of available formats. " + msg += "Possible formats are: YYYYMMDDhhmmss, YYYYMMDDhhmm, YYYYMMDDhh, YYYYMMDD" + return nil, errors.New(msg) +} + +// FiatRatesStoreTicker stores ticker data at the specified time +func (d *RocksDB) FiatRatesStoreTicker(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 + } + return nil +} + +// FiatRatesFindTicker gets FiatRates data closest to the specified timestamp +func (d *RocksDB) FiatRatesFindTicker(tickerTime *time.Time) (*CurrencyRatesTicker, error) { + ticker := &CurrencyRatesTicker{} + 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())) + if err != nil { + glog.Error("FiatRatesFindTicker time parse 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 + } + 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 +} + +// FiatRatesFindLastTicker gets the last FiatRates record +func (d *RocksDB) FiatRatesFindLastTicker() (*CurrencyRatesTicker, error) { + ticker := &CurrencyRatesTicker{} + 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())) + if err != nil { + glog.Error("FiatRatesFindTicker time parse 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 + } + 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 +} + // Close releases the RocksDB environment opened in NewRocksDB. func (d *RocksDB) Close() error { if d.db != nil { diff --git a/db/rocksdb_test.go b/db/rocksdb_test.go index 5a6137e5..4f5766b2 100644 --- a/db/rocksdb_test.go +++ b/db/rocksdb_test.go @@ -9,6 +9,7 @@ import ( "blockbook/tests/dbtestdata" "encoding/binary" "encoding/hex" + "encoding/json" "io/ioutil" "math/big" "os" @@ -16,6 +17,7 @@ import ( "sort" "strings" "testing" + "time" vlq "github.com/bsm/go-vlq" "github.com/juju/errors" @@ -1071,3 +1073,73 @@ func Test_packAddrBalance_unpackAddrBalance(t *testing.T) { }) } } + +func TestRocksTickers(t *testing.T) { + d := setupRocksDB(t, &testBitcoinParser{ + BitcoinParser: bitcoinTestnetParser(), + }) + defer closeAndDestroyRocksDB(t, d) + + // Test valid formats + for _, date := range []string{"20190130", "2019013012", "201901301250", "20190130125030"} { + _, err := FiatRatesConvertDate(date) + if err != nil { + t.Errorf("%v", err) + } + } + + // Test invalid formats + for _, date := range []string{"01102019", "10201901", "", "abc", "20190130xxx"} { + _, err := FiatRatesConvertDate(date) + if err == nil { + t.Errorf("Wrongly-formatted date \"%v\" marked as valid!", date) + } + } + + // Test storing & finding tickers + key, _ := time.Parse(FiatRatesTimeFormat, "20190627000000") + futureKey, _ := time.Parse(FiatRatesTimeFormat, "20190630000000") + + ts1, _ := time.Parse(FiatRatesTimeFormat, "20190628000000") + ticker1 := &CurrencyRatesTicker{ + Timestamp: &ts1, + Rates: map[string]json.Number{ + "usd": "20000", + }, + } + + ts2, _ := time.Parse(FiatRatesTimeFormat, "20190629000000") + ticker2 := &CurrencyRatesTicker{ + Timestamp: &ts2, + Rates: map[string]json.Number{ + "usd": "30000", + }, + } + d.FiatRatesStoreTicker(ticker1) + d.FiatRatesStoreTicker(ticker2) + + ticker, err := d.FiatRatesFindTicker(&key) // 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() // 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", ticker1.Timestamp, ticker.Timestamp) + } + + 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.") + } +} diff --git a/fiat/coingecko.go b/fiat/coingecko.go new file mode 100644 index 00000000..84139db6 --- /dev/null +++ b/fiat/coingecko.go @@ -0,0 +1,136 @@ +package fiat + +import ( + "blockbook/db" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "strconv" + "time" + + "github.com/golang/glog" +) + +// Coingecko is a structure that implements RatesDownloaderInterface +type Coingecko struct { + url string + coin string + httpTimeoutSeconds time.Duration + timeFormat string +} + +// NewCoinGeckoDownloader creates a coingecko structure that implements the RatesDownloaderInterface +func NewCoinGeckoDownloader(url string, coin string, timeFormat string) RatesDownloaderInterface { + return &Coingecko{ + url: url, + coin: coin, + httpTimeoutSeconds: 15 * time.Second, + timeFormat: timeFormat, + } +} + +// 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, + } + 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) + if err != nil { + return nil, err + } + return bodyBytes, 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 + } + 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]json.Number `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) + if err != nil { + glog.Error("Error getting market data: ", err) + return false, err + } + type FiatRatesResponse struct { + MarketData struct { + Prices map[string]interface{} `json:"current_price"` + } `json:"market_data"` + } + var data FiatRatesResponse + err = json.Unmarshal(resp, &data) + if err != nil { + glog.Errorf("Error parsing Coingecko response: %v", err) + return false, err + } + return len(data.MarketData.Prices) != 0, nil +} diff --git a/fiat/fiat_rates.go b/fiat/fiat_rates.go new file mode 100644 index 00000000..a3e01889 --- /dev/null +++ b/fiat/fiat_rates.go @@ -0,0 +1,214 @@ +package fiat + +import ( + "blockbook/db" + "encoding/json" + "errors" + "fmt" + "reflect" + "time" + + "github.com/golang/glog" +) + +// OnNewFiatRatesTicker is used to send notification about a new FiatRates ticker +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) +} + +// RatesDownloader stores FiatRates API parameters +type RatesDownloader struct { + periodSeconds time.Duration + 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 initiallizes 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) { + var rd = &RatesDownloader{} + type fiatRatesParams struct { + URL string `json:"url"` + Coin string `json:"coin"` + PeriodSeconds int `json:"periodSeconds"` + } + rdParams := &fiatRatesParams{} + err := json.Unmarshal([]byte(params), &rdParams) + if err != nil { + return nil, err + } + 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.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) + } 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. +func (rd *RatesDownloader) Run() error { + var timestamp *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) + } else { + minDate = currentDate + currentDate = currentDate.Add(maxDate.Sub(currentDate) / 2) + } + } +} + +// syncLatest downloads the latest FiatRates data every rd.PeriodSeconds +func (rd *RatesDownloader) syncLatest() error { + timer := time.NewTimer(rd.periodSeconds) + var lastTickerRates map[string]json.Number = nil + 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 +} diff --git a/fiat/fiat_rates_test.go b/fiat/fiat_rates_test.go new file mode 100644 index 00000000..a131cb4a --- /dev/null +++ b/fiat/fiat_rates_test.go @@ -0,0 +1,176 @@ +// +build unittest + +package fiat + +import ( + "blockbook/bchain" + "blockbook/bchain/coins/btc" + "blockbook/common" + "blockbook/db" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/golang/glog" + "github.com/martinboehm/btcutil/chaincfg" +) + +func TestMain(m *testing.M) { + // set the current directory to blockbook root so that ./static/ works + if err := os.Chdir(".."); err != nil { + glog.Fatal("Chdir error:", err) + } + c := m.Run() + chaincfg.ResetParams() + os.Exit(c) +} + +func setupRocksDB(t *testing.T, parser bchain.BlockChainParser) (*db.RocksDB, *common.InternalState, string) { + tmp, err := ioutil.TempDir("", "testdb") + if err != nil { + t.Fatal(err) + } + d, err := db.NewRocksDB(tmp, 100000, -1, parser, nil) + if err != nil { + t.Fatal(err) + } + is, err := d.LoadInternalState("fakecoin") + if err != nil { + t.Fatal(err) + } + d.SetInternalState(is) + return d, is, tmp +} + +func closeAndDestroyRocksDB(t *testing.T, db *db.RocksDB, dbpath string) { + // destroy db + if err := db.Close(); err != nil { + t.Fatal(err) + } + os.RemoveAll(dbpath) +} + +type testBitcoinParser struct { + *btc.BitcoinParser +} + +func bitcoinTestnetParser() *btc.BitcoinParser { + return btc.NewBitcoinParser( + btc.GetChainParams("test"), + &btc.Configuration{BlockAddressesToKeep: 1}) +} + +// getFiatRatesMockData reads a stub JSON response from a file and returns its content as string +func getFiatRatesMockData(dateParam string) (string, error) { + var filename string + if dateParam == "current" { + filename = "fiat/mock_data/current.json" + } else { + filename = "fiat/mock_data/" + dateParam + ".json" + } + mockFile, err := os.Open(filename) + if err != nil { + glog.Errorf("Cannot open file %v", filename) + return "", err + } + b, err := ioutil.ReadAll(mockFile) + if err != nil { + glog.Errorf("Cannot read file %v", filename) + return "", err + } + return string(b), nil +} + +func TestFiatRates(t *testing.T) { + d, _, tmp := setupRocksDB(t, &testBitcoinParser{ + BitcoinParser: bitcoinTestnetParser(), + }) + defer closeAndDestroyRocksDB(t, d, tmp) + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var err error + var mockData string + + 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 { + t.Errorf("Unknown URL path: %v", r.URL.Path) + } + + if err != nil { + t.Errorf("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}"}` + + type fiatRatesConfig struct { + FiatRates string `json:"fiat_rates"` + FiatRatesParams string `json:"fiat_rates_params"` + } + + var config fiatRatesConfig + err := json.Unmarshal([]byte(configJSON), &config) + if err != nil { + t.Errorf("Error parsing config: %v", err) + } + + if config.FiatRates == "" || config.FiatRatesParams == "" { + t.Errorf("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) + if err != nil { + t.Errorf("FiatRates init error: %v\n", err) + } + if config.FiatRates == "coingecko" { + timestamp, err := fiatRates.findEarliestMarketData() + if err != nil { + t.Errorf("Error looking up earliest market data: %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) + 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) + + err = fiatRates.syncHistorical(timestamp) + if err != nil { + t.Errorf("RatesDownloader syncHistorical error: %v", err) + return + } + 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 + } + err = fiatRates.db.FiatRatesStoreTicker(ticker) + if err != nil { + glog.Errorf("Sync StoreTicker error %v", err) + return + } + } +} diff --git a/fiat/mock_data/01-02-2013.json b/fiat/mock_data/01-02-2013.json new file mode 100644 index 00000000..94ba1bd5 --- /dev/null +++ b/fiat/mock_data/01-02-2013.json @@ -0,0 +1 @@ +{"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"}} diff --git a/fiat/mock_data/01-05-2013.json b/fiat/mock_data/01-05-2013.json new file mode 100644 index 00000000..060f0e08 --- /dev/null +++ b/fiat/mock_data/01-05-2013.json @@ -0,0 +1 @@ +{"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}} \ No newline at end of file diff --git a/fiat/mock_data/04-04-2013.json b/fiat/mock_data/04-04-2013.json new file mode 100644 index 00000000..94ba1bd5 --- /dev/null +++ b/fiat/mock_data/04-04-2013.json @@ -0,0 +1 @@ +{"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"}} diff --git a/fiat/mock_data/05-05-2013.json b/fiat/mock_data/05-05-2013.json new file mode 100644 index 00000000..1cd65273 --- /dev/null +++ b/fiat/mock_data/05-05-2013.json @@ -0,0 +1 @@ +{"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}} \ No newline at end of file diff --git a/fiat/mock_data/05-06-2013.json b/fiat/mock_data/05-06-2013.json new file mode 100644 index 00000000..d45b810a --- /dev/null +++ b/fiat/mock_data/05-06-2013.json @@ -0,0 +1 @@ +{"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}} \ No newline at end of file diff --git a/fiat/mock_data/07-10-2013.json b/fiat/mock_data/07-10-2013.json new file mode 100644 index 00000000..328a45dd --- /dev/null +++ b/fiat/mock_data/07-10-2013.json @@ -0,0 +1 @@ +{"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}} \ No newline at end of file diff --git a/fiat/mock_data/13-06-2014.json b/fiat/mock_data/13-06-2014.json new file mode 100644 index 00000000..c784ee48 --- /dev/null +++ b/fiat/mock_data/13-06-2014.json @@ -0,0 +1 @@ +{"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}} diff --git a/fiat/mock_data/20-04-2013.json b/fiat/mock_data/20-04-2013.json new file mode 100644 index 00000000..94ba1bd5 --- /dev/null +++ b/fiat/mock_data/20-04-2013.json @@ -0,0 +1 @@ +{"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"}} diff --git a/fiat/mock_data/20-11-2019.json b/fiat/mock_data/20-11-2019.json new file mode 100644 index 00000000..2dcf9060 --- /dev/null +++ b/fiat/mock_data/20-11-2019.json @@ -0,0 +1 @@ +{"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":{"aed":29895.556252804836,"ars":485421.07790578756,"aud":11927.653049612183,"bch":33.63953944857689,"bdt":690035.4308521386,"bhd":3068.738317969027,"bmd":8138.831605359057,"bnb":440.3065882935356,"brl":34128.562570752125,"btc":1.0,"cad":10801.69453000047,"chf":8061.919646688408,"clp":6426418.993942025,"cny":57197.26687298181,"czk":187745.75358926164,"dkk":54903.744126591664,"eos":2611.7345692770737,"eth":46.298470465960534,"eur":7346.923290157612,"gbp":6295.369969082021,"hkd":63709.552982009845,"huf":2444677.126964913,"idr":114676137.3195094,"ils":28177.44890091363,"inr":584864.5454373802,"jpy":882985.9102812062,"krw":9504690.325370407,"kwd":2470.900442397376,"lkr":1459699.318199838,"ltc":147.7002152423325,"mmk":12336753.62587893,"mxn":157572.5004020829,"myr":33838.006282440794,"nok":74323.81022013885,"nzd":12659.561898218928,"php":414372.5288556039,"pkr":1268614.8449705977,"pln":31481.39945227751,"rub":519856.47442806256,"sar":30523.100863736137,"sek":78433.10629768472,"sgd":11082.158667121108,"thb":245711.32616578983,"try":46424.77447078138,"twd":247819.28355157803,"uah":196944.8714820098,"usd":8138.831605359057,"vef":2022399076.2122076,"vnd":188819978.95240378,"xag":474.4301555409632,"xau":5.522685574132437,"xdr":5913.04021558867,"xlm":124534.24474263463,"xrp":32009.044210117067,"zar":120220.54543519647},"market_cap":{"aed":539216184347.0786,"ars":8754702929895.195,"aud":215108532402.1839,"bch":606965693.7308347,"bdt":12445939086798.977,"bhd":55349810272.20994,"bmd":146797393103.30997,"bnb":7956861576.178785,"brl":615565508500.11,"btc":18056975.0,"cad":194793828360.1895,"chf":145401203097.50433,"clp":115881862115752.81,"cny":1031662719251.4414,"czk":3386836054983.015,"dkk":990235760930.7228,"eos":47156656992.1783,"eth":836285546.5188296,"eur":132513272767.39243,"gbp":113531342257.38261,"hkd":1149073035824.1863,"huf":44093634990629.44,"idr":2068029594287882.2,"ils":508227254662.9701,"inr":10549007465796.953,"jpy":15924140811667.746,"krw":171432931613907.47,"kwd":44566807761.80631,"lkr":26328110104320.3,"ltc":2665915975.9376416,"mmk":222513913476766.62,"mxn":2842848955360.077,"myr":610324841566.3215,"nok":1340509754601.4958,"nzd":228301948107.3441,"php":7486962257826.343,"pkr":22881583146556.33,"pln":567829491818.596,"rub":9376404569427.018,"sar":550534997342.308,"sek":1414830338781.8389,"sgd":199869788618.91415,"thb":4430345323857.896,"try":837402059023.0033,"twd":4471888986106.136,"uah":3552229007857.6865,"usd":146797393103.30997,"vef":36477338099366760,"vnd":3405959412743900,"xag":8552645126.132073,"xau":99600563.24666482,"xdr":106651535632.2029,"xlm":2242611242035.2026,"xrp":577654290936.0109,"zar":2168300254311.0623},"total_volume":{"aed":91806191763.19203,"ars":1490678420139.214,"aud":36628601050.19057,"bch":103303580.75045899,"bdt":2119028144266.9175,"bhd":9423781116.770079,"bmd":24993518393.55118,"bnb":1352136442.5417123,"brl":104805320679.67813,"btc":3074333.834646516,"cad":33170897741.553368,"chf":24757329644.7321,"clp":19734874625492.48,"cny":175646949214.35953,"czk":576547982950.5977,"dkk":168603775731.05692,"eos":8020369404.536936,"eth":142177861.5523058,"eur":22561649053.858624,"gbp":19332436490.375057,"hkd":195645512956.95944,"huf":7507353106907.763,"idr":352158674165137.06,"ils":86530060030.31366,"inr":1796059125304.906,"jpy":2711559307275.5625,"krw":29187930650356.914,"kwd":7587882223.171772,"lkr":4482587123987.1,"ltc":453572235.5970587,"mmk":37884907025485.92,"mxn":483889012339.82043,"myr":103913052073.02832,"nok":228240809969.9092,"nzd":38876218172.28588,"php":1272495601815.3118,"pkr":3895786274927.592,"pln":96676153828.6573,"rub":1596425996462.3315,"sar":93733316998.92708,"sek":240860037406.81345,"sgd":34032174385.395035,"thb":754554320301.3098,"try":142565728216.80225,"twd":761027641565.2402,"uah":604797531952.8716,"usd":24993518393.55118,"vef":6210580456920606,"vnd":579846819033510.8,"xag":1456926423.0950127,"xau":16959601.841128074,"xdr":18158340970.319584,"xlm":382431912530.61053,"xrp":98296496845.90811,"zar":369184983706.85724}},"community_data":{"facebook_likes":null,"twitter_followers":68549,"reddit_average_posts_48h":6.429,"reddit_average_comments_48h":227.357,"reddit_subscribers":1190720,"reddit_accounts_active_48h":"3557.33333333333"},"developer_data":{"forks":24603,"stars":41204,"subscribers":3495,"total_issues":0,"closed_issues":0,"pull_requests_merged":6973,"pull_request_contributors":623,"code_additions_deletions_4_weeks":{"additions":3441,"deletions":-1615},"commit_count_4_weeks":375},"public_interest_stats":{"alexa_rank":12740,"bing_matches":135000000}} diff --git a/fiat/mock_data/21-11-2019.json b/fiat/mock_data/21-11-2019.json new file mode 100644 index 00000000..14dd021d --- /dev/null +++ b/fiat/mock_data/21-11-2019.json @@ -0,0 +1 @@ +{"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":{"aed":29737.733661544353,"ars":482992.0576847001,"aud":11909.083120440264,"bch":33.38939623106484,"bdt":686435.825992477,"bhd":3052.4408925589955,"bmd":8095.865638011639,"bnb":447.42498131132714,"brl":33973.49056335204,"btc":1.0,"cad":10770.529152304092,"chf":8017.651480082802,"clp":6417583.259568359,"cny":56962.51062904988,"czk":186532.80032847245,"dkk":54611.02597516125,"eos":2625.1996127062725,"eth":46.30756277128346,"eur":7307.587392569718,"gbp":6263.7712441296035,"hkd":63361.07803605232,"huf":2437260.3503234047,"idr":113658578.86366026,"ils":28110.059875022114,"inr":581104.954710677,"jpy":878241.5283779115,"krw":9474186.762883117,"kwd":2457.953382894158,"lkr":1451476.6944985148,"ltc":147.17764652439055,"mmk":12276458.59050864,"mxn":157745.51319696513,"myr":33723.328315137434,"nok":73919.30120786528,"nzd":12630.060434833318,"php":412223.4002195826,"pkr":1261966.2105007921,"pln":31386.456698725444,"rub":516701.6230282524,"sar":30360.961494224146,"sek":78027.18391192055,"sgd":11029.362072616914,"thb":244420.8060296636,"try":46147.243723230094,"twd":246632.45889225203,"uah":195582.93374510246,"usd":8095.865638011639,"vef":2011722564.2894442,"vnd":187451397.8769113,"xag":471.370523519991,"xau":5.491344703606915,"xdr":5886.050536922535,"xlm":126226.86768614998,"xrp":32338.763084294602,"zar":119758.9020368509},"market_cap":{"aed":536925789662.0078,"ars":8721126622799.587,"aud":214993230987.64774,"bch":603145132.8718755,"bdt":12393852945152.863,"bhd":55112950276.81427,"bmd":146173851045.95633,"bnb":8069562575.040651,"brl":613403948529.2512,"btc":18058775.0,"cad":194516467063.8744,"chf":144785199461.0198,"clp":115857415095711.94,"cny":1028479215959.3489,"czk":3368167110571.1367,"dkk":986119641838.593,"eos":47362660343.0749,"eth":834881213.8173289,"eur":131953912642.35466,"gbp":113092515946.4908,"hkd":1143920014822.8933,"huf":44007835435061.79,"idr":2051934847845294.5,"ils":507537536909.2167,"inr":10491950179817.375,"jpy":15862731500313.05,"krw":171059949186530.47,"kwd":44379258220.658554,"lkr":26206949031136.875,"ltc":2660325666.65285,"mmk":221656004387641.53,"mxn":2848592157028.2905,"myr":608887176531.9319,"nok":1334790467520.1284,"nzd":228006504250.86572,"php":7443249821227.297,"pkr":22785267089002.48,"pln":566719821025.2997,"rub":9329253695306.072,"sar":548178398889.3755,"sek":1408776800748.5935,"sgd":199092731818.5716,"thb":4413592261082.244,"try":833269884841.5155,"twd":4453040344437.87,"uah":3531322270240.796,"usd":146173851045.95633,"vef":36322395603696830,"vnd":3384688469578368,"xag":8512844964.182701,"xau":99193575.31978594,"xdr":106274821359.85637,"xlm":2281320453520.52,"xrp":583446527953.1724,"zar":2162554421914.3018},"total_volume":{"aed":86108153638.4696,"ars":1398544851555.271,"aud":34483769701.4642,"bch":96681855.22416703,"bdt":1987633699334.181,"bhd":8838603921.209757,"bmd":23442272034.865948,"bnb":1295557337.0497513,"brl":98373150367.11145,"btc":2896005.038733083,"cad":31186989216.112743,"chf":23215796244.73709,"clp":18582661731791.32,"cny":164939826037.3168,"czk":540121692261.5999,"dkk":158130900913.4299,"eos":7601490219.642489,"eth":134087512.35435177,"eur":21159744891.37511,"gbp":18137285873.37578,"hkd":183467425740.0729,"huf":7057295996096.396,"idr":329108145311632.75,"ils":81395084845.85982,"inr":1682639144253.6147,"jpy":2543023530910.265,"krw":27433318848801.867,"kwd":7117214443.4175005,"lkr":4202875028575.5767,"ltc":426165475.2614542,"mmk":35547536825741.08,"mxn":456765637917.7518,"myr":97648784161.23398,"nok":214039664814.34357,"nzd":36571421237.528984,"php":1193628145421.9192,"pkr":3654131198332.759,"pln":90882172338.37012,"rub":1496153783854.0442,"sar":87912763181.98567,"sek":225934390856.19467,"sgd":31936462095.3393,"thb":707741368511.1285,"try":133623294825.93925,"twd":714145398712.4277,"uah":566327128343.4884,"usd":23442272034.865948,"vef":5825114906715977,"vnd":542781570103440.4,"xag":1364893704.471942,"xau":15900658.698529225,"xdr":17043563229.317081,"xlm":365500701557.407,"xrp":93639657003.90553,"zar":346772153302.9578}},"community_data":{"facebook_likes":null,"twitter_followers":68537,"reddit_average_posts_48h":6.5,"reddit_average_comments_48h":248.25,"reddit_subscribers":1191683,"reddit_accounts_active_48h":"3672.76923076923"},"developer_data":{"forks":24612,"stars":41218,"subscribers":3495,"total_issues":0,"closed_issues":0,"pull_requests_merged":6978,"pull_request_contributors":623,"code_additions_deletions_4_weeks":{"additions":3491,"deletions":-1642},"commit_count_4_weeks":388},"public_interest_stats":{"alexa_rank":12740,"bing_matches":135000000}} diff --git a/fiat/mock_data/22-11-2019.json b/fiat/mock_data/22-11-2019.json new file mode 100644 index 00000000..19ad0808 --- /dev/null +++ b/fiat/mock_data/22-11-2019.json @@ -0,0 +1 @@ +{"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":{"aed":28035.679772629657,"ars":455975.92278356064,"aud":11242.023660019182,"bch":33.7483287902988,"bdt":647296.9519021783,"bhd":2877.7787238679057,"bmd":7632.494765498649,"bnb":454.38160859138037,"brl":32004.577050688924,"btc":1.0,"cad":10134.64789197728,"chf":7577.578965660877,"clp":6088438.929707239,"cny":53650.33220564306,"czk":176064.87038406474,"dkk":51541.93185162167,"eos":2702.457481762241,"eth":47.35816140456382,"eur":6897.386297149177,"gbp":5908.00889818188,"hkd":59688.78043936735,"huf":2307617.389787238,"idr":107442628.81392431,"ils":26414.69053433299,"inr":547635.3233044049,"jpy":828835.8421129878,"krw":8981385.56540522,"kwd":2317.3933256902314,"lkr":1371578.7340592865,"ltc":150.51551045178746,"mmk":11576409.193776581,"mxn":147997.88975040163,"myr":31823.686924746547,"nok":69791.7992730364,"nzd":11915.530263116301,"php":388189.40886026394,"pkr":1185305.8751410455,"pln":29644.4875492805,"rub":486228.079036091,"sar":28621.16844609101,"sek":73412.31132752451,"sgd":10402.327115898055,"thb":230471.15540126187,"try":43490.718423287806,"twd":232501.04791412465,"uah":184490.35082571974,"usd":7632.494765498649,"vef":1896580628.6955354,"vnd":177318106.98911732,"xag":446.02452345840845,"xau":5.21078050135358,"xdr":5544.7555748075,"xlm":125542.05293968241,"xrp":31338.87101002938,"zar":112082.42238187103},"market_cap":{"aed":505952466029.5935,"ars":8228063686842.731,"aud":202906746120.7657,"bch":610302833.79301,"bdt":11681596156197.934,"bhd":51934508235.10716,"bmd":137741605692.4732,"bnb":8208811080.573547,"brl":577564326829.1097,"btc":18060475.0,"cad":182927050731.8614,"chf":136752620963.6013,"clp":109834974835741.86,"cny":968213294733.5328,"czk":3177872948714.952,"dkk":930200055102.5531,"eos":48834956976.25193,"eth":855479661.9732217,"eur":124481359054.06459,"gbp":106626741157.78348,"hkd":1077223378894.6129,"huf":41647543632679.81,"idr":1939126324938642.2,"ils":476698903812.6255,"inr":9882973293887.5,"jpy":14957705316159.902,"krw":162084679666504.06,"kwd":41821381803.56006,"lkr":24752517095323.91,"ltc":2718657573.4072695,"mmk":208916381798492.3,"mxn":2670451606202.2573,"myr":574313624934.7668,"nok":1259581556794.9636,"nzd":215062993789.54813,"php":7005804182301.389,"pkr":21390900288155.363,"pln":535015393864.2815,"rub":8774828990639.0205,"sar":516518624602.2619,"sek":1324936091931.084,"sgd":187713296046.46274,"thb":4159796491912.691,"try":784893129459.0262,"twd":4195747188740.0435,"uah":3329448357091.939,"usd":137741605692.4732,"vef":34227086837012150,"vnd":3200663218868120.5,"xag":8055064362.341162,"xau":94058232.86316237,"xdr":100064731062.59404,"xlm":2269248253100.4473,"xrp":566093764804.5127,"zar":2022682035850.9631},"total_volume":{"aed":96938279018.82988,"ars":1576616710817.6833,"aud":38871268152.918,"bch":116690764.74068807,"bdt":2238142718151.254,"bhd":9950424571.517105,"bmd":26390689050.100677,"bnb":1571104089.9267807,"brl":110661437324.88211,"btc":3458209.3682739506,"cad":35042322250.70604,"chf":26200808042.38517,"clp":21051845239481.676,"cny":185505431470.96753,"czk":608775163260.6022,"dkk":178215267527.76758,"eos":9344220633.428228,"eth":163749147.56075427,"eur":23848922615.61833,"gbp":20427976766.120914,"hkd":206384425112.9548,"huf":7978991778122.924,"idr":371501729758266.6,"ils":91333424478.36926,"inr":1893545161079.935,"jpy":2865845264861.2,"krw":31054715525924.953,"kwd":8012793790.76967,"lkr":4742473986606.729,"ltc":520433771.07914567,"mmk":40027464772158.91,"mxn":511728656025.9774,"myr":110035977994.39453,"nok":241317384348.23724,"nzd":41200035336.076935,"php":1342232952203.5798,"pkr":4098402912964.3467,"pln":102501014019.56622,"rub":1681218845936.662,"sar":98962708775.86293,"sek":253835939652.59772,"sgd":35967870106.38203,"thb":796894434137.848,"try":150376785276.3785,"twd":803913143453.4763,"uah":637907739340.2526,"usd":26390689050.100677,"vef":6557760099174900,"vnd":613108448584249.1,"xag":1542208002.620382,"xau":18017187.321394224,"xdr":19171964702.159466,"xlm":434083662502.9747,"xrp":108359641954.22104,"zar":387544629631.8232}},"community_data":{"facebook_likes":null,"twitter_followers":68552,"reddit_average_posts_48h":8.0,"reddit_average_comments_48h":363.727,"reddit_subscribers":1192543,"reddit_accounts_active_48h":"4102.33333333333"},"developer_data":{"forks":24627,"stars":41240,"subscribers":3497,"total_issues":0,"closed_issues":0,"pull_requests_merged":6982,"pull_request_contributors":623,"code_additions_deletions_4_weeks":{"additions":3706,"deletions":-1803},"commit_count_4_weeks":370},"public_interest_stats":{"alexa_rank":12740,"bing_matches":135000000}} \ No newline at end of file diff --git a/fiat/mock_data/23-09-2011.json b/fiat/mock_data/23-09-2011.json new file mode 100644 index 00000000..94ba1bd5 --- /dev/null +++ b/fiat/mock_data/23-09-2011.json @@ -0,0 +1 @@ +{"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"}} diff --git a/fiat/mock_data/27-04-2013.json b/fiat/mock_data/27-04-2013.json new file mode 100644 index 00000000..94ba1bd5 --- /dev/null +++ b/fiat/mock_data/27-04-2013.json @@ -0,0 +1 @@ +{"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"}} diff --git a/fiat/mock_data/28-04-2013.json b/fiat/mock_data/28-04-2013.json new file mode 100644 index 00000000..b9f2a4ad --- /dev/null +++ b/fiat/mock_data/28-04-2013.json @@ -0,0 +1 @@ +{"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}} diff --git a/fiat/mock_data/29-04-2013.json b/fiat/mock_data/29-04-2013.json new file mode 100644 index 00000000..d7f64132 --- /dev/null +++ b/fiat/mock_data/29-04-2013.json @@ -0,0 +1 @@ +{"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}} diff --git a/fiat/mock_data/current.json b/fiat/mock_data/current.json new file mode 100644 index 00000000..7d66dd3e --- /dev/null +++ b/fiat/mock_data/current.json @@ -0,0 +1 @@ +{"id":"bitcoin","symbol":"btc","name":"Bitcoin","asset_platform_id":null,"block_time_in_minutes":10,"categories":["Cryptocurrency"],"description":{"en":"Bitcoin is the first successful internet money based on peer-to-peer technology; whereby no central bank or authority is involved in the transaction and production of the Bitcoin currency. It was created by an anonymous individual/group under the name, Satoshi Nakamoto. The source code is available publicly as an open source project, anybody can look at it and be part of the developmental process.\r\n\r\nBitcoin is changing the way we see money as we speak. The idea was to produce a means of exchange, independent of any central authority, that could be transferred electronically in a secure, verifiable and immutable way. It is a decentralized peer-to-peer internet currency making mobile payment easy, very low transaction fees, protects your identity, and it works anywhere all the time with no central authority or banks.\r\n\r\nBitcoin is design to have only 21 million BTC ever created, thus making it a deflationary currency. Bitcoin uses the \u003ca href=\"https://www.coingecko.com/en?hashing_algorithm=SHA-256\"\u003eSHA-256\u003c/a\u003e hashing algorithm with an average transaction confirmation time of 10 minutes. Miners today are mining Bitcoin using ASIC chip dedicated to only mining Bitcoin, and the hash rate has shot up to peta hashes.\r\n\r\nBeing the first successful online cryptography currency, Bitcoin has inspired other alternative currencies such as \u003ca href=\"https://www.coingecko.com/en/coins/litecoin\"\u003eLitecoin\u003c/a\u003e, \u003ca href=\"https://www.coingecko.com/en/coins/peercoin\"\u003ePeercoin\u003c/a\u003e, \u003ca href=\"https://www.coingecko.com/en/coins/primecoin\"\u003ePrimecoin\u003c/a\u003e, and so on.\r\n\r\nThe cryptocurrency then took off with the innovation of the turing-complete smart contract by \u003ca href=\"https://www.coingecko.com/en/coins/ethereum\"\u003eEthereum\u003c/a\u003e which led to the development of other amazing projects such as \u003ca href=\"https://www.coingecko.com/en/coins/eos\"\u003eEOS\u003c/a\u003e, \u003ca href=\"https://www.coingecko.com/en/coins/tron\"\u003eTron\u003c/a\u003e, and even crypto-collectibles such as \u003ca href=\"https://www.coingecko.com/buzz/ethereum-still-king-dapps-cryptokitties-need-1-billion-on-eos\"\u003eCryptoKitties\u003c/a\u003e."},"links":{"homepage":["http://www.bitcoin.org","",""],"blockchain_site":["https://blockchair.com/bitcoin/","https://btc.com/","","",""],"official_forum_url":["https://bitcointalk.org/","",""],"chat_url":["","",""],"announcement_url":["",""],"twitter_screen_name":"btc","facebook_username":"bitcoins","bitcointalk_thread_identifier":null,"telegram_channel_identifier":"","subreddit_url":"https://www.reddit.com/r/Bitcoin/","repos_url":{"github":["https://github.com/bitcoin/bitcoin","https://github.com/bitcoin/bips"],"bitbucket":[]}},"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","large":"https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579"},"country_origin":"","genesis_date":"2009-01-03","sentiment_votes_up_percentage":38.69,"sentiment_votes_down_percentage":61.31,"market_cap_rank":1,"coingecko_rank":1,"coingecko_score":87.571,"developer_score":91.999,"community_score":80.5,"liquidity_score":100.084,"public_interest_score":44.29,"market_data":{"current_price":{"aed":26233,"ars":427146,"aud":10530.78,"bch":34.754373,"bdt":605978,"bhd":2693.12,"bmd":7142.59,"bnb":469.136,"brl":29879,"btc":1.0,"cad":9489.19,"chf":7113.95,"clp":5688362,"cny":50277,"czk":165074,"dkk":48362,"eos":2760,"eth":48.466911,"eur":6471.78,"gbp":5562.63,"hkd":55896,"huf":2162211,"idr":100796288,"ils":24797,"inr":512355,"jpy":776361,"krw":8423047,"kwd":2169.53,"lkr":1286409,"ltc":152.386,"mmk":10823325,"mxn":138495,"myr":29800,"nok":65379,"nzd":11149.85,"php":363629,"pkr":1110673,"pln":27816,"rub":455640,"sar":26785,"sek":68693,"sgd":9743.25,"thb":215688,"try":40799,"twd":218342,"uah":172189,"usd":7142.59,"vef":1774846373,"vnd":166109525,"xag":418.79,"xau":4.88,"xdr":5191.74,"xlm":125849,"xrp":30745,"zar":104995},"roi":null,"ath":{"aed":72229,"ars":654921,"aud":25717,"bch":42.242547,"bdt":1631248,"bhd":7416.37,"bmd":19665.39,"bnb":143062,"brl":64777,"btc":1.003301,"cad":25303,"chf":19484.57,"clp":12582805,"cny":130006,"czk":429834,"dkk":124584,"eos":3287,"eth":624.203,"eur":16727.68,"gbp":14759.86,"hkd":153608,"huf":5263109,"idr":266681922,"ils":69096,"inr":1259942,"jpy":2214028,"krw":21418073,"kwd":5939.64,"lkr":3024387,"ltc":318.98,"mmk":26871485,"mxn":376059,"myr":80224,"nok":164805,"nzd":28131,"php":991988,"pkr":2181203,"pln":70407,"rub":1157051,"sar":73750,"sek":167278,"sgd":26517,"thb":639578,"try":80284,"twd":589706,"uah":541437,"usd":19665.39,"vef":3454441855,"vnd":446720468,"xag":1225.45,"xau":15.67,"xdr":13907.05,"xlm":189028,"xrp":42151,"zar":257660},"ath_change_percentage":{"aed":-63.62124,"ars":-34.67198,"aud":-58.99662,"bch":-17.39149,"bdt":-62.79247,"bhd":-63.62871,"bmd":-63.62129,"bnb":-99.67103,"brl":-53.8445,"btc":-0.32896,"cad":-62.45264,"chf":-63.4543,"clp":-54.7089,"cny":-61.26497,"czk":-61.55431,"dkk":-61.13905,"eos":-16.10009,"eth":-92.21431,"eur":-61.26899,"gbp":-62.27369,"hkd":-63.55334,"huf":-58.87639,"idr":-62.15657,"ils":-64.07374,"inr":-59.26992,"jpy":-64.90022,"krw":-60.60644,"kwd":-63.4152,"lkr":-57.39745,"ltc":-52.11769,"mmk":-59.65748,"mxn":-63.10475,"myr":-62.79525,"nok":-60.29271,"nzd":-60.32548,"php":-63.31142,"pkr":-48.99833,"pln":-60.4529,"rub":-60.55354,"sar":-63.62382,"sek":-58.87165,"sgd":-63.20581,"thb":-66.23726,"try":-49.0973,"twd":-62.92736,"uah":-68.14693,"usd":-63.62129,"vef":-48.53915,"vnd":-62.77453,"xag":-65.86833,"xau":-68.86202,"xdr":-62.60861,"xlm":-33.15539,"xrp":-27.01399,"zar":-59.19278},"ath_date":{"aed":"2017-12-16T00:00:00.000Z","ars":"2019-08-13T13:41:31.186Z","aud":"2017-12-16T00:00:00.000Z","bch":"2018-12-15T16:19:57.060Z","bdt":"2017-12-16T00:00:00.000Z","bhd":"2017-12-16T00:00:00.000Z","bmd":"2017-12-16T00:00:00.000Z","bnb":"2017-10-19T00:00:00.000Z","brl":"2017-12-16T00:00:00.000Z","btc":"2019-10-15T16:00:56.136Z","cad":"2017-12-16T00:00:00.000Z","chf":"2017-12-16T00:00:00.000Z","clp":"2017-12-16T00:00:00.000Z","cny":"2017-12-16T00:00:00.000Z","czk":"2017-12-16T00:00:00.000Z","dkk":"2017-12-16T00:00:00.000Z","eos":"2019-09-05T20:22:13.572Z","eth":"2015-10-20T00:00:00.000Z","eur":"2017-12-16T00:00:00.000Z","gbp":"2017-12-16T00:00:00.000Z","hkd":"2017-12-16T00:00:00.000Z","huf":"2017-12-16T00:00:00.000Z","idr":"2017-12-16T00:00:00.000Z","ils":"2017-12-16T00:00:00.000Z","inr":"2017-12-16T00:00:00.000Z","jpy":"2017-12-16T00:00:00.000Z","krw":"2017-12-16T00:00:00.000Z","kwd":"2017-12-16T00:00:00.000Z","lkr":"2017-12-16T00:00:00.000Z","ltc":"2017-03-05T00:00:00.000Z","mmk":"2017-12-16T00:00:00.000Z","mxn":"2017-12-16T00:00:00.000Z","myr":"2017-12-16T00:00:00.000Z","nok":"2017-12-16T00:00:00.000Z","nzd":"2017-12-16T00:00:00.000Z","php":"2017-12-16T00:00:00.000Z","pkr":"2019-06-26T19:55:29.614Z","pln":"2017-12-16T00:00:00.000Z","rub":"2017-12-16T00:00:00.000Z","sar":"2017-12-16T00:00:00.000Z","sek":"2017-12-16T00:00:00.000Z","sgd":"2017-12-16T00:00:00.000Z","thb":"2017-12-16T00:00:00.000Z","try":"2019-06-26T19:55:29.614Z","twd":"2017-12-16T00:00:00.000Z","uah":"2017-12-16T00:00:00.000Z","usd":"2017-12-16T00:00:00.000Z","vef":"2019-06-26T19:55:29.614Z","vnd":"2017-12-16T00:00:00.000Z","xag":"2017-12-16T00:00:00.000Z","xau":"2017-12-16T00:00:00.000Z","xdr":"2017-12-16T00:00:00.000Z","xlm":"2019-09-12T22:33:57.455Z","xrp":"2019-09-06T12:53:39.935Z","zar":"2017-12-16T00:00:00.000Z"},"market_cap":{"aed":474793759318,"ars":7730972325788,"aud":190537867813,"bch":630551829,"bdt":10967221735647,"bhd":48741175024,"bmd":129269449023,"bnb":8504056047,"brl":540242881359,"btc":18061500,"cad":171672542961,"chf":128668733894,"clp":102976043092026,"cny":909940578620,"czk":2986033783826,"dkk":874823757177,"eos":49831754917,"eth":878149440,"eur":117068610616,"gbp":100617263456,"hkd":1011622634528,"huf":39109243612989,"idr":1823601645999521,"ils":448552061166,"inr":9272819588645,"jpy":14042144114110,"krw":152458449136441,"kwd":39265078063,"lkr":23281924163795,"ltc":2759842022,"mmk":195884749415125,"mxn":2507103402139,"myr":539325068270,"nok":1182462552968,"nzd":201673138152,"php":6576324680166,"pkr":20101399323136,"pln":503125744460,"rub":8247196943518,"sar":484759011874,"sek":1243159730063,"sgd":176297674578,"thb":3901906020384,"try":738440868912,"twd":3950345221975,"uah":3116344627531,"usd":129269449023,"vef":32121860601613224,"vnd":3004844249179323,"xag":7557859850,"xau":88142374,"xdr":93962084412,"xlm":2283168044885,"xrp":555894381400,"zar":1899894292486},"market_cap_rank":1,"total_volume":{"aed":138687050476,"ars":2258211477718,"aud":55673594578,"bch":183737598,"bdt":3203651532313,"bhd":14237857483,"bmd":37761091954,"bnb":2480204906,"brl":157962199864,"btc":5303296,"cad":50166932300,"chf":37609632215,"clp":30072933632557,"cny":265804102377,"czk":872704148379,"dkk":255678541091,"eos":14590103457,"eth":256232321,"eur":34214645720,"gbp":29408225131,"hkd":295508865363,"huf":11431065850716,"idr":532884529661782,"ils":131094616522,"inr":2708689636557,"jpy":4104423009447,"krw":44530522909173,"kwd":11469780637,"lkr":6800917663597,"ltc":805626595,"mmk":57220186912138,"mxn":732187572998,"myr":157543051743,"nok":345640000147,"nzd":58946461701,"php":1922417191403,"pkr":5871849798923,"pln":147054530842,"rub":2408855577961,"sar":141607455567,"sek":363160165026,"sgd":51510094341,"thb":1140290574296,"try":215692490077,"twd":1154318782196,"uah":910320086696,"usd":37761091954,"vef":9383164708217112,"vnd":878179122153079,"xag":2214048370,"xau":25773833,"xdr":27447404909,"xlm":665333514482,"xrp":162538792944,"zar":555079555485},"high_24h":{"aed":28288,"ars":460171,"aud":11341.92,"bch":35.462898,"bdt":653121,"bhd":2902.95,"bmd":7701.17,"bnb":479.999,"brl":32313,"btc":1.0,"cad":10224.26,"chf":7651.19,"clp":6150290,"cny":54135,"czk":177709,"dkk":52023,"eos":2804,"eth":49.32148,"eur":6961.83,"gbp":5962.03,"hkd":60252,"huf":2329092,"idr":108526800,"ils":26652,"inr":552563,"jpy":836775,"krw":9070591,"kwd":2338.72,"lkr":1386522,"ltc":155.932,"mmk":11702598,"mxn":149346,"myr":32102,"nok":70413,"nzd":12023.03,"php":391391,"pkr":1198463,"pln":29923,"rub":490607,"sar":28878,"sek":74080,"sgd":10492.07,"thb":232716,"try":43892,"twd":235078,"uah":186500,"usd":7701.17,"vef":1913645346,"vnd":178999196,"xag":450.44,"xau":5.26,"xdr":5594.65,"xlm":128230,"xrp":31649,"zar":113045},"low_24h":{"aed":25389,"ars":413252,"aud":10177.28,"bch":33.498779,"bdt":586479,"bhd":2606.33,"bmd":6912.76,"bnb":441.91,"brl":28872,"btc":1.0,"cad":9172.15,"chf":6875.83,"clp":5495871,"cny":48647,"czk":159479,"dkk":46725,"eos":2688,"eth":47.221694,"eur":6252.27,"gbp":5375.94,"hkd":54096,"huf":2089289,"idr":97454723,"ils":23978,"inr":495700,"jpy":750674,"krw":8145203,"kwd":2099.06,"lkr":1245015,"ltc":149.451,"mmk":10475054,"mxn":133891,"myr":28843,"nok":63132,"nzd":10775.68,"php":351479,"pkr":1073206,"pln":26857,"rub":440210,"sar":25927,"sek":66395,"sgd":9421.43,"thb":208711,"try":39550,"twd":211136,"uah":166648,"usd":6912.76,"vef":1717735697,"vnd":160482399,"xag":404.21,"xau":4.71,"xdr":5021.89,"xlm":124993,"xrp":30492,"zar":101450},"price_change_24h":-480.5673621733,"price_change_percentage_24h":-6.30404,"price_change_percentage_7d":-17.44599,"price_change_percentage_14d":-22.54054,"price_change_percentage_30d":-11.22023,"price_change_percentage_60d":-28.86786,"price_change_percentage_200d":24.23364,"price_change_percentage_1y":54.82116,"market_cap_change_24h":-9006537915.032,"market_cap_change_percentage_24h":-6.51345,"price_change_24h_in_currency":{"aed":-1765.3849375,"ars":-28250.7846463,"aud":-687.42268421,"bch":1.209693,"bdt":-40527.63640467,"bhd":-180.62666736,"bmd":-480.56736217,"bnb":13.500179,"brl":-2234.43033708,"btc":0.0,"cad":-631.36102032,"chf":-447.60591608,"clp":-374343.553352,"cny":-3307.29242038,"czk":-10750.48677935,"dkk":-3087.31721729,"eos":64.38,"eth":1.189393,"eur":-412.89174814,"gbp":-335.73696182,"hkd":-3726.49580785,"huf":-136103.17232295,"idr":-6721091.02167486,"ils":-1585.24792676,"inr":-34656.74698489,"jpy":-51434.05221209,"krw":-545602.5158788,"kwd":-145.59687908,"lkr":-83492.89071827,"ltc":0.41120093,"mmk":-738928.15288522,"mxn":-9373.69014498,"myr":-1954.66222593,"nok":-4225.97032316,"nzd":-741.18047963,"php":-24202.68533704,"pkr":-74347.06674375,"pln":-1774.3736239,"rub":-30122.73186387,"sar":-1801.65200366,"sek":-4761.52882112,"sgd":-641.55584025,"thb":-14530.99082294,"try":-2654.49777029,"twd":-14416.02940028,"uah":-12075.69031495,"usd":-480.56736217,"vef":-119415050.76443768,"vnd":-10911575.70808423,"xag":-25.13762803,"xau":-0.3155855,"xdr":-347.48043979,"xlm":-1338.794227113,"xrp":-784.6183422333,"zar":-7137.11714324},"price_change_percentage_1h_in_currency":{"aed":0.84905,"ars":0.86161,"aud":0.8867,"bch":-1.38159,"bdt":0.85317,"bhd":0.85317,"bmd":0.85317,"bnb":-1.51553,"brl":0.91348,"btc":0.0,"cad":0.915,"chf":0.88072,"clp":0.78989,"cny":0.87467,"czk":0.97735,"dkk":0.94907,"eos":-0.63738,"eth":-1.42231,"eur":0.94977,"gbp":0.91433,"hkd":0.86091,"huf":0.9903,"idr":0.88666,"ils":0.8754,"inr":0.90239,"jpy":0.87893,"krw":0.96319,"kwd":0.86978,"lkr":0.85317,"ltc":-1.45742,"mmk":0.85317,"mxn":0.85889,"myr":0.84592,"nok":1.07686,"nzd":0.85963,"php":1.01808,"pkr":0.85317,"pln":0.98571,"rub":0.95446,"sar":0.85328,"sek":0.92645,"sgd":0.9031,"thb":0.87489,"try":0.72911,"twd":0.89938,"uah":0.85317,"usd":0.85317,"vef":0.85317,"vnd":0.95458,"xag":1.08837,"xau":1.10648,"xdr":0.85317,"xlm":-1.37016,"xrp":0.15375,"zar":1.04254},"price_change_percentage_24h_in_currency":{"aed":-6.30532,"ars":-6.20356,"aud":-6.12774,"bch":3.60621,"bdt":-6.26872,"bhd":-6.2854,"bmd":-6.30404,"bnb":2.96293,"brl":-6.95795,"btc":0.0,"cad":-6.23841,"chf":-5.9195,"clp":-6.17453,"cny":-6.17208,"czk":-6.11433,"dkk":-6.00068,"eos":2.38856,"eth":2.51577,"eur":-5.99726,"gbp":-5.69203,"hkd":-6.25013,"huf":-5.92187,"idr":-6.25117,"ils":-6.0088,"inr":-6.33565,"jpy":-6.21338,"krw":-6.08344,"kwd":-6.28893,"lkr":-6.09481,"ltc":0.27057,"mmk":-6.39087,"mxn":-6.3392,"myr":-6.15559,"nok":-6.0714,"nzd":-6.2331,"php":-6.24051,"pkr":-6.27391,"pln":-5.99652,"rub":-6.20112,"sar":-6.30234,"sek":-6.48232,"sgd":-6.17783,"thb":-6.3118,"try":-6.10886,"twd":-6.19357,"uah":-6.55345,"usd":-6.30404,"vef":-6.30404,"vnd":-6.164,"xag":-5.66252,"xau":-6.07975,"xdr":-6.2731,"xlm":-1.05261,"xrp":-2.48855,"zar":-6.36494},"price_change_percentage_7d_in_currency":{"aed":-17.4561,"ars":-17.17189,"aud":-17.39343,"bch":11.54617,"bdt":-17.34556,"bhd":-17.42365,"bmd":-17.44599,"bnb":15.37505,"brl":-17.6939,"btc":0.0,"cad":-17.22355,"chf":-16.83939,"clp":-18.16528,"cny":-17.23081,"czk":-17.84696,"dkk":-17.5639,"eos":8.28045,"eth":3.50175,"eur":-17.56998,"gbp":-17.18365,"hkd":-17.46919,"huf":-17.62502,"idr":-17.26247,"ils":-17.71536,"inr":-17.71385,"jpy":-17.2919,"krw":-16.68652,"kwd":-17.43375,"lkr":-17.57364,"ltc":3.79137,"mmk":-17.47497,"mxn":-17.18117,"myr":-17.25559,"nok":-17.57382,"nzd":-17.76103,"php":-17.19551,"pkr":-17.66361,"pln":-17.28903,"rub":-17.622,"sar":-17.43643,"sek":-17.99201,"sgd":-17.30006,"thb":-17.44962,"try":-17.99541,"twd":-17.11248,"uah":-17.8811,"usd":-17.44599,"vef":-17.44599,"vnd":-17.01601,"xag":-17.68437,"xau":-17.19364,"xdr":-17.64438,"xlm":7.48208,"xrp":-4.57226,"zar":-18.1527},"price_change_percentage_14d_in_currency":{"aed":-22.55003,"ars":-22.26953,"aud":-21.20934,"bch":10.18823,"bdt":-22.4719,"bhd":-22.52698,"bmd":-22.54054,"bnb":3.78256,"brl":-20.98795,"btc":0.0,"cad":-21.89472,"chf":-22.44172,"clp":-16.95103,"cny":-21.8595,"czk":-22.52847,"dkk":-22.42537,"eos":3.89877,"eth":-1.80404,"eur":-22.42496,"gbp":-22.65386,"hkd":-22.54945,"huf":-22.08321,"idr":-21.79502,"ils":-23.09687,"inr":-21.7934,"jpy":-22.93533,"krw":-20.89782,"kwd":-22.52728,"lkr":-22.87826,"ltc":1.53118,"mmk":-22.72098,"mxn":-21.51393,"myr":-21.70354,"nok":-22.15384,"nzd":-22.92904,"php":-21.95551,"pkr":-22.80961,"pln":-21.72901,"rub":-22.2441,"sar":-22.54442,"sek":-22.62979,"sgd":-22.15219,"thb":-23.10963,"try":-23.08949,"twd":-21.78058,"uah":-23.91091,"usd":-22.54054,"vef":-22.54054,"vnd":-22.53941,"xag":-22.26463,"xau":-22.33687,"xdr":-22.55567,"xlm":1.69618,"xrp":-2.72537,"zar":-22.70147},"price_change_percentage_30d_in_currency":{"aed":-11.22443,"ars":-9.51444,"aud":-10.193,"bch":-1.85067,"bdt":-11.07576,"bhd":-11.21198,"bmd":-11.22023,"bnb":6.12033,"brl":-8.97239,"btc":0.0,"cad":-9.91713,"chf":-10.62881,"clp":-2.46349,"cny":-11.69817,"czk":-10.69528,"dkk":-10.45514,"eos":-0.60245,"eth":3.31477,"eur":-10.48102,"gbp":-10.92388,"hkd":-11.40589,"huf":-9.11838,"idr":-11.12997,"ils":-12.76593,"inr":-10.10704,"jpy":-11.04788,"krw":-10.68933,"kwd":-11.09524,"lkr":-11.91065,"ltc":1.16327,"mmk":-12.33699,"mxn":-10.07551,"myr":-11.44098,"nok":-11.18008,"nzd":-11.13426,"php":-11.70222,"pkr":-11.68762,"pln":-9.93606,"rub":-11.10484,"sar":-11.23433,"sek":-11.4895,"sgd":-11.12778,"thb":-11.50173,"try":-12.60444,"twd":-10.99896,"uah":-14.02182,"usd":-11.22023,"vef":-11.22023,"vnd":-11.12455,"xag":-8.57976,"xau":-9.77676,"xdr":-11.09094,"xlm":-2.0601,"xrp":11.18539,"zar":-10.5719},"price_change_percentage_60d_in_currency":{"aed":-28.87271,"ars":-24.94049,"aud":-28.98967,"bch":6.58558,"bdt":-28.55284,"bhd":-28.85919,"bmd":-28.86786,"bnb":-4.73332,"brl":-28.29542,"btc":0.0,"cad":-28.7742,"chf":-28.5492,"clp":-21.05412,"cny":-29.39446,"czk":-29.96813,"dkk":-28.93541,"eos":4.89171,"eth":2.15282,"eur":-28.98347,"gbp":-30.91919,"hkd":-29.00032,"huf":-28.93845,"idr":-28.57896,"ils":-29.88395,"inr":-28.33674,"jpy":-28.19392,"krw":-29.75392,"kwd":-28.86693,"lkr":-29.26237,"ltc":10.0907,"mmk":-29.68127,"mxn":-29.02416,"myr":-28.91758,"nok":-28.16281,"nzd":-30.41575,"php":-30.43231,"pkr":-29.27026,"pln":-30.19557,"rub":-29.14377,"sar":-28.88798,"sek":-29.4752,"sgd":-29.51848,"thb":-29.55026,"try":-29.23667,"twd":-29.86135,"uah":-29.77018,"usd":-28.86786,"vef":-28.86786,"vnd":-28.86503,"xag":-24.99432,"xau":-26.49988,"xdr":-29.12285,"xlm":-13.83671,"xrp":-14.54016,"zar":-29.92383},"price_change_percentage_200d_in_currency":{"aed":24.22488,"ars":66.85932,"aud":27.95174,"bch":76.94309,"bdt":24.91123,"bhd":24.2366,"bmd":24.23364,"bnb":86.35415,"brl":31.96581,"btc":0.0,"cad":22.59678,"chf":21.70062,"clp":46.20803,"cny":29.85077,"czk":25.05176,"dkk":26.08954,"eos":133.30477,"eth":36.87714,"eur":25.97742,"gbp":27.31032,"hkd":23.93812,"huf":30.22984,"idr":23.22097,"ils":20.37488,"inr":28.82094,"jpy":21.90092,"krw":25.82773,"kwd":24.06617,"lkr":26.37467,"ltc":100.2474,"mmk":23.76562,"mxn":26.42772,"myr":25.30265,"nok":30.11168,"nzd":28.35377,"php":22.01045,"pkr":36.51723,"pln":26.39489,"rub":21.26104,"sar":24.22499,"sek":24.9225,"sgd":24.29049,"thb":17.38252,"try":18.62205,"twd":22.88347,"uah":13.11625,"usd":24.23364,"vef":24.23364,"vnd":24.69572,"xag":8.68024,"xau":8.77097,"xdr":25.56291,"xlm":115.80188,"xrp":60.72697,"zar":26.48666},"price_change_percentage_1y_in_currency":{"aed":54.80299,"ars":155.34218,"aud":65.7028,"bch":78.89456,"bdt":57.19653,"bhd":54.81048,"bmd":54.82116,"bnb":-36.65258,"brl":70.52838,"btc":0.0,"cad":55.45654,"chf":55.10131,"clp":84.99866,"cny":57.31982,"czk":56.67972,"dkk":59.98455,"eos":129.05448,"eth":44.12795,"eur":59.73458,"gbp":54.03378,"hkd":54.68674,"huf":65.94232,"idr":49.69388,"ils":44.12632,"inr":56.02681,"jpy":48.86278,"krw":60.38647,"kwd":54.63991,"lkr":56.01994,"ltc":15.13364,"mmk":46.86442,"mxn":48.17348,"myr":54.61929,"nok":66.2395,"nzd":64.92835,"php":51.02989,"pkr":79.1998,"pln":59.54312,"rub":50.32841,"sar":54.6784,"sek":64.78944,"sgd":53.74122,"thb":42.06052,"try":66.34431,"twd":53.34085,"uah":34.53434,"usd":54.82116,"vef":54.80868,"vnd":54.8204,"xag":31.52974,"xau":29.51256,"xdr":56.55346,"xlm":458.8577,"xrp":200.09987,"zar":63.33739},"market_cap_change_24h_in_currency":{"aed":-33066285509.427795,"ars":-530278117251.8633,"aud":-12960274920.457092,"bch":24024713,"bdt":-759694248769.8789,"bhd":-3385553428.380455,"bmd":-9006537915.03212,"bnb":255092140,"brl":-42175575626.08362,"btc":1663,"cad":-12057111575.367401,"chf":-8491780658.011612,"clp":-7119297708339.391,"cny":-62028988767.01172,"czk":-203550613409.21533,"dkk":-58915127066.18848,"eos":1113960094,"eth":23139205,"eur":-7879783597.133743,"gbp":-6416093889.904633,"hkd":-69937566307.74951,"huf":-2567138850248.992,"idr":-127617002526785.75,"ils":-30288002282.410645,"inr":-646961300054.1992,"jpy":-965736921441.2344,"krw":-10313128196732.844,"kwd":-2728924342.179413,"lkr":-1566622601424.3633,"ltc":12247122,"mmk":-13842142728744.844,"mxn":-177409423821.13818,"myr":-36663555321.51227,"nok":-81603974747.83325,"nzd":-14103220841.779144,"php":-455362689074.46094,"pkr":-1393602846440.6172,"pln":-33879559789.90509,"rub":-570801019531.376,"sar":-33765015342.341797,"sek":-90223858525.68628,"sgd":-12077223463.969208,"thb":-272646025286.54932,"try":-49865416412.02966,"twd":-271842901196.03174,"uah":-226020631120.77588,"usd":-9006537915.03212,"vef":-2.238013371260464e+15,"vnd":-207810852801656.0,"xag":-501296089.4158993,"xau":-5993152.57134043,"xdr":-6570642236.026748,"xlm":-26680177257.057617,"xrp":-14067201416.741577,"zar":-135321097818.74121},"market_cap_change_percentage_24h_in_currency":{"aed":-6.51091,"ars":-6.41886,"aud":-6.36874,"bch":3.96103,"bdt":-6.47821,"bhd":-6.49485,"bmd":-6.51345,"bnb":3.09241,"brl":-7.24146,"btc":0.00921,"cad":-6.56242,"chf":-6.19113,"clp":-6.46648,"cny":-6.38178,"czk":-6.38173,"dkk":-6.30959,"eos":2.28656,"eth":2.70631,"eur":-6.30643,"gbp":-5.99448,"hkd":-6.46636,"huf":-6.1597,"idr":-6.54037,"ils":-6.32529,"inr":-6.52193,"jpy":-6.43487,"krw":-6.33595,"kwd":-6.49837,"lkr":-6.30469,"ltc":0.44574,"mmk":-6.60008,"mxn":-6.60863,"myr":-6.36533,"nok":-6.45567,"nzd":-6.53604,"php":-6.47587,"pkr":-6.48338,"pln":-6.30898,"rub":-6.47314,"sar":-6.51176,"sek":-6.76653,"sgd":-6.41127,"thb":-6.53114,"try":-6.32564,"twd":-6.43844,"uah":-6.7623,"usd":-6.51345,"vef":-6.51345,"vnd":-6.46851,"xag":-6.22021,"xau":-6.36652,"xdr":-6.53582,"xlm":-1.15506,"xrp":-2.4681,"zar":-6.64898},"total_supply":21000000.0,"circulating_supply":18061500.0,"last_updated":"2019-11-22T15:50:18.919Z"},"public_interest_stats":{"alexa_rank":12740,"bing_matches":135000000},"status_updates":[],"last_updated":"2019-11-22T15:50:18.919Z"} diff --git a/server/public.go b/server/public.go index ba73c09b..d6838238 100644 --- a/server/public.go +++ b/server/public.go @@ -186,6 +186,8 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/v2/sendtx/", s.jsonHandler(s.apiSendTx, apiV2)) serveMux.HandleFunc(path+"api/v2/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV2)) serveMux.HandleFunc(path+"api/v2/feestats/", s.jsonHandler(s.apiFeeStats, apiV2)) + serveMux.HandleFunc(path+"api/v2/tickers/", s.jsonHandler(s.apiTickers, apiV2)) + serveMux.HandleFunc(path+"api/v2/tickers-list/", s.jsonHandler(s.apiTickersList, apiV2)) // socket.io interface serveMux.Handle(path+"socket.io/", s.socketio.GetHandler()) // websocket interface @@ -210,6 +212,11 @@ func (s *PublicServer) OnNewBlock(hash string, height uint32) { s.websocket.OnNewBlock(hash, height) } +// OnNewFiatRatesTicker notifies users subscribed to bitcoind/fiatrates about new ticker +func (s *PublicServer) OnNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) { + s.websocket.OnNewFiatRatesTicker(ticker) +} + // OnNewTxAddr notifies users subscribed to bitcoind/addresstxid about new block func (s *PublicServer) OnNewTxAddr(tx *bchain.Tx, desc bchain.AddressDescriptor) { s.socketio.OnNewTxAddr(tx.Txid, desc) @@ -1088,6 +1095,43 @@ 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) { + s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-list"}).Inc() + date := strings.ToLower(r.URL.Query().Get("date")) + result, err := s.api.GetFiatRatesTickersList(date) + return result, err +} + +// apiTickers returns FiatRates ticker prices for the specified block or date. +func (s *PublicServer) apiTickers(r *http.Request, apiVersion int) (interface{}, error) { + var result *db.ResultTickerAsString + var err error + currency := strings.ToLower(r.URL.Query().Get("currency")) + + if block := r.URL.Query().Get("block"); block != "" { + // Get tickers for specified block height or block hash + s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-block"}).Inc() + result, err = s.api.GetFiatRatesForBlockID(block, currency) + } else if date := r.URL.Query().Get("date"); date != "" { + // Get tickers for specified date + s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-date"}).Inc() + resultTickers, err := s.api.GetFiatRatesForDates([]string{date}, currency) + if err != nil { + return nil, err + } + result = &resultTickers.Tickers[0] + } else { + // No parameters - get the latest available ticker + s.metrics.ExplorerViews.With(common.Labels{"action": "api-tickers-last"}).Inc() + result, err = s.api.GetCurrentFiatRates(currency) + } + if err != nil { + return nil, err + } + return result, nil +} + type resultEstimateFeeAsString struct { Result string `json:"result"` } diff --git a/server/public_test.go b/server/public_test.go index 60afb45c..d3ae15d8 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -58,6 +58,9 @@ func setupRocksDB(t *testing.T, parser bchain.BlockChainParser) (*db.RocksDB, *c if err := d.ConnectBlock(block2); err != nil { t.Fatal(err) } + if err := InitTestFiatRates(d); err != nil { + t.Fatal(err) + } is.FinishedSync(block2.Height) return d, is, tmp } @@ -146,6 +149,41 @@ func newPostRequest(u string, body string) *http.Request { return r } +// InitTestFiatRates initializes test data for /api/v2/tickers endpoint +func InitTestFiatRates(d *db.RocksDB) error { + convertedDate, err := db.FiatRatesConvertDate("20191121140000") + if err != nil { + return err + } + ticker := &db.CurrencyRatesTicker{ + Timestamp: convertedDate, + Rates: map[string]json.Number{ + "usd": "7814.5", + "eur": "7100.0", + }, + } + err = d.FiatRatesStoreTicker(ticker) + if err != nil { + return err + } + convertedDate, err = db.FiatRatesConvertDate("20191121143015") + if err != nil { + return err + } + ticker = &db.CurrencyRatesTicker{ + Timestamp: convertedDate, + Rates: map[string]json.Number{ + "usd": "7914.5", + "eur": "7134.1", + }, + } + err = d.FiatRatesStoreTicker(ticker) + if err != nil { + return err + } + return nil +} + func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { tests := []struct { name string @@ -460,6 +498,105 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { `{"txCount":3,"totalFeesSat":"1284","averageFeePerKb":1398,"decilesFeePerKb":[155,155,155,155,1679,1679,1679,2361,2361,2361,2361]}`, }, }, + { + name: "apiFiatRates missing currency", + r: newGetRequest(ts.URL + "/api/v2/tickers"), + status: http.StatusBadRequest, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"error":"Missing or empty \"currency\" parameter"}`, + }, + }, + { + name: "apiFiatRates get last rate", + r: newGetRequest(ts.URL + "/api/v2/tickers?currency=usd"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"data_timestamp":"20191121143015","rates":{"usd":7914.5}}`, + }, + }, + { + name: "apiFiatRates get rate by exact date", + r: newGetRequest(ts.URL + "/api/v2/tickers?currency=usd&date=20191121140000"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"data_timestamp":"20191121140000","rates":{"usd":7814.5}}`, + }, + }, + { + name: "apiFiatRates incorrect date", + r: newGetRequest(ts.URL + "/api/v2/tickers?currency=usd&date=yesterday"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"error":"Date \"yesterday\" does not match any of available formats. Possible formats are: YYYYMMDDhhmmss, YYYYMMDDhhmm, YYYYMMDDhh, YYYYMMDD"}`, + }, + }, + { + name: "apiFiatRates future date", + r: newGetRequest(ts.URL + "/api/v2/tickers?currency=usd&date=20200101000000"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"error":"No tickers available for 2020-01-01 00:00:00 +0000 UTC (usd)"}`, + }, + }, + { + name: "apiFiatRates get EUR rate (exact date)", + r: newGetRequest(ts.URL + "/api/v2/tickers?date=20191121140000¤cy=eur"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"data_timestamp":"20191121140000","rates":{"eur":7100.0,"usd":7814.5}}`, + }, + }, + { + name: "apiFiatRates get closest rate", + r: newGetRequest(ts.URL + "/api/v2/tickers?date=20191121130000¤cy=usd"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"data_timestamp":"20191121140000","rates":{"usd":7814.5}}`, + }, + }, + { + name: "apiFiatRates get rate by block height", + r: newGetRequest(ts.URL + "/api/v2/tickers?block=225494¤cy=usd"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"data_timestamp":"20191121140000","rates":{"usd":7814.5}}`, + }, + }, + { + name: "apiFiatRates get rate for EUR", + r: newGetRequest(ts.URL + "/api/v2/tickers?date=20191121140000¤cy=eur"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"data_timestamp":"20191121140000","rates":{"eur":7100.0,"usd":7814.5}}`, + }, + }, + { + name: "apiFiatRates get exact rate for an incorrect currency", + r: newGetRequest(ts.URL + "/api/v2/tickers?date=20191121140000¤cy=does_not_exist"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"error":"Currency \"does_not_exist\" is not available for timestamp 20191121140000. Available currencies are: eur, usd."}`, + }, + }, + { + name: "apiTickerList", + r: newGetRequest(ts.URL + "/api/v2/tickers-list?date=20191121140000"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"data_timestamp":"20191121140000","available_currencies":["eur","usd"]}`, + }, + }, { name: "apiAddress v1", r: newGetRequest(ts.URL + "/api/v1/address/mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"), @@ -965,6 +1102,154 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { }, want: `{"id":"16","data":{}}`, }, + { + name: "websocket getCurrentFiatRates no currency", + req: websocketReq{ + Method: "getCurrentFiatRates", + Params: map[string]interface{}{ + "": "", + }, + }, + want: `{"id":"17","data":{"error":{"message":"Missing or empty \"currency\" parameter"}}}`, + }, + { + name: "websocket getCurrentFiatRates usd", + req: websocketReq{ + Method: "getCurrentFiatRates", + Params: map[string]interface{}{ + "currency": "usd", + }, + }, + want: `{"id":"18","data":{"data_timestamp":"20191121143015","rates":{"usd":7914.5}}}`, + }, + { + name: "websocket getCurrentFiatRates eur", + req: websocketReq{ + Method: "getCurrentFiatRates", + Params: map[string]interface{}{ + "currency": "eur", + }, + }, + want: `{"id":"19","data":{"data_timestamp":"20191121143015","rates":{"eur":7134.1,"usd":7914.5}}}`, + }, + { + name: "websocket getCurrentFiatRates incorrect currency", + req: websocketReq{ + Method: "getCurrentFiatRates", + Params: map[string]interface{}{ + "currency": "does-not-exist", + }, + }, + want: `{"id":"20","data":{"error":{"message":"Currency \"does-not-exist\" is not available for timestamp 20191121143015. Available currencies are: eur, usd."}}}`, + }, + { + name: "websocket getFiatRatesForDates missing date", + req: websocketReq{ + Method: "getFiatRatesForDates", + Params: map[string]interface{}{ + "currency": "usd", + }, + }, + want: `{"id":"21","data":{"error":{"message":"No dates provided"}}}`, + }, + { + name: "websocket getFiatRatesForDates incorrect date", + req: websocketReq{ + Method: "getFiatRatesForDates", + Params: map[string]interface{}{ + "currency": "usd", + "dates": []string{"yesterday"}, + }, + }, + want: `{"id":"22","data":{"tickers":[{"error":"Date \"yesterday\" does not match any of available formats. Possible formats are: YYYYMMDDhhmmss, YYYYMMDDhhmm, YYYYMMDDhh, YYYYMMDD"}]}}`, + }, + { + name: "websocket getFiatRatesForDates incorrect (future) date", + req: websocketReq{ + Method: "getFiatRatesForDates", + Params: map[string]interface{}{ + "currency": "usd", + "dates": []string{"20200101000000"}, + }, + }, + want: `{"id":"23","data":{"tickers":[{"error":"No tickers available for 2020-01-01 00:00:00 +0000 UTC (usd)"}]}}`, + }, + { + name: "websocket getFiatRatesForDates exact date", + req: websocketReq{ + Method: "getFiatRatesForDates", + Params: map[string]interface{}{ + "currency": "usd", + "dates": []string{"20191121140000"}, + }, + }, + want: `{"id":"24","data":{"tickers":[{"data_timestamp":"20191121140000","rates":{"usd":7814.5}}]}}`, + }, + { + name: "websocket getFiatRatesForDates closest date, eur", + req: websocketReq{ + Method: "getFiatRatesForDates", + Params: map[string]interface{}{ + "currency": "eur", + "dates": []string{"20191121130000"}, + }, + }, + want: `{"id":"25","data":{"tickers":[{"data_timestamp":"20191121140000","rates":{"eur":7100.0,"usd":7814.5}}]}}`, + }, + { + name: "websocket getFiatRatesForDates multiple dates usd", + req: websocketReq{ + Method: "getFiatRatesForDates", + Params: map[string]interface{}{ + "currency": "usd", + "dates": []string{"20191121140000", "20191121143015"}, + }, + }, + want: `{"id":"26","data":{"tickers":[{"data_timestamp":"20191121140000","rates":{"usd":7814.5}},{"data_timestamp":"20191121143015","rates":{"usd":7914.5}}]}}`, + }, + { + name: "websocket getFiatRatesForDates multiple dates eur", + req: websocketReq{ + Method: "getFiatRatesForDates", + Params: map[string]interface{}{ + "currency": "eur", + "dates": []string{"20191121140000", "20191121143015"}, + }, + }, + want: `{"id":"27","data":{"tickers":[{"data_timestamp":"20191121140000","rates":{"eur":7100.0,"usd":7814.5}},{"data_timestamp":"20191121143015","rates":{"eur":7134.1,"usd":7914.5}}]}}`, + }, + { + name: "websocket getFiatRatesForDates multiple dates with an error", + req: websocketReq{ + Method: "getFiatRatesForDates", + Params: map[string]interface{}{ + "currency": "usd", + "dates": []string{"20191121140000", "20191121143015", "not-a-real-date"}, + }, + }, + want: `{"id":"28","data":{"tickers":[{"data_timestamp":"20191121140000","rates":{"usd":7814.5}},{"data_timestamp":"20191121143015","rates":{"usd":7914.5}},{"error":"Date \"not-a-real-date\" does not match any of available formats. Possible formats are: YYYYMMDDhhmmss, YYYYMMDDhhmm, YYYYMMDDhh, YYYYMMDD"}]}}`, + }, + { + name: "websocket getFiatRatesForDates multiple errors", + req: websocketReq{ + Method: "getFiatRatesForDates", + Params: map[string]interface{}{ + "currency": "usd", + "dates": []string{"20200101000000", "not-a-real-date"}, + }, + }, + want: `{"id":"29","data":{"tickers":[{"error":"No tickers available for 2020-01-01 00:00:00 +0000 UTC (usd)"},{"error":"Date \"not-a-real-date\" does not match any of available formats. Possible formats are: YYYYMMDDhhmmss, YYYYMMDDhhmm, YYYYMMDDhh, YYYYMMDD"}]}}`, + }, + { + name: "websocket getTickersList", + req: websocketReq{ + Method: "getFiatRatesTickersList", + Params: map[string]interface{}{ + "date": "20191121140000", + }, + }, + want: `{"id":"30","data":{"data_timestamp":"20191121140000","available_currencies":["eur","usd"]}}`, + }, } // send all requests at once diff --git a/server/websocket.go b/server/websocket.go index 1e4ea3e6..fa47835b 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -53,21 +53,23 @@ type websocketChannel struct { // WebsocketServer is a handle to websocket server type WebsocketServer struct { - socket *websocket.Conn - upgrader *websocket.Upgrader - db *db.RocksDB - txCache *db.TxCache - chain bchain.BlockChain - chainParser bchain.BlockChainParser - mempool bchain.Mempool - metrics *common.Metrics - is *common.InternalState - api *api.Worker - block0hash string - newBlockSubscriptions map[*websocketChannel]string - newBlockSubscriptionsLock sync.Mutex - addressSubscriptions map[string]map[*websocketChannel]string - addressSubscriptionsLock sync.Mutex + socket *websocket.Conn + upgrader *websocket.Upgrader + db *db.RocksDB + txCache *db.TxCache + chain bchain.BlockChain + chainParser bchain.BlockChainParser + mempool bchain.Mempool + metrics *common.Metrics + is *common.InternalState + api *api.Worker + block0hash string + newBlockSubscriptions map[*websocketChannel]string + newBlockSubscriptionsLock sync.Mutex + addressSubscriptions map[string]map[*websocketChannel]string + addressSubscriptionsLock sync.Mutex + fiatRatesSubscriptions map[string]map[*websocketChannel]string + fiatRatesSubscriptionsLock sync.Mutex } // NewWebsocketServer creates new websocket interface to blockbook and returns its handle @@ -86,17 +88,18 @@ func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain. WriteBufferSize: 1024 * 32, CheckOrigin: checkOrigin, }, - db: db, - txCache: txCache, - chain: chain, - chainParser: chain.GetChainParser(), - mempool: mempool, - metrics: metrics, - is: is, - api: api, - block0hash: b0, - newBlockSubscriptions: make(map[*websocketChannel]string), - addressSubscriptions: make(map[string]map[*websocketChannel]string), + db: db, + txCache: txCache, + chain: chain, + chainParser: chain.GetChainParser(), + mempool: mempool, + metrics: metrics, + is: is, + api: api, + block0hash: b0, + newBlockSubscriptions: make(map[*websocketChannel]string), + addressSubscriptions: make(map[string]map[*websocketChannel]string), + fiatRatesSubscriptions: make(map[string]map[*websocketChannel]string), } return s, nil } @@ -214,6 +217,7 @@ func (s *WebsocketServer) onConnect(c *websocketChannel) { func (s *WebsocketServer) onDisconnect(c *websocketChannel) { s.unsubscribeNewBlock(c) s.unsubscribeAddresses(c) + s.unsubscribeFiatRates(c) glog.Info("Client disconnected ", c.id, ", ", c.ip) s.metrics.WebsocketClients.Dec() } @@ -298,10 +302,54 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs "unsubscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { return s.unsubscribeAddresses(c) }, + "subscribeFiatRates": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + r := struct { + Currency string `json:"currency"` + }{} + err = json.Unmarshal(req.Params, &r) + if err != nil { + return nil, err + } + return s.subscribeFiatRates(c, r.Currency, req) + }, + "unsubscribeFiatRates": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + return s.unsubscribeFiatRates(c) + }, "ping": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { r := struct{}{} return r, nil }, + "getCurrentFiatRates": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + r := struct { + Currency string `json:"currency"` + }{} + err = json.Unmarshal(req.Params, &r) + if err == nil { + rv, err = s.getCurrentFiatRates(r.Currency) + } + return + }, + "getFiatRatesForDates": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + r := struct { + Dates []string `json:"dates"` + Currency string `json:"currency"` + }{} + err = json.Unmarshal(req.Params, &r) + if err == nil { + rv, err = s.getFiatRatesForDates(r.Dates, r.Currency) + } + return + }, + "getFiatRatesTickersList": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + r := struct { + Date string `json:"date"` + }{} + err = json.Unmarshal(req.Params, &r) + if err == nil { + rv, err = s.getFiatRatesTickersList(r.Date) + } + return + }, } func sendResponse(c *websocketChannel, req *websocketReq, data interface{}) { @@ -613,6 +661,36 @@ func (s *WebsocketServer) unsubscribeAddresses(c *websocketChannel) (res interfa return &subscriptionResponse{false}, nil } +// subscribeFiatRates subscribes all FiatRates subscriptions by this channel +func (s *WebsocketServer) subscribeFiatRates(c *websocketChannel, currency string, req *websocketReq) (res interface{}, err error) { + // unsubscribe all previous subscriptions + s.unsubscribeFiatRates(c) + s.fiatRatesSubscriptionsLock.Lock() + defer s.fiatRatesSubscriptionsLock.Unlock() + + as, ok := s.fiatRatesSubscriptions[currency] + if !ok { + as = make(map[*websocketChannel]string) + s.fiatRatesSubscriptions[currency] = as + } + as[c] = req.ID + return &subscriptionResponse{true}, nil +} + +// unsubscribeFiatRates unsubscribes all FiatRates subscriptions by this channel +func (s *WebsocketServer) unsubscribeFiatRates(c *websocketChannel) (res interface{}, err error) { + s.fiatRatesSubscriptionsLock.Lock() + defer s.fiatRatesSubscriptionsLock.Unlock() + for _, sa := range s.fiatRatesSubscriptions { + for sc := range sa { + if sc == c { + delete(sa, c) + } + } + } + return &subscriptionResponse{false}, nil +} + // OnNewBlock is a callback that broadcasts info about new block to subscribed clients func (s *WebsocketServer) OnNewBlock(hash string, height uint32) { s.newBlockSubscriptionsLock.Lock() @@ -640,8 +718,9 @@ func (s *WebsocketServer) OnNewTxAddr(tx *bchain.Tx, addrDesc bchain.AddressDesc // check if there is any subscription but release the lock immediately, GetTransactionFromBchainTx may take some time s.addressSubscriptionsLock.Lock() as, ok := s.addressSubscriptions[string(addrDesc)] + lenAs := len(as) s.addressSubscriptionsLock.Unlock() - if ok && len(as) > 0 { + if ok && lenAs > 0 { addr, _, err := s.chainParser.GetAddressesFromAddrDesc(addrDesc) if err != nil { glog.Error("GetAddressesFromAddrDesc error ", err, " for ", addrDesc) @@ -678,3 +757,51 @@ func (s *WebsocketServer) OnNewTxAddr(tx *bchain.Tx, addrDesc bchain.AddressDesc } } } + +func (s *WebsocketServer) broadcastTicker(coin string, rate json.Number) { + s.fiatRatesSubscriptionsLock.Lock() + defer s.fiatRatesSubscriptionsLock.Unlock() + as, ok := s.fiatRatesSubscriptions[coin] + if ok && len(as) > 0 { + data := struct { + Rate interface{} `json:"rate"` + }{ + Rate: rate, + } + // get the list of subscriptions again, this time keep the lock + as, ok = s.fiatRatesSubscriptions[coin] + if ok { + for c, id := range as { + if c.IsAlive() { + c.out <- &websocketRes{ + ID: id, + Data: &data, + } + } + } + glog.Info("broadcasting new rate ", rate, " for coin ", coin, " to ", len(as), " channels") + } + } +} + +// OnNewFiatRatesTicker is a callback that broadcasts info about fiat rates affecting subscribed currency +func (s *WebsocketServer) OnNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) { + for currency, rate := range ticker.Rates { + s.broadcastTicker(currency, rate) + } +} + +func (s *WebsocketServer) getCurrentFiatRates(currency string) (interface{}, error) { + ret, err := s.api.GetCurrentFiatRates(currency) + return ret, err +} + +func (s *WebsocketServer) getFiatRatesForDates(dates []string, currency string) (interface{}, error) { + ret, err := s.api.GetFiatRatesForDates(dates, currency) + return ret, err +} + +func (s *WebsocketServer) getFiatRatesTickersList(date string) (interface{}, error) { + ret, err := s.api.GetFiatRatesTickersList(date) + return ret, err +} diff --git a/static/test-websocket.html b/static/test-websocket.html index 523ae86c..ae9c2504 100644 --- a/static/test-websocket.html +++ b/static/test-websocket.html @@ -1,6 +1,5 @@ - @@ -279,6 +278,70 @@ }); } + function getFiatRatesForDates() { + const method = 'getFiatRatesForDates'; + var dates = document.getElementById('getFiatRatesForDatesList').value.split(","); + var currency = document.getElementById('getFiatRatesForDatesCurrency').value; + dates = dates.map(s => s.trim()); + const params = { + dates, + "currency": currency + }; + send(method, params, function (result) { + document.getElementById('getFiatRatesForDatesResult').innerText = JSON.stringify(result).replace(/,/g, ", "); + }); + } + + function getCurrentFiatRates() { + const method = 'getCurrentFiatRates'; + var currency = document.getElementById('getCurrentFiatRatesCurrency').value; + const params = { + "currency": currency + }; + send(method, params, function (result) { + document.getElementById('getCurrentFiatRatesResult').innerText = JSON.stringify(result).replace(/,/g, ", "); + }); + } + + function getFiatRatesTickersList() { + const method = 'getFiatRatesTickersList'; + var date = document.getElementById('getFiatRatesTickersListDate').value; + const params = { + date, + }; + send(method, params, function (result) { + document.getElementById('getFiatRatesTickersListResult').innerText = JSON.stringify(result).replace(/,/g, ", "); + }); + } + + function subscribeNewFiatRatesTicker() { + const method = 'subscribeFiatRates'; + var currency = document.getElementById('subscribeFiatRatesCurrency').value; + const params = { + "currency": currency + }; + if (subscribeNewFiatRatesTickerId) { + delete subscriptions[subscribeNewFiatRatesTickerId]; + subscribeNewFiatRatesTickerId = ""; + } + subscribeNewFiatRatesTickerId = subscribe(method, params, function (result) { + document.getElementById('subscribeNewFiatRatesTickerResult').innerText += JSON.stringify(result).replace(/,/g, ", ") + "\n"; + }); + document.getElementById('subscribeNewFiatRatesTickerId').innerText = subscribeNewFiatRatesTickerId; + document.getElementById('unsubscribeNewFiatRatesTickerButton').setAttribute("style", "display: inherit;"); + } + + function unsubscribeNewFiatRatesTicker() { + const method = 'unsubscribeFiatRates'; + const params = { + }; + unsubscribe(method, subscribeNewFiatRatesTickerId, params, function (result) { + subscribeNewFiatRatesTickerId = ""; + document.getElementById('subscribeNewFiatRatesTickerResult').innerText += JSON.stringify(result).replace(/,/g, ", ") + "\n"; + document.getElementById('subscribeNewFiatRatesTickerId').innerText = ""; + document.getElementById('unsubscribeNewFiatRatesTickerButton').setAttribute("style", "display: none;"); + }); + } @@ -460,6 +523,59 @@
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+