Merge branch 'master' into balanceHistory
This commit is contained in:
commit
1cec22ecba
141
api/worker.go
141
api/worker.go
@ -13,6 +13,7 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
@ -1114,6 +1115,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()
|
||||
|
||||
@ -22,10 +22,10 @@
|
||||
"package_name": "backend-bcash",
|
||||
"package_revision": "satoshilabs-1",
|
||||
"system_user": "bcash",
|
||||
"version": "0.20.7",
|
||||
"binary_url": "https://download.bitcoinabc.org/0.20.7/linux/bitcoin-abc-0.20.7-x86_64-linux-gnu.tar.gz",
|
||||
"version": "0.20.8",
|
||||
"binary_url": "https://download.bitcoinabc.org/0.20.8/linux/bitcoin-abc-0.20.8-x86_64-linux-gnu.tar.gz",
|
||||
"verification_type": "sha256",
|
||||
"verification_source": "a8ccb15b2eb37e7dc0790fb72e5f31fba9c7df5134df2110eaf59724636b64ef",
|
||||
"verification_source": "cb420f65244dd7514ed3f71be710e5704f075a97c35fd54b470b87b29440eb3b",
|
||||
"extract_command": "tar -C backend --strip 1 -xf",
|
||||
"exclude_files": [
|
||||
"bin/bitcoin-qt"
|
||||
@ -49,18 +49,21 @@
|
||||
"additional_params": "",
|
||||
"block_chain": {
|
||||
"parse": true,
|
||||
"subversion": "/Bitcoin ABC:0.20.7/",
|
||||
"subversion": "/Bitcoin ABC:0.20.8/",
|
||||
"address_format": "cashaddr",
|
||||
"mempool_workers": 8,
|
||||
"mempool_sub_workers": 2,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,10 +22,10 @@
|
||||
"package_name": "backend-bcash-testnet",
|
||||
"package_revision": "satoshilabs-1",
|
||||
"system_user": "bcash",
|
||||
"version": "0.20.7",
|
||||
"binary_url": "https://download.bitcoinabc.org/0.20.7/linux/bitcoin-abc-0.20.7-x86_64-linux-gnu.tar.gz",
|
||||
"version": "0.20.8",
|
||||
"binary_url": "https://download.bitcoinabc.org/0.20.8/linux/bitcoin-abc-0.20.8-x86_64-linux-gnu.tar.gz",
|
||||
"verification_type": "sha256",
|
||||
"verification_source": "a8ccb15b2eb37e7dc0790fb72e5f31fba9c7df5134df2110eaf59724636b64ef",
|
||||
"verification_source": "cb420f65244dd7514ed3f71be710e5704f075a97c35fd54b470b87b29440eb3b",
|
||||
"extract_command": "tar -C backend --strip 1 -xf",
|
||||
"exclude_files": [
|
||||
"bin/bitcoin-qt"
|
||||
@ -49,7 +49,7 @@
|
||||
"additional_params": "",
|
||||
"block_chain": {
|
||||
"parse": true,
|
||||
"subversion": "/Bitcoin ABC:0.20.7/",
|
||||
"subversion": "/Bitcoin ABC:0.20.8/",
|
||||
"address_format": "cashaddr",
|
||||
"mempool_workers": 8,
|
||||
"mempool_sub_workers": 2,
|
||||
|
||||
@ -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"
|
||||
@ -1099,3 +1101,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.")
|
||||
}
|
||||
}
|
||||
|
||||
136
fiat/coingecko.go
Normal file
136
fiat/coingecko.go
Normal file
@ -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
|
||||
}
|
||||
214
fiat/fiat_rates.go
Normal file
214
fiat/fiat_rates.go
Normal file
@ -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
|
||||
}
|
||||
176
fiat/fiat_rates_test.go
Normal file
176
fiat/fiat_rates_test.go
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
1
fiat/mock_data/01-02-2013.json
Normal file
1
fiat/mock_data/01-02-2013.json
Normal file
@ -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"}}
|
||||
1
fiat/mock_data/01-05-2013.json
Normal file
1
fiat/mock_data/01-05-2013.json
Normal file
@ -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}}
|
||||
1
fiat/mock_data/04-04-2013.json
Normal file
1
fiat/mock_data/04-04-2013.json
Normal file
@ -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"}}
|
||||
1
fiat/mock_data/05-05-2013.json
Normal file
1
fiat/mock_data/05-05-2013.json
Normal file
@ -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}}
|
||||
1
fiat/mock_data/05-06-2013.json
Normal file
1
fiat/mock_data/05-06-2013.json
Normal file
@ -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}}
|
||||
1
fiat/mock_data/07-10-2013.json
Normal file
1
fiat/mock_data/07-10-2013.json
Normal file
@ -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}}
|
||||
1
fiat/mock_data/13-06-2014.json
Normal file
1
fiat/mock_data/13-06-2014.json
Normal file
@ -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}}
|
||||
1
fiat/mock_data/20-04-2013.json
Normal file
1
fiat/mock_data/20-04-2013.json
Normal file
@ -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"}}
|
||||
1
fiat/mock_data/20-11-2019.json
Normal file
1
fiat/mock_data/20-11-2019.json
Normal file
File diff suppressed because one or more lines are too long
1
fiat/mock_data/21-11-2019.json
Normal file
1
fiat/mock_data/21-11-2019.json
Normal file
File diff suppressed because one or more lines are too long
1
fiat/mock_data/22-11-2019.json
Normal file
1
fiat/mock_data/22-11-2019.json
Normal file
File diff suppressed because one or more lines are too long
1
fiat/mock_data/23-09-2011.json
Normal file
1
fiat/mock_data/23-09-2011.json
Normal file
@ -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"}}
|
||||
1
fiat/mock_data/27-04-2013.json
Normal file
1
fiat/mock_data/27-04-2013.json
Normal file
@ -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"}}
|
||||
1
fiat/mock_data/28-04-2013.json
Normal file
1
fiat/mock_data/28-04-2013.json
Normal file
@ -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}}
|
||||
1
fiat/mock_data/29-04-2013.json
Normal file
1
fiat/mock_data/29-04-2013.json
Normal file
@ -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}}
|
||||
1
fiat/mock_data/current.json
Normal file
1
fiat/mock_data/current.json
Normal file
File diff suppressed because one or more lines are too long
@ -188,6 +188,8 @@ func (s *PublicServer) ConnectFullPublicInterface() {
|
||||
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/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault))
|
||||
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
|
||||
@ -212,6 +214,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)
|
||||
@ -1119,6 +1126,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"`
|
||||
}
|
||||
|
||||
@ -63,6 +63,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
|
||||
}
|
||||
@ -151,6 +154,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
|
||||
@ -465,6 +503,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"),
|
||||
@ -1024,6 +1161,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
Block a user