Add fiat rates functionality (#316)
* Add initial commit for fiat rates functionality * templates.go: use bash from current user's environment * bitcoinrpc.go: add FiatRates and FiatRatesParams to config * blockbook.go: add initFiatRatesDownloader kickoff * bitcoin.json: add coingecko API URL * rockdb.go: add FindTicker and StoreTicker functions * rocksdb_test.go: add a simple test for storing and getting FiatRate tickers * rocksdb: add FindLastTicker and convertDate, make FindTicker return strings * rocksdb: add ConvertDate function and CoinGeckoTicker struct, update tests * blockbook.go, fiat: finalize the CoinGecko downloader * coingecko.go: do not stop syncing when encountered an error * rocksdb_test: fix the exported function name * worker.go: make getBlockInfoFromBlockID a public function * public.go: apiTickers kickoff * rocksdb_test: fix the unittest comment * coingecko.go: update comments * blockbook.go, fiat: reword CoinGecko -> FiatRates, fix binary search upper bound, remove assignment of goroutine call result * rename coingecko -> fiat_rates * fiat_rates: export only the necessary methods * blockbook.go: update log message * bitcoinrpc.go: remove fiatRates settings * use CurrencyRatesTicker structure everywhere, fix time format string, update tests, use UTC time * add /api/v2/tickers tests, store rates as strings (json.Number) * fiat_rates: add more tests, metrics and tickers-list endpoint, make the "currency" parameter mandatory * public, worker: move FiatRates API logic to worker.go * fiat_rates: add a future date test, fix comments, add more checks, store time as a pointer * rocksdb_test: remove unneeded code * fiat_rates: add a "ping" call to check server availability * fiat_rates: do not return empty ticker, return nil instead if not found add a test for non-existent ticker * rocksdb_test: remove Sleep from tests * worker.go: do not propagate all API errors to the client * move InitTestFiatRates from rocksdb.go to public_test.go * public.go: fix FiatRatesFindLastTicker result check * fiat_rates: mock API server responses * remove commented-out code * fiat_rates: add comment explaining what periodSeconds attribute is used for * websocket.go: implement fiatRates websocket endpoints & add tests * fiatRates: add getFiatRatesTickersList websocket endpoint & test * fiatRates: make websocket getFiatRatesByDate accept an array of dates, add more tests * fiatRates: remove getFiatRatesForBlockID from websocket endpoints * fiatRates: remove "if test", use custom startTime instead Update tests and mock data * fiatRates: finalize websocket functionality add "date" parameter to TickerList return data timestamps where needed fix sync bugs (nil timestamp, duplicate save) * fiatRates: add FiatRates configs for different coins * worker.go: make GetBlockInfoFromBlockID private again * fiatRates: wait & retry on errors, remove Ping function * websocket.go: remove incorrect comment * fiatRates: move coingecko-related code to a separate file, use interface * fiatRates: if the new rates are the same as previous, try five more times, and only then store them * coingecko: fix getting actual rates, add a timestamp parameter to get uncached responses * vertcoin_testnet.json: remove fiat rates parameters * fiat_rates: add timestamp to log message about skipping the repeating ratesbalanceHistory
parent
e2b34afb9c
commit
f6111af5da
141
api/worker.go
141
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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
78
blockbook.go
78
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
132
db/rocksdb.go
132
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 {
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"}}
|
|
@ -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}}
|
|
@ -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"}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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"}}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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"}}
|
|
@ -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"}}
|
|
@ -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}}
|
|
@ -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}}
|
File diff suppressed because one or more lines are too long
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
@ -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;");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
|
@ -460,6 +523,59 @@
|
|||
<div class="row">
|
||||
<div class="col" id="subscribeAddressesResult"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="get fiat rates for dates" onclick="getFiatRatesForDates()">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input type="text" class="form-control" id="getFiatRatesForDatesCurrency" value="usd">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input type="text" class="form-control" id="getFiatRatesForDatesList" value="20191121140000,20191121143015">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="getFiatRatesForDatesResult"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="get current firat rates" onclick="getCurrentFiatRates()">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input type="text" class="form-control" id="getCurrentFiatRatesCurrency" value="usd">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="getCurrentFiatRatesResult"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="get fiat rates tickers" onclick="getFiatRatesTickersList()">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input type="text" class="form-control" id="getFiatRatesTickersListDate" value="20191121140000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="getFiatRatesTickersListResult"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="subscribe new fiat rates" onclick="subscribeNewFiatRatesTicker()">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<span id="subscribeNewFiatRatesTickerId"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input type="text" class="form-control" id="subscribeFiatRatesCurrency" value="usd">
|
||||
</div>
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" id="unsubscribeNewFiatRatesTickerButton" style="display: none;" type="button" value="unsubscribe" onclick="unsubscribeNewFiatRatesTicker()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="subscribeNewFiatRatesTickerResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
|
|
Loading…
Reference in New Issue