2018-06-26 05:02:53 -06:00
package api
import (
"blockbook/bchain"
2018-11-28 06:27:02 -07:00
"blockbook/bchain/coins/eth"
2018-06-27 16:36:56 -06:00
"blockbook/common"
2018-06-26 05:02:53 -06:00
"blockbook/db"
2018-08-31 07:23:04 -06:00
"bytes"
2018-11-13 02:31:27 -07:00
"encoding/json"
2018-08-27 07:36:33 -06:00
"fmt"
2019-04-28 03:54:12 -06:00
"math"
2018-07-24 07:58:37 -06:00
"math/big"
2019-04-28 03:54:12 -06:00
"os"
"sort"
2018-09-17 10:28:08 -06:00
"strconv"
Add fiat rates functionality (#316)
* Add initial commit for fiat rates functionality
* templates.go: use bash from current user's environment
* bitcoinrpc.go: add FiatRates and FiatRatesParams to config
* blockbook.go: add initFiatRatesDownloader kickoff
* bitcoin.json: add coingecko API URL
* rockdb.go: add FindTicker and StoreTicker functions
* rocksdb_test.go: add a simple test for storing and getting FiatRate tickers
* rocksdb: add FindLastTicker and convertDate, make FindTicker return strings
* rocksdb: add ConvertDate function and CoinGeckoTicker struct, update tests
* blockbook.go, fiat: finalize the CoinGecko downloader
* coingecko.go: do not stop syncing when encountered an error
* rocksdb_test: fix the exported function name
* worker.go: make getBlockInfoFromBlockID a public function
* public.go: apiTickers kickoff
* rocksdb_test: fix the unittest comment
* coingecko.go: update comments
* blockbook.go, fiat: reword CoinGecko -> FiatRates, fix binary search upper bound, remove assignment of goroutine call result
* rename coingecko -> fiat_rates
* fiat_rates: export only the necessary methods
* blockbook.go: update log message
* bitcoinrpc.go: remove fiatRates settings
* use CurrencyRatesTicker structure everywhere, fix time format string, update tests, use UTC time
* add /api/v2/tickers tests, store rates as strings (json.Number)
* fiat_rates: add more tests, metrics and tickers-list endpoint, make the "currency" parameter mandatory
* public, worker: move FiatRates API logic to worker.go
* fiat_rates: add a future date test, fix comments, add more checks, store time as a pointer
* rocksdb_test: remove unneeded code
* fiat_rates: add a "ping" call to check server availability
* fiat_rates: do not return empty ticker, return nil instead if not found
add a test for non-existent ticker
* rocksdb_test: remove Sleep from tests
* worker.go: do not propagate all API errors to the client
* move InitTestFiatRates from rocksdb.go to public_test.go
* public.go: fix FiatRatesFindLastTicker result check
* fiat_rates: mock API server responses
* remove commented-out code
* fiat_rates: add comment explaining what periodSeconds attribute is used for
* websocket.go: implement fiatRates websocket endpoints & add tests
* fiatRates: add getFiatRatesTickersList websocket endpoint & test
* fiatRates: make websocket getFiatRatesByDate accept an array of dates, add more tests
* fiatRates: remove getFiatRatesForBlockID from websocket endpoints
* fiatRates: remove "if test", use custom startTime instead
Update tests and mock data
* fiatRates: finalize websocket functionality
add "date" parameter to TickerList
return data timestamps where needed
fix sync bugs (nil timestamp, duplicate save)
* fiatRates: add FiatRates configs for different coins
* worker.go: make GetBlockInfoFromBlockID private again
* fiatRates: wait & retry on errors, remove Ping function
* websocket.go: remove incorrect comment
* fiatRates: move coingecko-related code to a separate file, use interface
* fiatRates: if the new rates are the same as previous, try five more times, and only then store them
* coingecko: fix getting actual rates, add a timestamp parameter to get uncached responses
* vertcoin_testnet.json: remove fiat rates parameters
* fiat_rates: add timestamp to log message about skipping the repeating rates
2019-12-17 02:40:02 -07:00
"strings"
2018-08-24 08:17:43 -06:00
"time"
2018-06-29 18:02:16 -06:00
"github.com/golang/glog"
2018-08-24 08:17:43 -06:00
"github.com/juju/errors"
2018-06-26 05:02:53 -06:00
)
// Worker is handle to api worker
type Worker struct {
db * db . RocksDB
txCache * db . TxCache
chain bchain . BlockChain
chainParser bchain . BlockChainParser
2018-11-06 16:24:53 -07:00
chainType bchain . ChainType
2019-03-25 09:43:57 -06:00
mempool bchain . Mempool
2018-06-27 16:36:56 -06:00
is * common . InternalState
2018-06-26 05:02:53 -06:00
}
// NewWorker creates new api worker
2019-03-25 09:43:57 -06:00
func NewWorker ( db * db . RocksDB , chain bchain . BlockChain , mempool bchain . Mempool , txCache * db . TxCache , is * common . InternalState ) ( * Worker , error ) {
2018-06-26 05:02:53 -06:00
w := & Worker {
db : db ,
txCache : txCache ,
chain : chain ,
chainParser : chain . GetChainParser ( ) ,
2018-11-06 16:24:53 -07:00
chainType : chain . GetChainParser ( ) . GetChainType ( ) ,
2019-03-25 09:43:57 -06:00
mempool : mempool ,
2018-06-27 16:36:56 -06:00
is : is ,
2018-06-26 05:02:53 -06:00
}
return w , nil
}
2018-08-31 07:23:04 -06:00
func ( w * Worker ) getAddressesFromVout ( vout * bchain . Vout ) ( bchain . AddressDescriptor , [ ] string , bool , error ) {
2018-08-28 16:25:26 -06:00
addrDesc , err := w . chainParser . GetAddrDescFromVout ( vout )
if err != nil {
2018-08-31 07:23:04 -06:00
return nil , nil , false , err
2018-08-28 16:25:26 -06:00
}
2018-08-31 07:23:04 -06:00
a , s , err := w . chainParser . GetAddressesFromAddrDesc ( addrDesc )
return addrDesc , a , s , err
2018-08-28 16:25:26 -06:00
}
2018-09-27 05:44:13 -06:00
// setSpendingTxToVout is helper function, that finds transaction that spent given output and sets it to the output
2018-11-14 06:46:42 -07:00
// there is no direct index for the operation, it must be found using addresses -> txaddresses -> tx
2018-10-08 06:55:21 -06:00
func ( w * Worker ) setSpendingTxToVout ( vout * Vout , txid string , height uint32 ) error {
2019-02-12 07:15:10 -07:00
err := w . db . GetAddrDescTransactions ( vout . AddrDesc , height , maxUint32 , func ( t string , height uint32 , indexes [ ] int32 ) error {
2019-01-03 09:19:56 -07:00
for _ , index := range indexes {
// take only inputs
if index < 0 {
index = ^ index
tsp , err := w . db . GetTxAddresses ( t )
if err != nil {
return err
} else if tsp == nil {
glog . Warning ( "DB inconsistency: tx " , t , ": not found in txAddresses" )
} else if len ( tsp . Inputs ) > int ( index ) {
if tsp . Inputs [ index ] . ValueSat . Cmp ( ( * big . Int ) ( vout . ValueSat ) ) == 0 {
spentTx , spentHeight , err := w . txCache . GetTransaction ( t )
if err != nil {
glog . Warning ( "Tx " , t , ": not found" )
} else {
if len ( spentTx . Vin ) > int ( index ) {
if spentTx . Vin [ index ] . Txid == txid {
vout . SpentTxID = t
vout . SpentHeight = int ( spentHeight )
vout . SpentIndex = int ( index )
return & db . StopIteration { }
}
2018-09-27 05:44:13 -06:00
}
}
}
}
}
}
return nil
} )
return err
}
// GetSpendingTxid returns transaction id of transaction that spent given output
func ( w * Worker ) GetSpendingTxid ( txid string , n int ) ( string , error ) {
start := time . Now ( )
2018-11-13 02:31:27 -07:00
tx , err := w . GetTransaction ( txid , false , false )
2018-09-27 05:44:13 -06:00
if err != nil {
return "" , err
}
if n >= len ( tx . Vout ) || n < 0 {
2018-11-05 03:26:23 -07:00
return "" , NewAPIError ( fmt . Sprintf ( "Passed incorrect vout index %v for tx %v, len vout %v" , n , tx . Txid , len ( tx . Vout ) ) , false )
2018-09-27 05:44:13 -06:00
}
2018-10-08 06:55:21 -06:00
err = w . setSpendingTxToVout ( & tx . Vout [ n ] , tx . Txid , uint32 ( tx . Blockheight ) )
2018-09-27 05:44:13 -06:00
if err != nil {
return "" , err
}
glog . Info ( "GetSpendingTxid " , txid , " " , n , " finished in " , time . Since ( start ) )
return tx . Vout [ n ] . SpentTxID , nil
}
2018-06-27 16:36:56 -06:00
// GetTransaction reads transaction data from txid
2018-11-25 16:20:01 -07:00
func ( w * Worker ) GetTransaction ( txid string , spendingTxs bool , specificJSON bool ) ( * Tx , error ) {
2018-10-08 06:55:21 -06:00
bchainTx , height , err := w . txCache . GetTransaction ( txid )
2018-06-26 05:02:53 -06:00
if err != nil {
2019-01-10 08:39:36 -07:00
if err == bchain . ErrTxNotFound {
return nil , NewAPIError ( fmt . Sprintf ( "Transaction '%v' not found" , txid ) , true )
}
return nil , NewAPIError ( fmt . Sprintf ( "Transaction '%v' not found (%v)" , txid , err ) , true )
2018-06-26 05:02:53 -06:00
}
2018-12-10 09:22:37 -07:00
return w . GetTransactionFromBchainTx ( bchainTx , height , spendingTxs , specificJSON )
}
// GetTransactionFromBchainTx reads transaction data from txid
2019-10-09 06:49:06 -06:00
func ( w * Worker ) GetTransactionFromBchainTx ( bchainTx * bchain . Tx , height int , spendingTxs bool , specificJSON bool ) ( * Tx , error ) {
2018-12-10 09:22:37 -07:00
var err error
2018-11-14 06:46:42 -07:00
var ta * db . TxAddresses
2018-12-20 05:18:38 -07:00
var tokens [ ] TokenTransfer
2018-12-12 16:41:58 -07:00
var ethSpecific * EthereumSpecific
2018-06-26 05:02:53 -06:00
var blockhash string
if bchainTx . Confirmations > 0 {
2018-11-06 16:24:53 -07:00
if w . chainType == bchain . ChainBitcoinType {
2018-12-10 09:22:37 -07:00
ta , err = w . db . GetTxAddresses ( bchainTx . Txid )
2018-11-06 16:24:53 -07:00
if err != nil {
2018-12-10 09:22:37 -07:00
return nil , errors . Annotatef ( err , "GetTxAddresses %v" , bchainTx . Txid )
2018-11-06 16:24:53 -07:00
}
2018-11-14 06:46:42 -07:00
}
2019-10-09 06:49:06 -06:00
blockhash , err = w . db . GetBlockHash ( uint32 ( height ) )
2018-06-26 05:02:53 -06:00
if err != nil {
2018-08-24 08:17:43 -06:00
return nil , errors . Annotatef ( err , "GetBlockHash %v" , height )
2018-06-26 05:02:53 -06:00
}
}
2018-07-24 07:58:37 -06:00
var valInSat , valOutSat , feesSat big . Int
2018-12-14 08:42:35 -07:00
var pValInSat * big . Int
2018-06-26 05:02:53 -06:00
vins := make ( [ ] Vin , len ( bchainTx . Vin ) )
2019-07-30 08:43:13 -06:00
rbf := false
2018-06-26 05:02:53 -06:00
for i := range bchainTx . Vin {
bchainVin := & bchainTx . Vin [ i ]
vin := & vins [ i ]
vin . Txid = bchainVin . Txid
vin . N = i
vin . Vout = bchainVin . Vout
2018-10-06 15:35:03 -06:00
vin . Sequence = int64 ( bchainVin . Sequence )
2019-07-30 08:43:13 -06:00
// detect explicit Replace-by-Fee transactions as defined by BIP125
if bchainTx . Confirmations == 0 && bchainVin . Sequence < 0xffffffff - 1 {
rbf = true
}
2018-12-19 05:59:18 -07:00
vin . Hex = bchainVin . ScriptSig . Hex
2019-01-17 12:30:18 -07:00
vin . Coinbase = bchainVin . Coinbase
2018-11-06 16:24:53 -07:00
if w . chainType == bchain . ChainBitcoinType {
// bchainVin.Txid=="" is coinbase transaction
if bchainVin . Txid != "" {
// load spending addresses from TxAddresses
tas , err := w . db . GetTxAddresses ( bchainVin . Txid )
2018-08-27 07:36:33 -06:00
if err != nil {
2018-11-06 16:24:53 -07:00
return nil , errors . Annotatef ( err , "GetTxAddresses %v" , bchainVin . Txid )
2018-08-27 07:36:33 -06:00
}
2018-11-06 16:24:53 -07:00
if tas == nil {
// try to load from backend
otx , _ , err := w . txCache . GetTransaction ( bchainVin . Txid )
2018-08-28 16:25:26 -06:00
if err != nil {
2019-01-10 08:39:36 -07:00
if err == bchain . ErrTxNotFound {
// try to get AddrDesc using coin specific handling and continue processing the tx
vin . AddrDesc = w . chainParser . GetAddrDescForUnknownInput ( bchainTx , i )
2019-06-19 06:15:56 -06:00
vin . Addresses , vin . IsAddress , err = w . chainParser . GetAddressesFromAddrDesc ( vin . AddrDesc )
2019-01-10 09:24:29 -07:00
if err != nil {
glog . Warning ( "GetAddressesFromAddrDesc tx " , bchainVin . Txid , ", addrDesc " , vin . AddrDesc , ": " , err )
}
2019-01-10 08:39:36 -07:00
continue
}
2018-11-06 16:24:53 -07:00
return nil , errors . Annotatef ( err , "txCache.GetTransaction %v" , bchainVin . Txid )
2018-08-27 07:36:33 -06:00
}
2019-01-10 09:24:29 -07:00
// mempool transactions are not in TxAddresses but confirmed should be there, log a problem
if bchainTx . Confirmations > 0 {
2019-05-23 07:56:40 -06:00
inSync , _ , _ := w . is . GetSyncState ( )
// backend can report tx as confirmed, however blockbook is still syncing (!inSync), in this case do not log a problem
if bchainTx . Confirmations != 1 || inSync {
glog . Warning ( "DB inconsistency: tx " , bchainVin . Txid , ": not found in txAddresses" )
}
2019-01-10 09:24:29 -07:00
}
2018-11-06 16:24:53 -07:00
if len ( otx . Vout ) > int ( vin . Vout ) {
vout := & otx . Vout [ vin . Vout ]
2018-12-12 16:41:58 -07:00
vin . ValueSat = ( * Amount ) ( & vout . ValueSat )
2019-06-19 06:15:56 -06:00
vin . AddrDesc , vin . Addresses , vin . IsAddress , err = w . getAddressesFromVout ( vout )
2018-11-06 16:24:53 -07:00
if err != nil {
glog . Errorf ( "getAddressesFromVout error %v, vout %+v" , err , vout )
}
}
} else {
if len ( tas . Outputs ) > int ( vin . Vout ) {
output := & tas . Outputs [ vin . Vout ]
2018-12-12 16:41:58 -07:00
vin . ValueSat = ( * Amount ) ( & output . ValueSat )
2018-11-06 16:24:53 -07:00
vin . AddrDesc = output . AddrDesc
2019-06-19 06:15:56 -06:00
vin . Addresses , vin . IsAddress , err = output . Addresses ( w . chainParser )
2018-11-06 16:24:53 -07:00
if err != nil {
glog . Errorf ( "output.Addresses error %v, tx %v, output %v" , err , bchainVin . Txid , i )
}
}
}
2019-01-17 11:58:59 -07:00
if vin . ValueSat != nil {
valInSat . Add ( & valInSat , ( * big . Int ) ( vin . ValueSat ) )
}
2018-11-06 16:24:53 -07:00
}
} else if w . chainType == bchain . ChainEthereumType {
if len ( bchainVin . Addresses ) > 0 {
vin . AddrDesc , err = w . chainParser . GetAddrDescFromAddress ( bchainVin . Addresses [ 0 ] )
if err != nil {
glog . Errorf ( "GetAddrDescFromAddress error %v, tx %v, bchainVin %v" , err , bchainTx . Txid , bchainVin )
2018-06-28 13:18:52 -06:00
}
2018-11-06 16:24:53 -07:00
vin . Addresses = bchainVin . Addresses
2019-06-19 06:15:56 -06:00
vin . IsAddress = true
2018-06-26 05:02:53 -06:00
}
}
}
vouts := make ( [ ] Vout , len ( bchainTx . Vout ) )
for i := range bchainTx . Vout {
bchainVout := & bchainTx . Vout [ i ]
vout := & vouts [ i ]
vout . N = i
2018-12-12 16:41:58 -07:00
vout . ValueSat = ( * Amount ) ( & bchainVout . ValueSat )
2018-07-24 07:58:37 -06:00
valOutSat . Add ( & valOutSat , & bchainVout . ValueSat )
2018-12-19 05:59:18 -07:00
vout . Hex = bchainVout . ScriptPubKey . Hex
2019-06-19 06:15:56 -06:00
vout . AddrDesc , vout . Addresses , vout . IsAddress , err = w . getAddressesFromVout ( bchainVout )
2018-10-22 02:39:29 -06:00
if err != nil {
2018-10-22 03:11:27 -06:00
glog . V ( 2 ) . Infof ( "getAddressesFromVout error %v, %v, output %v" , err , bchainTx . Txid , bchainVout . N )
2018-10-22 02:39:29 -06:00
}
2018-08-31 07:23:04 -06:00
if ta != nil {
vout . Spent = ta . Outputs [ i ] . Spent
if spendingTxs && vout . Spent {
2019-10-09 06:49:06 -06:00
err = w . setSpendingTxToVout ( vout , bchainTx . Txid , uint32 ( height ) )
2018-09-03 09:25:57 -06:00
if err != nil {
2018-12-19 05:59:18 -07:00
glog . Errorf ( "setSpendingTxToVout error %v, %v, output %v" , err , vout . AddrDesc , vout . N )
2018-09-03 09:25:57 -06:00
}
2018-08-31 07:23:04 -06:00
}
2018-06-26 05:02:53 -06:00
}
}
2018-11-28 06:27:02 -07:00
if w . chainType == bchain . ChainBitcoinType {
// for coinbase transactions valIn is 0
feesSat . Sub ( & valInSat , & valOutSat )
if feesSat . Sign ( ) == - 1 {
feesSat . SetUint64 ( 0 )
}
2018-12-14 08:42:35 -07:00
pValInSat = & valInSat
2018-11-28 06:27:02 -07:00
} else if w . chainType == bchain . ChainEthereumType {
2018-12-19 02:06:25 -07:00
ets , err := w . chainParser . EthereumTypeGetErc20FromTx ( bchainTx )
2018-11-28 06:27:02 -07:00
if err != nil {
glog . Errorf ( "GetErc20FromTx error %v, %v" , err , bchainTx )
}
2018-12-20 05:18:38 -07:00
tokens = make ( [ ] TokenTransfer , len ( ets ) )
2018-11-28 06:27:02 -07:00
for i := range ets {
e := & ets [ i ]
cd , err := w . chainParser . GetAddrDescFromAddress ( e . Contract )
if err != nil {
glog . Errorf ( "GetAddrDescFromAddress error %v, contract %v" , err , e . Contract )
continue
}
erc20c , err := w . chain . EthereumTypeGetErc20ContractInfo ( cd )
if err != nil {
glog . Errorf ( "GetErc20ContractInfo error %v, contract %v" , err , e . Contract )
2018-12-03 07:48:07 -07:00
}
if erc20c == nil {
erc20c = & bchain . Erc20Contract { Name : e . Contract }
2018-11-28 06:27:02 -07:00
}
2018-12-20 05:18:38 -07:00
tokens [ i ] = TokenTransfer {
2019-01-07 07:45:00 -07:00
Type : ERC20TokenType ,
2018-12-20 05:18:38 -07:00
Token : e . Contract ,
2018-11-28 06:27:02 -07:00
From : e . From ,
To : e . To ,
2018-12-12 16:41:58 -07:00
Decimals : erc20c . Decimals ,
2018-12-20 05:18:38 -07:00
Value : ( * Amount ) ( & e . Tokens ) ,
2018-11-28 06:27:02 -07:00
Name : erc20c . Name ,
Symbol : erc20c . Symbol ,
}
}
2018-12-12 16:41:58 -07:00
ethTxData := eth . GetEthereumTxData ( bchainTx )
2018-11-28 06:56:45 -07:00
// mempool txs do not have fees yet
2018-12-12 16:41:58 -07:00
if ethTxData . GasUsed != nil {
feesSat . Mul ( ethTxData . GasPrice , ethTxData . GasUsed )
2018-11-28 06:56:45 -07:00
}
2018-11-28 06:27:02 -07:00
if len ( bchainTx . Vout ) > 0 {
2018-12-14 08:42:35 -07:00
valOutSat = bchainTx . Vout [ 0 ] . ValueSat
2018-11-28 06:27:02 -07:00
}
2018-12-12 16:41:58 -07:00
ethSpecific = & EthereumSpecific {
GasLimit : ethTxData . GasLimit ,
GasPrice : ( * Amount ) ( ethTxData . GasPrice ) ,
GasUsed : ethTxData . GasUsed ,
Nonce : ethTxData . Nonce ,
Status : ethTxData . Status ,
}
2018-06-28 13:18:52 -06:00
}
2018-06-26 05:02:53 -06:00
// for now do not return size, we would have to compute vsize of segwit transactions
// size:=len(bchainTx.Hex) / 2
2018-11-25 16:20:01 -07:00
var sj json . RawMessage
if specificJSON {
sj , err = w . chain . GetTransactionSpecific ( bchainTx )
2018-11-13 02:31:27 -07:00
if err != nil {
return nil , err
}
}
2019-04-03 06:08:36 -06:00
// for mempool transaction get first seen time
if bchainTx . Confirmations == 0 {
bchainTx . Blocktime = int64 ( w . mempool . GetTransactionTime ( bchainTx . Txid ) )
}
2018-06-26 05:02:53 -06:00
r := & Tx {
2018-11-13 02:31:27 -07:00
Blockhash : blockhash ,
2019-10-09 06:49:06 -06:00
Blockheight : height ,
2018-11-13 02:31:27 -07:00
Blocktime : bchainTx . Blocktime ,
Confirmations : bchainTx . Confirmations ,
2018-12-12 16:41:58 -07:00
FeesSat : ( * Amount ) ( & feesSat ) ,
2018-11-13 02:31:27 -07:00
Locktime : bchainTx . LockTime ,
Txid : bchainTx . Txid ,
2018-12-14 08:42:35 -07:00
ValueInSat : ( * Amount ) ( pValInSat ) ,
2018-12-12 16:41:58 -07:00
ValueOutSat : ( * Amount ) ( & valOutSat ) ,
2018-11-13 02:31:27 -07:00
Version : bchainTx . Version ,
Hex : bchainTx . Hex ,
2019-07-30 08:43:13 -06:00
Rbf : rbf ,
2018-11-13 02:31:27 -07:00
Vin : vins ,
Vout : vouts ,
2018-11-25 16:20:01 -07:00
CoinSpecificData : bchainTx . CoinSpecificData ,
CoinSpecificJSON : sj ,
2018-12-20 05:18:38 -07:00
TokenTransfers : tokens ,
2018-11-28 06:27:02 -07:00
EthereumSpecific : ethSpecific ,
2018-06-26 05:02:53 -06:00
}
return r , nil
}
2018-06-29 18:02:16 -06:00
2019-01-09 03:48:19 -07:00
func ( w * Worker ) getAddressTxids ( addrDesc bchain . AddressDescriptor , mempool bool , filter * AddressFilter , maxResults int ) ( [ ] string , error ) {
2018-06-29 18:02:16 -06:00
var err error
2018-11-14 15:02:42 -07:00
txids := make ( [ ] string , 0 , 4 )
2019-01-03 09:19:56 -07:00
var callback db . GetTransactionsCallback
if filter . Vout == AddressFilterVoutOff {
callback = func ( txid string , height uint32 , indexes [ ] int32 ) error {
2018-06-29 18:02:16 -06:00
txids = append ( txids , txid )
2019-01-09 03:48:19 -07:00
if len ( txids ) >= maxResults {
return & db . StopIteration { }
}
2019-01-03 09:19:56 -07:00
return nil
}
} else {
callback = func ( txid string , height uint32 , indexes [ ] int32 ) error {
for _ , index := range indexes {
vout := index
if vout < 0 {
vout = ^ vout
}
if ( filter . Vout == AddressFilterVoutInputs && index < 0 ) ||
( filter . Vout == AddressFilterVoutOutputs && index >= 0 ) ||
( vout == int32 ( filter . Vout ) ) {
txids = append ( txids , txid )
2019-01-09 03:48:19 -07:00
if len ( txids ) >= maxResults {
return & db . StopIteration { }
}
2019-01-03 09:19:56 -07:00
break
}
}
return nil
2018-12-04 03:54:15 -07:00
}
}
if mempool {
2019-01-03 09:19:56 -07:00
uniqueTxs := make ( map [ string ] struct { } )
2019-03-25 09:43:57 -06:00
o , err := w . mempool . GetAddrDescTransactions ( addrDesc )
2018-06-29 18:02:16 -06:00
if err != nil {
return nil , err
}
2018-12-04 03:54:15 -07:00
for _ , m := range o {
2019-01-03 09:19:56 -07:00
if _ , found := uniqueTxs [ m . Txid ] ; ! found {
l := len ( txids )
callback ( m . Txid , 0 , [ ] int32 { m . Vout } )
if len ( txids ) > l {
uniqueTxs [ m . Txid ] = struct { } { }
}
2018-12-04 03:54:15 -07:00
}
}
2018-06-29 18:02:16 -06:00
} else {
2018-12-14 08:42:35 -07:00
to := filter . ToHeight
if to == 0 {
2019-02-12 07:15:10 -07:00
to = maxUint32
2018-12-14 08:42:35 -07:00
}
2019-01-03 09:19:56 -07:00
err = w . db . GetAddrDescTransactions ( addrDesc , filter . FromHeight , to , callback )
2018-06-29 18:02:16 -06:00
if err != nil {
return nil , err
}
}
return txids , nil
}
2018-08-31 07:23:04 -06:00
func ( t * Tx ) getAddrVoutValue ( addrDesc bchain . AddressDescriptor ) * big . Int {
2018-07-24 07:58:37 -06:00
var val big . Int
2018-06-29 18:02:16 -06:00
for _ , vout := range t . Vout {
2018-12-19 05:59:18 -07:00
if bytes . Equal ( vout . AddrDesc , addrDesc ) && vout . ValueSat != nil {
2018-12-12 16:41:58 -07:00
val . Add ( & val , ( * big . Int ) ( vout . ValueSat ) )
2018-06-29 18:02:16 -06:00
}
}
2018-07-24 07:58:37 -06:00
return & val
2018-06-29 18:02:16 -06:00
}
2018-08-31 07:23:04 -06:00
func ( t * Tx ) getAddrVinValue ( addrDesc bchain . AddressDescriptor ) * big . Int {
2018-07-24 07:58:37 -06:00
var val big . Int
2018-06-29 18:02:16 -06:00
for _ , vin := range t . Vin {
2018-12-14 08:42:35 -07:00
if bytes . Equal ( vin . AddrDesc , addrDesc ) && vin . ValueSat != nil {
2018-12-12 16:41:58 -07:00
val . Add ( & val , ( * big . Int ) ( vin . ValueSat ) )
2018-06-29 18:02:16 -06:00
}
}
2018-07-24 07:58:37 -06:00
return & val
2018-06-29 18:02:16 -06:00
}
2018-12-20 09:33:13 -07:00
// GetUniqueTxids removes duplicate transactions
func GetUniqueTxids ( txids [ ] string ) [ ] string {
ut := make ( [ ] string , len ( txids ) )
2018-07-20 11:07:43 -06:00
txidsMap := make ( map [ string ] struct { } )
2018-12-20 09:33:13 -07:00
i := 0
2018-07-20 11:07:43 -06:00
for _ , txid := range txids {
_ , e := txidsMap [ txid ]
if ! e {
ut [ i ] = txid
2018-12-20 09:33:13 -07:00
i ++
2018-07-20 11:07:43 -06:00
txidsMap [ txid ] = struct { } { }
}
}
2018-12-20 09:33:13 -07:00
return ut [ 0 : i ]
2018-07-20 11:07:43 -06:00
}
2018-08-21 16:48:53 -06:00
func ( w * Worker ) txFromTxAddress ( txid string , ta * db . TxAddresses , bi * db . BlockInfo , bestheight uint32 ) * Tx {
2018-08-28 16:25:26 -06:00
var err error
2018-08-21 16:48:53 -06:00
var valInSat , valOutSat , feesSat big . Int
vins := make ( [ ] Vin , len ( ta . Inputs ) )
for i := range ta . Inputs {
tai := & ta . Inputs [ i ]
vin := & vins [ i ]
vin . N = i
2018-12-12 16:41:58 -07:00
vin . ValueSat = ( * Amount ) ( & tai . ValueSat )
valInSat . Add ( & valInSat , & tai . ValueSat )
2019-06-19 06:15:56 -06:00
vin . Addresses , vin . IsAddress , err = tai . Addresses ( w . chainParser )
2018-08-28 16:25:26 -06:00
if err != nil {
2018-09-03 06:10:28 -06:00
glog . Errorf ( "tai.Addresses error %v, tx %v, input %v, tai %+v" , err , txid , i , tai )
2018-08-21 16:48:53 -06:00
}
}
vouts := make ( [ ] Vout , len ( ta . Outputs ) )
for i := range ta . Outputs {
tao := & ta . Outputs [ i ]
vout := & vouts [ i ]
vout . N = i
2018-12-12 16:41:58 -07:00
vout . ValueSat = ( * Amount ) ( & tao . ValueSat )
valOutSat . Add ( & valOutSat , & tao . ValueSat )
2019-06-19 06:15:56 -06:00
vout . Addresses , vout . IsAddress , err = tao . Addresses ( w . chainParser )
2018-08-28 16:25:26 -06:00
if err != nil {
2018-09-03 06:10:28 -06:00
glog . Errorf ( "tai.Addresses error %v, tx %v, output %v, tao %+v" , err , txid , i , tao )
2018-08-21 16:48:53 -06:00
}
2018-09-03 09:25:57 -06:00
vout . Spent = tao . Spent
2018-08-21 16:48:53 -06:00
}
// for coinbase transactions valIn is 0
feesSat . Sub ( & valInSat , & valOutSat )
if feesSat . Sign ( ) == - 1 {
feesSat . SetUint64 ( 0 )
}
r := & Tx {
2018-08-23 15:20:07 -06:00
Blockhash : bi . Hash ,
2018-08-21 16:48:53 -06:00
Blockheight : int ( ta . Height ) ,
2018-08-23 15:20:07 -06:00
Blocktime : bi . Time ,
2018-08-21 16:48:53 -06:00
Confirmations : bestheight - ta . Height + 1 ,
2018-12-12 16:41:58 -07:00
FeesSat : ( * Amount ) ( & feesSat ) ,
2018-08-21 16:48:53 -06:00
Txid : txid ,
2018-12-12 16:41:58 -07:00
ValueInSat : ( * Amount ) ( & valInSat ) ,
ValueOutSat : ( * Amount ) ( & valOutSat ) ,
2018-08-21 16:48:53 -06:00
Vin : vins ,
Vout : vouts ,
}
return r
}
2018-09-14 04:55:26 -06:00
func computePaging ( count , page , itemsOnPage int ) ( Paging , int , int , int ) {
from := page * itemsOnPage
totalPages := ( count - 1 ) / itemsOnPage
if totalPages < 0 {
totalPages = 0
}
if from >= count {
page = totalPages
}
from = page * itemsOnPage
to := ( page + 1 ) * itemsOnPage
if to > count {
to = count
}
return Paging {
ItemsOnPage : itemsOnPage ,
Page : page + 1 ,
TotalPages : totalPages + 1 ,
} , from , to , page
}
2019-02-28 07:07:07 -07:00
func ( w * Worker ) getEthereumTypeAddressBalances ( addrDesc bchain . AddressDescriptor , details AccountDetails , filter * AddressFilter ) ( * db . AddrBalance , [ ] Token , * bchain . Erc20Contract , uint64 , int , int , error ) {
2018-12-03 07:48:07 -07:00
var (
2019-01-09 03:48:19 -07:00
ba * db . AddrBalance
tokens [ ] Token
ci * bchain . Erc20Contract
n uint64
nonContractTxs int
2018-12-03 07:48:07 -07:00
)
2019-01-09 03:48:19 -07:00
// unknown number of results for paging
totalResults := - 1
2018-12-03 07:48:07 -07:00
ca , err := w . db . GetAddrDescContracts ( addrDesc )
if err != nil {
2019-01-09 03:48:19 -07:00
return nil , nil , nil , 0 , 0 , 0 , NewAPIError ( fmt . Sprintf ( "Address not found, %v" , err ) , true )
2018-12-03 07:48:07 -07:00
}
2019-02-19 12:14:04 -07:00
b , err := w . chain . EthereumTypeGetBalance ( addrDesc )
if err != nil {
return nil , nil , nil , 0 , 0 , 0 , errors . Annotatef ( err , "EthereumTypeGetBalance %v" , addrDesc )
}
2018-12-03 07:48:07 -07:00
if ca != nil {
ba = & db . AddrBalance {
2019-01-03 09:19:56 -07:00
Txs : uint32 ( ca . TotalTxs ) ,
2018-12-03 07:48:07 -07:00
}
2018-12-06 05:14:46 -07:00
if b != nil {
ba . BalanceSat = * b
}
n , err = w . chain . EthereumTypeGetNonce ( addrDesc )
if err != nil {
2019-01-09 03:48:19 -07:00
return nil , nil , nil , 0 , 0 , 0 , errors . Annotatef ( err , "EthereumTypeGetNonce %v" , addrDesc )
2018-12-06 05:14:46 -07:00
}
2018-12-14 08:42:35 -07:00
var filterDesc bchain . AddressDescriptor
if filter . Contract != "" {
filterDesc , err = w . chainParser . GetAddrDescFromAddress ( filter . Contract )
if err != nil {
2019-01-09 03:48:19 -07:00
return nil , nil , nil , 0 , 0 , 0 , NewAPIError ( fmt . Sprintf ( "Invalid contract filter, %v" , err ) , true )
2018-12-14 08:42:35 -07:00
}
}
2019-02-28 07:07:07 -07:00
if details > AccountDetailsBasic {
tokens = make ( [ ] Token , len ( ca . Contracts ) )
var j int
for i , c := range ca . Contracts {
if len ( filterDesc ) > 0 {
if ! bytes . Equal ( filterDesc , c . Contract ) {
continue
}
// filter only transactions of this contract
filter . Vout = i + 1
2018-12-03 07:48:07 -07:00
}
2019-02-28 07:07:07 -07:00
validContract := true
ci , err := w . chain . EthereumTypeGetErc20ContractInfo ( c . Contract )
2018-12-03 07:48:07 -07:00
if err != nil {
2019-02-28 07:07:07 -07:00
return nil , nil , nil , 0 , 0 , 0 , errors . Annotatef ( err , "EthereumTypeGetErc20ContractInfo %v" , c . Contract )
2018-12-04 06:23:11 -07:00
}
2019-02-28 07:07:07 -07:00
if ci == nil {
ci = & bchain . Erc20Contract { }
addresses , _ , _ := w . chainParser . GetAddressesFromAddrDesc ( c . Contract )
if len ( addresses ) > 0 {
ci . Contract = addresses [ 0 ]
ci . Name = addresses [ 0 ]
}
validContract = false
}
// do not read contract balances etc in case of Basic option
if details >= AccountDetailsTokenBalances && validContract {
b , err = w . chain . EthereumTypeGetErc20ContractBalance ( addrDesc , c . Contract )
if err != nil {
// return nil, nil, nil, errors.Annotatef(err, "EthereumTypeGetErc20ContractBalance %v %v", addrDesc, c.Contract)
glog . Warningf ( "EthereumTypeGetErc20ContractBalance addr %v, contract %v, %v" , addrDesc , c . Contract , err )
}
} else {
b = nil
}
tokens [ j ] = Token {
Type : ERC20TokenType ,
BalanceSat : ( * Amount ) ( b ) ,
Contract : ci . Contract ,
Name : ci . Name ,
Symbol : ci . Symbol ,
Transfers : int ( c . Txs ) ,
Decimals : ci . Decimals ,
ContractIndex : strconv . Itoa ( i + 1 ) ,
}
j ++
2018-12-03 07:48:07 -07:00
}
2019-02-28 07:07:07 -07:00
tokens = tokens [ : j ]
2018-12-03 07:48:07 -07:00
}
2018-12-06 05:14:46 -07:00
ci , err = w . chain . EthereumTypeGetErc20ContractInfo ( addrDesc )
if err != nil {
2019-01-09 03:48:19 -07:00
return nil , nil , nil , 0 , 0 , 0 , err
}
if filter . FromHeight == 0 && filter . ToHeight == 0 {
// compute total results for paging
if filter . Vout == AddressFilterVoutOff {
totalResults = int ( ca . TotalTxs )
} else if filter . Vout == 0 {
totalResults = int ( ca . NonContractTxs )
} else if filter . Vout > 0 && filter . Vout - 1 < len ( ca . Contracts ) {
totalResults = int ( ca . Contracts [ filter . Vout - 1 ] . Txs )
}
2018-12-06 05:14:46 -07:00
}
2019-01-09 03:48:19 -07:00
nonContractTxs = int ( ca . NonContractTxs )
2019-02-19 12:14:04 -07:00
} else {
// addresses without any normal transactions can have internal transactions and therefore balance
if b != nil {
ba = & db . AddrBalance {
BalanceSat : * b ,
}
}
2018-12-03 07:48:07 -07:00
}
2019-01-09 03:48:19 -07:00
return ba , tokens , ci , n , nonContractTxs , totalResults , nil
2018-12-03 07:48:07 -07:00
}
2019-04-28 03:54:12 -06:00
func ( w * Worker ) txFromTxid ( txid string , bestheight uint32 , option AccountDetails , blockInfo * db . BlockInfo ) ( * Tx , error ) {
2019-02-03 15:42:44 -07:00
var tx * Tx
var err error
// only ChainBitcoinType supports TxHistoryLight
2019-02-28 07:07:07 -07:00
if option == AccountDetailsTxHistoryLight && w . chainType == bchain . ChainBitcoinType {
2019-02-03 15:42:44 -07:00
ta , err := w . db . GetTxAddresses ( txid )
if err != nil {
return nil , errors . Annotatef ( err , "GetTxAddresses %v" , txid )
}
if ta == nil {
glog . Warning ( "DB inconsistency: tx " , txid , ": not found in txAddresses" )
2019-05-23 07:56:40 -06:00
// as fallback, get tx from backend
tx , err = w . GetTransaction ( txid , false , true )
2019-04-28 03:54:12 -06:00
if err != nil {
2019-05-23 07:56:40 -06:00
return nil , errors . Annotatef ( err , "GetTransaction %v" , txid )
2019-04-28 03:54:12 -06:00
}
2019-05-23 07:56:40 -06:00
} else {
2019-04-28 03:54:12 -06:00
if blockInfo == nil {
2019-05-23 07:56:40 -06:00
blockInfo , err = w . db . GetBlockInfo ( ta . Height )
if err != nil {
return nil , errors . Annotatef ( err , "GetBlockInfo %v" , ta . Height )
}
if blockInfo == nil {
glog . Warning ( "DB inconsistency: block height " , ta . Height , ": not found in db" )
// provide empty BlockInfo to return the rest of tx data
blockInfo = & db . BlockInfo { }
}
2019-04-28 03:54:12 -06:00
}
2019-05-23 07:56:40 -06:00
tx = w . txFromTxAddress ( txid , ta , blockInfo , bestheight )
2019-02-03 15:42:44 -07:00
}
} else {
tx , err = w . GetTransaction ( txid , false , true )
if err != nil {
return nil , errors . Annotatef ( err , "GetTransaction %v" , txid )
}
}
return tx , nil
}
2019-02-07 09:28:45 -07:00
func ( w * Worker ) getAddrDescAndNormalizeAddress ( address string ) ( bchain . AddressDescriptor , string , error ) {
addrDesc , err := w . chainParser . GetAddrDescFromAddress ( address )
if err != nil {
return nil , "" , NewAPIError ( fmt . Sprintf ( "Invalid address, %v" , err ) , true )
}
// convert the address to the format defined by the parser
addresses , _ , err := w . chainParser . GetAddressesFromAddrDesc ( addrDesc )
if err != nil {
glog . V ( 2 ) . Infof ( "GetAddressesFromAddrDesc error %v, %v" , err , addrDesc )
}
if len ( addresses ) == 1 {
address = addresses [ 0 ]
}
return addrDesc , address , nil
}
2018-06-29 18:02:16 -06:00
// GetAddress computes address value and gets transactions for given address
2019-02-28 07:07:07 -07:00
func ( w * Worker ) GetAddress ( address string , page int , txsOnPage int , option AccountDetails , filter * AddressFilter ) ( * Address , error ) {
2018-08-24 08:17:43 -06:00
start := time . Now ( )
2018-09-05 07:13:45 -06:00
page --
2018-08-27 07:36:33 -06:00
if page < 0 {
page = 0
}
2018-11-25 16:20:01 -07:00
var (
2018-12-12 16:41:58 -07:00
ba * db . AddrBalance
2019-01-07 07:45:00 -07:00
tokens [ ] Token
2018-12-12 16:41:58 -07:00
erc20c * bchain . Erc20Contract
txm [ ] string
txs [ ] * Tx
txids [ ] string
pg Paging
uBalSat big . Int
totalReceived , totalSent * big . Int
nonce string
2019-03-06 09:23:06 -07:00
unconfirmedTxs int
2019-01-07 07:45:00 -07:00
nonTokenTxs int
2019-01-09 03:48:19 -07:00
totalResults int
2018-11-25 16:20:01 -07:00
)
2019-02-07 09:28:45 -07:00
addrDesc , address , err := w . getAddrDescAndNormalizeAddress ( address )
if err != nil {
return nil , err
}
2018-11-25 16:20:01 -07:00
if w . chainType == bchain . ChainEthereumType {
2018-12-06 05:14:46 -07:00
var n uint64
2019-01-09 03:48:19 -07:00
ba , tokens , erc20c , n , nonTokenTxs , totalResults , err = w . getEthereumTypeAddressBalances ( addrDesc , option , filter )
2018-11-25 16:20:01 -07:00
if err != nil {
2018-12-03 07:48:07 -07:00
return nil , err
2018-11-25 16:20:01 -07:00
}
2018-12-06 05:14:46 -07:00
nonce = strconv . Itoa ( int ( n ) )
2018-11-25 16:20:01 -07:00
} else {
// ba can be nil if the address is only in mempool!
2019-05-11 18:19:51 -06:00
ba , err = w . db . GetAddrDescBalance ( addrDesc , db . AddressBalanceDetailNoUTXO )
2018-11-25 16:20:01 -07:00
if err != nil {
return nil , NewAPIError ( fmt . Sprintf ( "Address not found, %v" , err ) , true )
}
2019-01-09 03:48:19 -07:00
if ba != nil {
// totalResults is known only if there is no filter
if filter . Vout == AddressFilterVoutOff && filter . FromHeight == 0 && filter . ToHeight == 0 {
totalResults = int ( ba . Txs )
} else {
totalResults = - 1
}
}
2018-11-25 16:20:01 -07:00
}
2019-02-07 09:28:45 -07:00
// if there are only unconfirmed transactions, there is no paging
if ba == nil {
ba = & db . AddrBalance { }
page = 0
}
2019-02-12 07:15:10 -07:00
// process mempool, only if toHeight is not specified
if filter . ToHeight == 0 && ! filter . OnlyConfirmed {
2019-02-07 09:28:45 -07:00
txm , err = w . getAddressTxids ( addrDesc , true , filter , maxInt )
2018-08-21 02:11:27 -06:00
if err != nil {
2019-02-07 09:28:45 -07:00
return nil , errors . Annotatef ( err , "getAddressTxids %v true" , addrDesc )
2018-08-21 02:11:27 -06:00
}
2019-02-07 09:28:45 -07:00
for _ , txid := range txm {
tx , err := w . GetTransaction ( txid , false , false )
// mempool transaction may fail
if err != nil || tx == nil {
glog . Warning ( "GetTransaction in mempool: " , err )
2018-11-25 16:20:01 -07:00
} else {
2019-02-07 09:28:45 -07:00
// skip already confirmed txs, mempool may be out of sync
if tx . Confirmations == 0 {
2019-03-06 09:23:06 -07:00
unconfirmedTxs ++
2019-02-07 09:28:45 -07:00
uBalSat . Add ( & uBalSat , tx . getAddrVoutValue ( addrDesc ) )
uBalSat . Sub ( & uBalSat , tx . getAddrVinValue ( addrDesc ) )
if page == 0 {
2019-02-28 07:07:07 -07:00
if option == AccountDetailsTxidHistory {
2019-02-07 09:28:45 -07:00
txids = append ( txids , tx . Txid )
2019-02-28 07:07:07 -07:00
} else if option >= AccountDetailsTxHistoryLight {
2019-02-07 09:28:45 -07:00
txs = append ( txs , tx )
2018-12-06 05:14:46 -07:00
}
}
2018-11-25 16:20:01 -07:00
}
2018-12-06 05:14:46 -07:00
}
2019-02-07 09:28:45 -07:00
}
}
// get tx history if requested by option or check mempool if there are some transactions for a new address
2019-02-28 07:07:07 -07:00
if option >= AccountDetailsTxidHistory {
2019-02-07 09:28:45 -07:00
txc , err := w . getAddressTxids ( addrDesc , false , filter , ( page + 1 ) * txsOnPage )
if err != nil {
return nil , errors . Annotatef ( err , "getAddressTxids %v false" , addrDesc )
}
bestheight , _ , err := w . db . GetBestBlock ( )
if err != nil {
return nil , errors . Annotatef ( err , "GetBestBlock" )
}
var from , to int
pg , from , to , page = computePaging ( len ( txc ) , page , txsOnPage )
if len ( txc ) >= txsOnPage {
if totalResults < 0 {
pg . TotalPages = - 1
} else {
pg , _ , _ , _ = computePaging ( totalResults , page , txsOnPage )
2018-12-06 05:14:46 -07:00
}
2019-02-07 09:28:45 -07:00
}
for i := from ; i < to ; i ++ {
txid := txc [ i ]
2019-02-28 07:07:07 -07:00
if option == AccountDetailsTxidHistory {
2019-02-07 09:28:45 -07:00
txids = append ( txids , txid )
} else {
2019-04-28 03:54:12 -06:00
tx , err := w . txFromTxid ( txid , bestheight , option , nil )
2019-02-07 09:28:45 -07:00
if err != nil {
return nil , err
}
txs = append ( txs , tx )
2018-09-04 06:34:15 -06:00
}
2018-08-21 16:48:53 -06:00
}
2018-08-21 02:11:27 -06:00
}
2018-12-06 05:14:46 -07:00
if w . chainType == bchain . ChainBitcoinType {
2018-12-12 16:41:58 -07:00
totalReceived = ba . ReceivedSat ( )
totalSent = & ba . SentSat
2018-09-04 06:34:15 -06:00
}
2018-08-21 02:11:27 -06:00
r := & Address {
2019-01-07 07:45:00 -07:00
Paging : pg ,
AddrStr : address ,
BalanceSat : ( * Amount ) ( & ba . BalanceSat ) ,
TotalReceivedSat : ( * Amount ) ( totalReceived ) ,
TotalSentSat : ( * Amount ) ( totalSent ) ,
Txs : int ( ba . Txs ) ,
NonTokenTxs : nonTokenTxs ,
UnconfirmedBalanceSat : ( * Amount ) ( & uBalSat ) ,
2019-03-06 09:23:06 -07:00
UnconfirmedTxs : unconfirmedTxs ,
2019-01-07 07:45:00 -07:00
Transactions : txs ,
Txids : txids ,
Tokens : tokens ,
Erc20Contract : erc20c ,
Nonce : nonce ,
2018-08-21 02:11:27 -06:00
}
2018-09-03 09:25:57 -06:00
glog . Info ( "GetAddress " , address , " finished in " , time . Since ( start ) )
2018-08-21 02:11:27 -06:00
return r , nil
}
2018-09-14 04:10:03 -06:00
2019-12-03 08:02:20 -07:00
func ( w * Worker ) balanceHistoryHeightsFromTo ( fromTime , toTime time . Time ) ( uint32 , uint32 , uint32 , uint32 ) {
2019-11-29 11:26:20 -07:00
fromUnix := uint32 ( 0 )
toUnix := maxUint32
2019-11-19 00:46:47 -07:00
fromHeight := uint32 ( 0 )
2019-11-29 11:26:20 -07:00
toHeight := maxUint32
2019-11-19 00:46:47 -07:00
if ! fromTime . IsZero ( ) {
2019-11-29 11:26:20 -07:00
fromUnix = uint32 ( fromTime . Unix ( ) )
fromHeight = w . is . GetBlockHeightOfTime ( fromUnix )
2019-11-19 00:46:47 -07:00
}
if ! toTime . IsZero ( ) {
2019-11-29 11:26:20 -07:00
toUnix = uint32 ( toTime . Unix ( ) )
toHeight = w . is . GetBlockHeightOfTime ( toUnix )
}
2019-12-03 08:02:20 -07:00
return fromUnix , fromHeight , toUnix , toHeight
}
func ( w * Worker ) balanceHistoryForTxid ( addrDesc bchain . AddressDescriptor , txid string , fromUnix , toUnix uint32 ) ( * BalanceHistory , error ) {
2019-12-10 13:08:27 -07:00
var time uint32
var err error
var ta * db . TxAddresses
var bchainTx * bchain . Tx
var height uint32
if w . chainType == bchain . ChainBitcoinType {
ta , err = w . db . GetTxAddresses ( txid )
if err != nil {
return nil , err
}
if ta == nil {
glog . Warning ( "DB inconsistency: tx " , txid , ": not found in txAddresses" )
return nil , nil
}
height = ta . Height
} else if w . chainType == bchain . ChainEthereumType {
var h int
bchainTx , h , err = w . txCache . GetTransaction ( txid )
if err != nil {
return nil , err
}
if bchainTx == nil {
glog . Warning ( "Inconsistency: tx " , txid , ": not found in the blockchain" )
return nil , nil
}
height = uint32 ( h )
2019-12-03 08:02:20 -07:00
}
2019-12-10 13:08:27 -07:00
time = w . is . GetBlockTime ( height )
2019-12-03 08:02:20 -07:00
if time < fromUnix || time >= toUnix {
return nil , nil
}
bh := BalanceHistory {
Time : time ,
Txs : 1 ,
SentSat : & Amount { } ,
ReceivedSat : & Amount { } ,
Txid : txid ,
}
2019-12-10 13:08:27 -07:00
if w . chainType == bchain . ChainBitcoinType {
for i := range ta . Inputs {
tai := & ta . Inputs [ i ]
if bytes . Equal ( addrDesc , tai . AddrDesc ) {
( * big . Int ) ( bh . SentSat ) . Add ( ( * big . Int ) ( bh . SentSat ) , & tai . ValueSat )
}
2019-12-03 08:02:20 -07:00
}
2019-12-10 13:08:27 -07:00
for i := range ta . Outputs {
tao := & ta . Outputs [ i ]
if bytes . Equal ( addrDesc , tao . AddrDesc ) {
( * big . Int ) ( bh . ReceivedSat ) . Add ( ( * big . Int ) ( bh . ReceivedSat ) , & tao . ValueSat )
}
}
} else if w . chainType == bchain . ChainEthereumType {
var value big . Int
ethTxData := eth . GetEthereumTxData ( bchainTx )
// add received amount only for OK transactions
if ethTxData . Status == 1 {
if len ( bchainTx . Vout ) > 0 {
bchainVout := & bchainTx . Vout [ 0 ]
value = bchainVout . ValueSat
if len ( bchainVout . ScriptPubKey . Addresses ) > 0 {
txAddrDesc , err := w . chainParser . GetAddrDescFromAddress ( bchainVout . ScriptPubKey . Addresses [ 0 ] )
if err != nil {
return nil , err
}
if bytes . Equal ( addrDesc , txAddrDesc ) {
( * big . Int ) ( bh . ReceivedSat ) . Add ( ( * big . Int ) ( bh . ReceivedSat ) , & value )
}
}
}
}
for i := range bchainTx . Vin {
bchainVin := & bchainTx . Vin [ i ]
if len ( bchainVin . Addresses ) > 0 {
txAddrDesc , err := w . chainParser . GetAddrDescFromAddress ( bchainVin . Addresses [ 0 ] )
if err != nil {
return nil , err
}
if bytes . Equal ( addrDesc , txAddrDesc ) {
// add sent amount only for OK transactions, however fees always
if ethTxData . Status == 1 {
( * big . Int ) ( bh . SentSat ) . Add ( ( * big . Int ) ( bh . SentSat ) , & value )
}
var feesSat big . Int
// mempool txs do not have fees yet
if ethTxData . GasUsed != nil {
feesSat . Mul ( ethTxData . GasPrice , ethTxData . GasUsed )
}
( * big . Int ) ( bh . SentSat ) . Add ( ( * big . Int ) ( bh . SentSat ) , & feesSat )
}
}
2019-12-03 08:02:20 -07:00
}
}
return & bh , nil
}
2019-12-17 16:02:24 -07:00
func ( w * Worker ) setFiatRateToBalanceHistories ( histories BalanceHistories , fiat string ) error {
for i := range histories {
bh := & histories [ i ]
t := time . Unix ( int64 ( bh . Time ) , 0 )
ticker , err := w . db . FiatRatesFindTicker ( & t )
if err != nil {
glog . Errorf ( "Error finding ticker by date %v. Error: %v" , t , err )
continue
} else if ticker == nil {
continue
}
if rate , found := ticker . Rates [ fiat ] ; found {
bh . FiatRate = string ( rate )
}
}
return nil
}
2019-12-03 08:02:20 -07:00
// GetBalanceHistory returns history of balance for given address
2019-12-17 16:02:24 -07:00
func ( w * Worker ) GetBalanceHistory ( address string , fromTime , toTime time . Time , fiat string ) ( BalanceHistories , error ) {
2019-12-03 08:02:20 -07:00
bhs := make ( BalanceHistories , 0 )
start := time . Now ( )
addrDesc , _ , err := w . getAddrDescAndNormalizeAddress ( address )
if err != nil {
return nil , err
}
fromUnix , fromHeight , toUnix , toHeight := w . balanceHistoryHeightsFromTo ( fromTime , toTime )
2019-11-29 11:26:20 -07:00
if fromHeight >= toHeight {
return bhs , nil
2019-11-19 00:46:47 -07:00
}
2019-11-29 11:26:20 -07:00
txs , err := w . getAddressTxids ( addrDesc , false , & AddressFilter { Vout : AddressFilterVoutOff , FromHeight : fromHeight , ToHeight : toHeight } , maxInt )
2019-11-04 02:55:00 -07:00
if err != nil {
return nil , err
}
for txi := len ( txs ) - 1 ; txi >= 0 ; txi -- {
2019-12-03 08:02:20 -07:00
bh , err := w . balanceHistoryForTxid ( addrDesc , txs [ txi ] , fromUnix , toUnix )
2019-11-04 02:55:00 -07:00
if err != nil {
return nil , err
}
2019-12-03 08:02:20 -07:00
if bh != nil {
bhs = append ( bhs , * bh )
2019-11-04 02:55:00 -07:00
}
}
2019-11-29 11:26:20 -07:00
bha := bhs . SortAndAggregate ( 3600 )
2019-12-17 16:02:24 -07:00
if fiat != "" {
err = w . setFiatRateToBalanceHistories ( bha , fiat )
if err != nil {
return nil , err
}
}
2019-12-03 08:02:20 -07:00
glog . Info ( "GetBalanceHistory " , address , ", blocks " , fromHeight , "-" , toHeight , ", count " , len ( bha ) , " finished in " , time . Since ( start ) )
2019-11-29 11:26:20 -07:00
return bha , nil
2019-11-04 02:55:00 -07:00
}
2019-05-23 07:56:40 -06:00
func ( w * Worker ) waitForBackendSync ( ) {
// wait a short time if blockbook is synchronizing with backend
inSync , _ , _ := w . is . GetSyncState ( )
count := 30
for ! inSync && count > 0 {
time . Sleep ( time . Millisecond * 100 )
count --
inSync , _ , _ = w . is . GetSyncState ( )
}
}
2019-02-08 07:50:37 -07:00
func ( w * Worker ) getAddrDescUtxo ( addrDesc bchain . AddressDescriptor , ba * db . AddrBalance , onlyConfirmed bool , onlyMempool bool ) ( Utxos , error ) {
2019-05-23 07:56:40 -06:00
w . waitForBackendSync ( )
2019-02-08 07:50:37 -07:00
var err error
r := make ( Utxos , 0 , 8 )
2018-11-15 07:44:28 -07:00
spentInMempool := make ( map [ string ] struct { } )
if ! onlyConfirmed {
// get utxo from mempool
2019-01-09 03:48:19 -07:00
txm , err := w . getAddressTxids ( addrDesc , true , & AddressFilter { Vout : AddressFilterVoutOff } , maxInt )
2018-11-14 06:46:42 -07:00
if err != nil {
2019-02-08 07:50:37 -07:00
return nil , err
2018-11-15 07:44:28 -07:00
}
2019-02-08 07:50:37 -07:00
if len ( txm ) > 0 {
mc := make ( [ ] * bchain . Tx , len ( txm ) )
for i , txid := range txm {
// get mempool txs and process their inputs to detect spends between mempool txs
bchainTx , _ , err := w . txCache . GetTransaction ( txid )
// mempool transaction may fail
if err != nil {
glog . Error ( "GetTransaction in mempool " , txid , ": " , err )
} else {
mc [ i ] = bchainTx
// get outputs spent by the mempool tx
for i := range bchainTx . Vin {
vin := & bchainTx . Vin [ i ]
spentInMempool [ vin . Txid + strconv . Itoa ( int ( vin . Vout ) ) ] = struct { } { }
}
2018-11-15 07:44:28 -07:00
}
}
2019-02-08 07:50:37 -07:00
for _ , bchainTx := range mc {
if bchainTx != nil {
for i := range bchainTx . Vout {
vout := & bchainTx . Vout [ i ]
vad , err := w . chainParser . GetAddrDescFromVout ( vout )
if err == nil && bytes . Equal ( addrDesc , vad ) {
// report only outpoints that are not spent in mempool
_ , e := spentInMempool [ bchainTx . Txid + strconv . Itoa ( i ) ]
if ! e {
2019-07-23 04:52:18 -06:00
coinbase := false
if len ( bchainTx . Vin ) == 1 && len ( bchainTx . Vin [ 0 ] . Coinbase ) > 0 {
coinbase = true
}
2019-02-08 07:50:37 -07:00
r = append ( r , Utxo {
Txid : bchainTx . Txid ,
Vout : int32 ( i ) ,
AmountSat : ( * Amount ) ( & vout . ValueSat ) ,
2019-05-06 06:19:17 -06:00
Locktime : bchainTx . LockTime ,
2019-07-23 04:52:18 -06:00
Coinbase : coinbase ,
2019-02-08 07:50:37 -07:00
} )
}
2018-11-15 07:44:28 -07:00
}
}
2018-11-14 06:46:42 -07:00
}
}
}
}
2019-02-08 07:50:37 -07:00
if ! onlyMempool {
// get utxo from index
if ba == nil {
2019-05-11 18:19:51 -06:00
ba , err = w . db . GetAddrDescBalance ( addrDesc , db . AddressBalanceDetailUTXO )
2019-02-08 07:50:37 -07:00
if err != nil {
return nil , NewAPIError ( fmt . Sprintf ( "Address not found, %v" , err ) , true )
2018-11-14 06:46:42 -07:00
}
2018-11-14 11:33:57 -07:00
}
2019-02-08 07:50:37 -07:00
// ba can be nil if the address is only in mempool!
2019-05-06 06:19:17 -06:00
if ba != nil && len ( ba . Utxos ) > 0 {
2019-02-08 07:50:37 -07:00
b , _ , err := w . db . GetBestBlock ( )
if err != nil {
return nil , err
}
bestheight := int ( b )
2019-05-06 06:19:17 -06:00
var checksum big . Int
checksum . Set ( & ba . BalanceSat )
// go backwards to get the newest first
for i := len ( ba . Utxos ) - 1 ; i >= 0 ; i -- {
utxo := & ba . Utxos [ i ]
txid , err := w . chainParser . UnpackTxid ( utxo . BtxID )
if err != nil {
return nil , err
2019-02-08 07:50:37 -07:00
}
2019-05-06 06:19:17 -06:00
_ , e := spentInMempool [ txid + strconv . Itoa ( int ( utxo . Vout ) ) ]
if ! e {
2019-07-23 04:52:18 -06:00
confirmations := bestheight - int ( utxo . Height ) + 1
coinbase := false
// for performance reasons, check coinbase transactions only in minimim confirmantion range
if confirmations < w . chainParser . MinimumCoinbaseConfirmations ( ) {
ta , err := w . db . GetTxAddresses ( txid )
if err != nil {
return nil , err
}
if len ( ta . Inputs ) == 1 && len ( ta . Inputs [ 0 ] . AddrDesc ) == 0 && IsZeroBigInt ( & ta . Inputs [ 0 ] . ValueSat ) {
coinbase = true
}
}
2019-05-06 06:19:17 -06:00
r = append ( r , Utxo {
Txid : txid ,
Vout : utxo . Vout ,
AmountSat : ( * Amount ) ( & utxo . ValueSat ) ,
Height : int ( utxo . Height ) ,
2019-07-23 04:52:18 -06:00
Confirmations : confirmations ,
Coinbase : coinbase ,
2019-05-06 06:19:17 -06:00
} )
2018-11-14 06:46:42 -07:00
}
2019-05-06 06:19:17 -06:00
checksum . Sub ( & checksum , & utxo . ValueSat )
2018-11-14 06:46:42 -07:00
}
2019-02-08 07:50:37 -07:00
if checksum . Uint64 ( ) != 0 {
glog . Warning ( "DB inconsistency: " , addrDesc , ": checksum is not zero" )
}
2018-11-14 06:46:42 -07:00
}
}
2019-02-08 07:50:37 -07:00
return r , nil
}
// GetAddressUtxo returns unspent outputs for given address
func ( w * Worker ) GetAddressUtxo ( address string , onlyConfirmed bool ) ( Utxos , error ) {
if w . chainType != bchain . ChainBitcoinType {
return nil , NewAPIError ( "Not supported" , true )
}
start := time . Now ( )
addrDesc , err := w . chainParser . GetAddrDescFromAddress ( address )
if err != nil {
return nil , NewAPIError ( fmt . Sprintf ( "Invalid address '%v', %v" , address , err ) , true )
2018-11-14 15:02:42 -07:00
}
2019-02-08 07:50:37 -07:00
r , err := w . getAddrDescUtxo ( addrDesc , nil , onlyConfirmed , false )
2019-03-01 07:25:16 -07:00
if err != nil {
return nil , err
}
2018-11-14 06:46:42 -07:00
glog . Info ( "GetAddressUtxo " , address , ", " , len ( r ) , " utxos, finished in " , time . Since ( start ) )
return r , nil
}
2018-09-14 04:10:03 -06:00
// GetBlocks returns BlockInfo for blocks on given page
func ( w * Worker ) GetBlocks ( page int , blocksOnPage int ) ( * Blocks , error ) {
start := time . Now ( )
page --
if page < 0 {
page = 0
}
b , _ , err := w . db . GetBestBlock ( )
bestheight := int ( b )
if err != nil {
return nil , errors . Annotatef ( err , "GetBestBlock" )
}
2018-09-14 06:47:42 -06:00
pg , from , to , page := computePaging ( bestheight + 1 , page , blocksOnPage )
2018-09-14 04:55:26 -06:00
r := & Blocks { Paging : pg }
2018-09-14 04:10:03 -06:00
r . Blocks = make ( [ ] db . BlockInfo , to - from )
for i := from ; i < to ; i ++ {
bi , err := w . db . GetBlockInfo ( uint32 ( bestheight - i ) )
if err != nil {
return nil , err
}
2018-10-18 09:56:25 -06:00
if bi == nil {
r . Blocks = r . Blocks [ : i ]
break
}
2018-09-14 04:10:03 -06:00
r . Blocks [ i - from ] = * bi
}
glog . Info ( "GetBlocks page " , page , " finished in " , time . Since ( start ) )
return r , nil
}
2018-09-17 03:17:40 -06:00
Add fiat rates functionality (#316)
* Add initial commit for fiat rates functionality
* templates.go: use bash from current user's environment
* bitcoinrpc.go: add FiatRates and FiatRatesParams to config
* blockbook.go: add initFiatRatesDownloader kickoff
* bitcoin.json: add coingecko API URL
* rockdb.go: add FindTicker and StoreTicker functions
* rocksdb_test.go: add a simple test for storing and getting FiatRate tickers
* rocksdb: add FindLastTicker and convertDate, make FindTicker return strings
* rocksdb: add ConvertDate function and CoinGeckoTicker struct, update tests
* blockbook.go, fiat: finalize the CoinGecko downloader
* coingecko.go: do not stop syncing when encountered an error
* rocksdb_test: fix the exported function name
* worker.go: make getBlockInfoFromBlockID a public function
* public.go: apiTickers kickoff
* rocksdb_test: fix the unittest comment
* coingecko.go: update comments
* blockbook.go, fiat: reword CoinGecko -> FiatRates, fix binary search upper bound, remove assignment of goroutine call result
* rename coingecko -> fiat_rates
* fiat_rates: export only the necessary methods
* blockbook.go: update log message
* bitcoinrpc.go: remove fiatRates settings
* use CurrencyRatesTicker structure everywhere, fix time format string, update tests, use UTC time
* add /api/v2/tickers tests, store rates as strings (json.Number)
* fiat_rates: add more tests, metrics and tickers-list endpoint, make the "currency" parameter mandatory
* public, worker: move FiatRates API logic to worker.go
* fiat_rates: add a future date test, fix comments, add more checks, store time as a pointer
* rocksdb_test: remove unneeded code
* fiat_rates: add a "ping" call to check server availability
* fiat_rates: do not return empty ticker, return nil instead if not found
add a test for non-existent ticker
* rocksdb_test: remove Sleep from tests
* worker.go: do not propagate all API errors to the client
* move InitTestFiatRates from rocksdb.go to public_test.go
* public.go: fix FiatRatesFindLastTicker result check
* fiat_rates: mock API server responses
* remove commented-out code
* fiat_rates: add comment explaining what periodSeconds attribute is used for
* websocket.go: implement fiatRates websocket endpoints & add tests
* fiatRates: add getFiatRatesTickersList websocket endpoint & test
* fiatRates: make websocket getFiatRatesByDate accept an array of dates, add more tests
* fiatRates: remove getFiatRatesForBlockID from websocket endpoints
* fiatRates: remove "if test", use custom startTime instead
Update tests and mock data
* fiatRates: finalize websocket functionality
add "date" parameter to TickerList
return data timestamps where needed
fix sync bugs (nil timestamp, duplicate save)
* fiatRates: add FiatRates configs for different coins
* worker.go: make GetBlockInfoFromBlockID private again
* fiatRates: wait & retry on errors, remove Ping function
* websocket.go: remove incorrect comment
* fiatRates: move coingecko-related code to a separate file, use interface
* fiatRates: if the new rates are the same as previous, try five more times, and only then store them
* coingecko: fix getting actual rates, add a timestamp parameter to get uncached responses
* vertcoin_testnet.json: remove fiat rates parameters
* fiat_rates: add timestamp to log message about skipping the repeating rates
2019-12-17 02:40:02 -07:00
// getFiatRatesResult checks if CurrencyRatesTicker contains all necessary data and returns formatted result
func ( w * Worker ) getFiatRatesResult ( currency string , ticker * db . CurrencyRatesTicker ) ( * db . ResultTickerAsString , error ) {
2019-12-17 16:02:24 -07:00
rates := make ( map [ string ] json . Number , 2 )
Add fiat rates functionality (#316)
* Add initial commit for fiat rates functionality
* templates.go: use bash from current user's environment
* bitcoinrpc.go: add FiatRates and FiatRatesParams to config
* blockbook.go: add initFiatRatesDownloader kickoff
* bitcoin.json: add coingecko API URL
* rockdb.go: add FindTicker and StoreTicker functions
* rocksdb_test.go: add a simple test for storing and getting FiatRate tickers
* rocksdb: add FindLastTicker and convertDate, make FindTicker return strings
* rocksdb: add ConvertDate function and CoinGeckoTicker struct, update tests
* blockbook.go, fiat: finalize the CoinGecko downloader
* coingecko.go: do not stop syncing when encountered an error
* rocksdb_test: fix the exported function name
* worker.go: make getBlockInfoFromBlockID a public function
* public.go: apiTickers kickoff
* rocksdb_test: fix the unittest comment
* coingecko.go: update comments
* blockbook.go, fiat: reword CoinGecko -> FiatRates, fix binary search upper bound, remove assignment of goroutine call result
* rename coingecko -> fiat_rates
* fiat_rates: export only the necessary methods
* blockbook.go: update log message
* bitcoinrpc.go: remove fiatRates settings
* use CurrencyRatesTicker structure everywhere, fix time format string, update tests, use UTC time
* add /api/v2/tickers tests, store rates as strings (json.Number)
* fiat_rates: add more tests, metrics and tickers-list endpoint, make the "currency" parameter mandatory
* public, worker: move FiatRates API logic to worker.go
* fiat_rates: add a future date test, fix comments, add more checks, store time as a pointer
* rocksdb_test: remove unneeded code
* fiat_rates: add a "ping" call to check server availability
* fiat_rates: do not return empty ticker, return nil instead if not found
add a test for non-existent ticker
* rocksdb_test: remove Sleep from tests
* worker.go: do not propagate all API errors to the client
* move InitTestFiatRates from rocksdb.go to public_test.go
* public.go: fix FiatRatesFindLastTicker result check
* fiat_rates: mock API server responses
* remove commented-out code
* fiat_rates: add comment explaining what periodSeconds attribute is used for
* websocket.go: implement fiatRates websocket endpoints & add tests
* fiatRates: add getFiatRatesTickersList websocket endpoint & test
* fiatRates: make websocket getFiatRatesByDate accept an array of dates, add more tests
* fiatRates: remove getFiatRatesForBlockID from websocket endpoints
* fiatRates: remove "if test", use custom startTime instead
Update tests and mock data
* fiatRates: finalize websocket functionality
add "date" parameter to TickerList
return data timestamps where needed
fix sync bugs (nil timestamp, duplicate save)
* fiatRates: add FiatRates configs for different coins
* worker.go: make GetBlockInfoFromBlockID private again
* fiatRates: wait & retry on errors, remove Ping function
* websocket.go: remove incorrect comment
* fiatRates: move coingecko-related code to a separate file, use interface
* fiatRates: if the new rates are the same as previous, try five more times, and only then store them
* coingecko: fix getting actual rates, add a timestamp parameter to get uncached responses
* vertcoin_testnet.json: remove fiat rates parameters
* fiat_rates: add timestamp to log message about skipping the repeating rates
2019-12-17 02:40:02 -07:00
timeFormatted := ticker . Timestamp . Format ( db . FiatRatesTimeFormat )
2019-12-17 16:02:24 -07:00
if rate , found := ticker . Rates [ currency ] ; ! found {
availableCurrencies := make ( [ ] string , 0 , len ( ticker . Rates ) )
for availableCurrency := range ticker . Rates {
availableCurrencies = append ( availableCurrencies , availableCurrency )
Add fiat rates functionality (#316)
* Add initial commit for fiat rates functionality
* templates.go: use bash from current user's environment
* bitcoinrpc.go: add FiatRates and FiatRatesParams to config
* blockbook.go: add initFiatRatesDownloader kickoff
* bitcoin.json: add coingecko API URL
* rockdb.go: add FindTicker and StoreTicker functions
* rocksdb_test.go: add a simple test for storing and getting FiatRate tickers
* rocksdb: add FindLastTicker and convertDate, make FindTicker return strings
* rocksdb: add ConvertDate function and CoinGeckoTicker struct, update tests
* blockbook.go, fiat: finalize the CoinGecko downloader
* coingecko.go: do not stop syncing when encountered an error
* rocksdb_test: fix the exported function name
* worker.go: make getBlockInfoFromBlockID a public function
* public.go: apiTickers kickoff
* rocksdb_test: fix the unittest comment
* coingecko.go: update comments
* blockbook.go, fiat: reword CoinGecko -> FiatRates, fix binary search upper bound, remove assignment of goroutine call result
* rename coingecko -> fiat_rates
* fiat_rates: export only the necessary methods
* blockbook.go: update log message
* bitcoinrpc.go: remove fiatRates settings
* use CurrencyRatesTicker structure everywhere, fix time format string, update tests, use UTC time
* add /api/v2/tickers tests, store rates as strings (json.Number)
* fiat_rates: add more tests, metrics and tickers-list endpoint, make the "currency" parameter mandatory
* public, worker: move FiatRates API logic to worker.go
* fiat_rates: add a future date test, fix comments, add more checks, store time as a pointer
* rocksdb_test: remove unneeded code
* fiat_rates: add a "ping" call to check server availability
* fiat_rates: do not return empty ticker, return nil instead if not found
add a test for non-existent ticker
* rocksdb_test: remove Sleep from tests
* worker.go: do not propagate all API errors to the client
* move InitTestFiatRates from rocksdb.go to public_test.go
* public.go: fix FiatRatesFindLastTicker result check
* fiat_rates: mock API server responses
* remove commented-out code
* fiat_rates: add comment explaining what periodSeconds attribute is used for
* websocket.go: implement fiatRates websocket endpoints & add tests
* fiatRates: add getFiatRatesTickersList websocket endpoint & test
* fiatRates: make websocket getFiatRatesByDate accept an array of dates, add more tests
* fiatRates: remove getFiatRatesForBlockID from websocket endpoints
* fiatRates: remove "if test", use custom startTime instead
Update tests and mock data
* fiatRates: finalize websocket functionality
add "date" parameter to TickerList
return data timestamps where needed
fix sync bugs (nil timestamp, duplicate save)
* fiatRates: add FiatRates configs for different coins
* worker.go: make GetBlockInfoFromBlockID private again
* fiatRates: wait & retry on errors, remove Ping function
* websocket.go: remove incorrect comment
* fiatRates: move coingecko-related code to a separate file, use interface
* fiatRates: if the new rates are the same as previous, try five more times, and only then store them
* coingecko: fix getting actual rates, add a timestamp parameter to get uncached responses
* vertcoin_testnet.json: remove fiat rates parameters
* fiat_rates: add timestamp to log message about skipping the repeating rates
2019-12-17 02:40:02 -07:00
}
2019-12-17 16:02:24 -07:00
sort . Strings ( availableCurrencies ) // sort to get deterministic results
return nil , NewAPIError ( fmt . Sprintf ( "Currency %q is not available for timestamp %s. Available currencies are: %s" , currency , timeFormatted , strings . Join ( availableCurrencies , "," ) ) , true )
} else {
rates [ currency ] = rate
}
// add default usd currency
if currency != "usd" {
if rate , found := ticker . Rates [ "usd" ] ; found {
rates [ "usd" ] = rate
Add fiat rates functionality (#316)
* Add initial commit for fiat rates functionality
* templates.go: use bash from current user's environment
* bitcoinrpc.go: add FiatRates and FiatRatesParams to config
* blockbook.go: add initFiatRatesDownloader kickoff
* bitcoin.json: add coingecko API URL
* rockdb.go: add FindTicker and StoreTicker functions
* rocksdb_test.go: add a simple test for storing and getting FiatRate tickers
* rocksdb: add FindLastTicker and convertDate, make FindTicker return strings
* rocksdb: add ConvertDate function and CoinGeckoTicker struct, update tests
* blockbook.go, fiat: finalize the CoinGecko downloader
* coingecko.go: do not stop syncing when encountered an error
* rocksdb_test: fix the exported function name
* worker.go: make getBlockInfoFromBlockID a public function
* public.go: apiTickers kickoff
* rocksdb_test: fix the unittest comment
* coingecko.go: update comments
* blockbook.go, fiat: reword CoinGecko -> FiatRates, fix binary search upper bound, remove assignment of goroutine call result
* rename coingecko -> fiat_rates
* fiat_rates: export only the necessary methods
* blockbook.go: update log message
* bitcoinrpc.go: remove fiatRates settings
* use CurrencyRatesTicker structure everywhere, fix time format string, update tests, use UTC time
* add /api/v2/tickers tests, store rates as strings (json.Number)
* fiat_rates: add more tests, metrics and tickers-list endpoint, make the "currency" parameter mandatory
* public, worker: move FiatRates API logic to worker.go
* fiat_rates: add a future date test, fix comments, add more checks, store time as a pointer
* rocksdb_test: remove unneeded code
* fiat_rates: add a "ping" call to check server availability
* fiat_rates: do not return empty ticker, return nil instead if not found
add a test for non-existent ticker
* rocksdb_test: remove Sleep from tests
* worker.go: do not propagate all API errors to the client
* move InitTestFiatRates from rocksdb.go to public_test.go
* public.go: fix FiatRatesFindLastTicker result check
* fiat_rates: mock API server responses
* remove commented-out code
* fiat_rates: add comment explaining what periodSeconds attribute is used for
* websocket.go: implement fiatRates websocket endpoints & add tests
* fiatRates: add getFiatRatesTickersList websocket endpoint & test
* fiatRates: make websocket getFiatRatesByDate accept an array of dates, add more tests
* fiatRates: remove getFiatRatesForBlockID from websocket endpoints
* fiatRates: remove "if test", use custom startTime instead
Update tests and mock data
* fiatRates: finalize websocket functionality
add "date" parameter to TickerList
return data timestamps where needed
fix sync bugs (nil timestamp, duplicate save)
* fiatRates: add FiatRates configs for different coins
* worker.go: make GetBlockInfoFromBlockID private again
* fiatRates: wait & retry on errors, remove Ping function
* websocket.go: remove incorrect comment
* fiatRates: move coingecko-related code to a separate file, use interface
* fiatRates: if the new rates are the same as previous, try five more times, and only then store them
* coingecko: fix getting actual rates, add a timestamp parameter to get uncached responses
* vertcoin_testnet.json: remove fiat rates parameters
* fiat_rates: add timestamp to log message about skipping the repeating rates
2019-12-17 02:40:02 -07:00
}
}
result := & db . ResultTickerAsString {
Timestamp : timeFormatted ,
2019-12-17 16:02:24 -07:00
Rates : rates ,
Add fiat rates functionality (#316)
* Add initial commit for fiat rates functionality
* templates.go: use bash from current user's environment
* bitcoinrpc.go: add FiatRates and FiatRatesParams to config
* blockbook.go: add initFiatRatesDownloader kickoff
* bitcoin.json: add coingecko API URL
* rockdb.go: add FindTicker and StoreTicker functions
* rocksdb_test.go: add a simple test for storing and getting FiatRate tickers
* rocksdb: add FindLastTicker and convertDate, make FindTicker return strings
* rocksdb: add ConvertDate function and CoinGeckoTicker struct, update tests
* blockbook.go, fiat: finalize the CoinGecko downloader
* coingecko.go: do not stop syncing when encountered an error
* rocksdb_test: fix the exported function name
* worker.go: make getBlockInfoFromBlockID a public function
* public.go: apiTickers kickoff
* rocksdb_test: fix the unittest comment
* coingecko.go: update comments
* blockbook.go, fiat: reword CoinGecko -> FiatRates, fix binary search upper bound, remove assignment of goroutine call result
* rename coingecko -> fiat_rates
* fiat_rates: export only the necessary methods
* blockbook.go: update log message
* bitcoinrpc.go: remove fiatRates settings
* use CurrencyRatesTicker structure everywhere, fix time format string, update tests, use UTC time
* add /api/v2/tickers tests, store rates as strings (json.Number)
* fiat_rates: add more tests, metrics and tickers-list endpoint, make the "currency" parameter mandatory
* public, worker: move FiatRates API logic to worker.go
* fiat_rates: add a future date test, fix comments, add more checks, store time as a pointer
* rocksdb_test: remove unneeded code
* fiat_rates: add a "ping" call to check server availability
* fiat_rates: do not return empty ticker, return nil instead if not found
add a test for non-existent ticker
* rocksdb_test: remove Sleep from tests
* worker.go: do not propagate all API errors to the client
* move InitTestFiatRates from rocksdb.go to public_test.go
* public.go: fix FiatRatesFindLastTicker result check
* fiat_rates: mock API server responses
* remove commented-out code
* fiat_rates: add comment explaining what periodSeconds attribute is used for
* websocket.go: implement fiatRates websocket endpoints & add tests
* fiatRates: add getFiatRatesTickersList websocket endpoint & test
* fiatRates: make websocket getFiatRatesByDate accept an array of dates, add more tests
* fiatRates: remove getFiatRatesForBlockID from websocket endpoints
* fiatRates: remove "if test", use custom startTime instead
Update tests and mock data
* fiatRates: finalize websocket functionality
add "date" parameter to TickerList
return data timestamps where needed
fix sync bugs (nil timestamp, duplicate save)
* fiatRates: add FiatRates configs for different coins
* worker.go: make GetBlockInfoFromBlockID private again
* fiatRates: wait & retry on errors, remove Ping function
* websocket.go: remove incorrect comment
* fiatRates: move coingecko-related code to a separate file, use interface
* fiatRates: if the new rates are the same as previous, try five more times, and only then store them
* coingecko: fix getting actual rates, add a timestamp parameter to get uncached responses
* vertcoin_testnet.json: remove fiat rates parameters
* fiat_rates: add timestamp to log message about skipping the repeating rates
2019-12-17 02:40:02 -07:00
}
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 )
}
2019-12-17 07:37:09 -07:00
var ticker * db . CurrencyRatesTicker
Add fiat rates functionality (#316)
* Add initial commit for fiat rates functionality
* templates.go: use bash from current user's environment
* bitcoinrpc.go: add FiatRates and FiatRatesParams to config
* blockbook.go: add initFiatRatesDownloader kickoff
* bitcoin.json: add coingecko API URL
* rockdb.go: add FindTicker and StoreTicker functions
* rocksdb_test.go: add a simple test for storing and getting FiatRate tickers
* rocksdb: add FindLastTicker and convertDate, make FindTicker return strings
* rocksdb: add ConvertDate function and CoinGeckoTicker struct, update tests
* blockbook.go, fiat: finalize the CoinGecko downloader
* coingecko.go: do not stop syncing when encountered an error
* rocksdb_test: fix the exported function name
* worker.go: make getBlockInfoFromBlockID a public function
* public.go: apiTickers kickoff
* rocksdb_test: fix the unittest comment
* coingecko.go: update comments
* blockbook.go, fiat: reword CoinGecko -> FiatRates, fix binary search upper bound, remove assignment of goroutine call result
* rename coingecko -> fiat_rates
* fiat_rates: export only the necessary methods
* blockbook.go: update log message
* bitcoinrpc.go: remove fiatRates settings
* use CurrencyRatesTicker structure everywhere, fix time format string, update tests, use UTC time
* add /api/v2/tickers tests, store rates as strings (json.Number)
* fiat_rates: add more tests, metrics and tickers-list endpoint, make the "currency" parameter mandatory
* public, worker: move FiatRates API logic to worker.go
* fiat_rates: add a future date test, fix comments, add more checks, store time as a pointer
* rocksdb_test: remove unneeded code
* fiat_rates: add a "ping" call to check server availability
* fiat_rates: do not return empty ticker, return nil instead if not found
add a test for non-existent ticker
* rocksdb_test: remove Sleep from tests
* worker.go: do not propagate all API errors to the client
* move InitTestFiatRates from rocksdb.go to public_test.go
* public.go: fix FiatRatesFindLastTicker result check
* fiat_rates: mock API server responses
* remove commented-out code
* fiat_rates: add comment explaining what periodSeconds attribute is used for
* websocket.go: implement fiatRates websocket endpoints & add tests
* fiatRates: add getFiatRatesTickersList websocket endpoint & test
* fiatRates: make websocket getFiatRatesByDate accept an array of dates, add more tests
* fiatRates: remove getFiatRatesForBlockID from websocket endpoints
* fiatRates: remove "if test", use custom startTime instead
Update tests and mock data
* fiatRates: finalize websocket functionality
add "date" parameter to TickerList
return data timestamps where needed
fix sync bugs (nil timestamp, duplicate save)
* fiatRates: add FiatRates configs for different coins
* worker.go: make GetBlockInfoFromBlockID private again
* fiatRates: wait & retry on errors, remove Ping function
* websocket.go: remove incorrect comment
* fiatRates: move coingecko-related code to a separate file, use interface
* fiatRates: if the new rates are the same as previous, try five more times, and only then store them
* coingecko: fix getting actual rates, add a timestamp parameter to get uncached responses
* vertcoin_testnet.json: remove fiat rates parameters
* fiat_rates: add timestamp to log message about skipping the repeating rates
2019-12-17 02:40:02 -07:00
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
2019-08-07 05:13:45 -06:00
func ( w * Worker ) getBlockInfoFromBlockID ( bid string ) ( * bchain . BlockInfo , error ) {
2018-10-22 02:39:29 -06:00
// try to decide if passed string (bid) is block height or block hash
// if it's a number, must be less than int32
2018-09-17 10:28:08 -06:00
var hash string
height , err := strconv . Atoi ( bid )
2019-02-12 07:15:10 -07:00
if err == nil && height < int ( maxUint32 ) {
2018-09-17 10:28:08 -06:00
hash , err = w . db . GetBlockHash ( uint32 ( height ) )
2018-10-22 02:39:29 -06:00
if err != nil {
hash = bid
}
2018-09-17 10:28:08 -06:00
} else {
hash = bid
}
2019-05-23 08:05:33 -06:00
if hash == "" {
return nil , NewAPIError ( "Block not found" , true )
}
2018-09-17 10:28:08 -06:00
bi , err := w . chain . GetBlockInfo ( hash )
2019-08-07 05:13:45 -06:00
return bi , err
}
// GetFeeStats returns statistics about block fees
func ( w * Worker ) GetFeeStats ( bid string ) ( * FeeStats , error ) {
// txSpecific extends Tx with an additional Size and Vsize info
type txSpecific struct {
* bchain . Tx
Vsize int ` json:"vsize,omitempty" `
Size int ` json:"size,omitempty" `
}
start := time . Now ( )
bi , err := w . getBlockInfoFromBlockID ( bid )
if err != nil {
if err == bchain . ErrBlockNotFound {
return nil , NewAPIError ( "Block not found" , true )
}
return nil , NewAPIError ( fmt . Sprintf ( "Block not found, %v" , err ) , true )
}
feesPerKb := make ( [ ] int64 , 0 , len ( bi . Txids ) )
totalFeesSat := big . NewInt ( 0 )
averageFeePerKb := int64 ( 0 )
for _ , txid := range bi . Txids {
// Get a raw JSON with transaction details, including size, vsize, hex
txSpecificJSON , err := w . chain . GetTransactionSpecific ( & bchain . Tx { Txid : txid } )
if err != nil {
return nil , errors . Annotatef ( err , "GetTransactionSpecific" )
}
// Serialize the raw JSON into TxSpecific struct
var txSpec txSpecific
err = json . Unmarshal ( txSpecificJSON , & txSpec )
if err != nil {
return nil , errors . Annotatef ( err , "Unmarshal" )
}
// Calculate the TX size in bytes
txSize := 0
if txSpec . Vsize > 0 {
txSize = txSpec . Vsize
} else if txSpec . Size > 0 {
txSize = txSpec . Size
} else if txSpec . Hex != "" {
txSize = len ( txSpec . Hex ) / 2
} else {
errMsg := "Cannot determine the transaction size from neither Vsize, Size nor Hex! Txid: " + txid
return nil , NewAPIError ( errMsg , true )
}
// Get values of TX inputs and outputs
txAddresses , err := w . db . GetTxAddresses ( txid )
if err != nil {
return nil , errors . Annotatef ( err , "GetTxAddresses" )
}
2019-08-21 15:11:50 -06:00
// Calculate total fees in Satoshis
2019-08-07 05:13:45 -06:00
feeSat := big . NewInt ( 0 )
for _ , input := range txAddresses . Inputs {
feeSat = feeSat . Add ( & input . ValueSat , feeSat )
}
// Zero inputs means it's a Coinbase TX - skip it
if feeSat . Cmp ( big . NewInt ( 0 ) ) == 0 {
continue
}
for _ , output := range txAddresses . Outputs {
feeSat = feeSat . Sub ( feeSat , & output . ValueSat )
}
totalFeesSat . Add ( totalFeesSat , feeSat )
// Convert feeSat to fee per kilobyte and add to an array for decile calculation
feePerKb := int64 ( float64 ( feeSat . Int64 ( ) ) / float64 ( txSize ) * 1000 )
averageFeePerKb += feePerKb
feesPerKb = append ( feesPerKb , feePerKb )
}
var deciles [ 11 ] int64
n := len ( feesPerKb )
if n > 0 {
averageFeePerKb /= int64 ( n )
// Sort fees and calculate the deciles
sort . Slice ( feesPerKb , func ( i , j int ) bool { return feesPerKb [ i ] < feesPerKb [ j ] } )
for k := 0 ; k <= 10 ; k ++ {
index := int ( math . Floor ( 0.5 + float64 ( k ) * float64 ( n + 1 ) / 10 ) ) - 1
if index < 0 {
index = 0
} else if index >= n {
index = n - 1
}
deciles [ k ] = feesPerKb [ index ]
}
}
glog . Info ( "GetFeeStats " , bid , " (" , len ( feesPerKb ) , " txs) finished in " , time . Since ( start ) )
return & FeeStats {
TxCount : len ( feesPerKb ) ,
AverageFeePerKb : averageFeePerKb ,
TotalFeesSat : ( * Amount ) ( totalFeesSat ) ,
DecilesFeePerKb : deciles ,
} , nil
}
// GetBlock returns paged data about block
func ( w * Worker ) GetBlock ( bid string , page int , txsOnPage int ) ( * Block , error ) {
start := time . Now ( )
page --
if page < 0 {
page = 0
}
bi , err := w . getBlockInfoFromBlockID ( bid )
2018-09-17 10:28:08 -06:00
if err != nil {
if err == bchain . ErrBlockNotFound {
2018-11-05 03:26:23 -07:00
return nil , NewAPIError ( "Block not found" , true )
2018-09-17 10:28:08 -06:00
}
2018-11-05 03:26:23 -07:00
return nil , NewAPIError ( fmt . Sprintf ( "Block not found, %v" , err ) , true )
2018-09-17 10:28:08 -06:00
}
dbi := & db . BlockInfo {
Hash : bi . Hash ,
Height : bi . Height ,
Time : bi . Time ,
}
txCount := len ( bi . Txids )
bestheight , _ , err := w . db . GetBestBlock ( )
if err != nil {
return nil , errors . Annotatef ( err , "GetBestBlock" )
}
pg , from , to , page := computePaging ( txCount , page , txsOnPage )
txs := make ( [ ] * Tx , to - from )
txi := 0
for i := from ; i < to ; i ++ {
2019-04-28 03:54:12 -06:00
txs [ txi ] , err = w . txFromTxid ( bi . Txids [ i ] , bestheight , AccountDetailsTxHistoryLight , dbi )
if err != nil {
return nil , err
2018-09-17 10:28:08 -06:00
}
txi ++
}
2018-12-03 08:34:38 -07:00
if bi . Prev == "" && bi . Height != 0 {
bi . Prev , _ = w . db . GetBlockHash ( bi . Height - 1 )
}
if bi . Next == "" && bi . Height != bestheight {
bi . Next , _ = w . db . GetBlockHash ( bi . Height + 1 )
}
2018-09-17 10:28:08 -06:00
txs = txs [ : txi ]
bi . Txids = nil
2018-12-03 08:34:38 -07:00
glog . Info ( "GetBlock " , bid , ", page " , page , " finished in " , time . Since ( start ) )
2018-09-17 10:28:08 -06:00
return & Block {
2019-03-21 02:36:18 -06:00
Paging : pg ,
BlockInfo : BlockInfo {
2019-04-16 06:44:50 -06:00
Hash : bi . Hash ,
Prev : bi . Prev ,
Next : bi . Next ,
Height : bi . Height ,
Confirmations : bi . Confirmations ,
Size : bi . Size ,
Time : bi . Time ,
Bits : bi . Bits ,
Difficulty : string ( bi . Difficulty ) ,
MerkleRoot : bi . MerkleRoot ,
Nonce : string ( bi . Nonce ) ,
Txids : bi . Txids ,
Version : bi . Version ,
2019-03-21 02:36:18 -06:00
} ,
2018-09-17 10:28:08 -06:00
TxCount : txCount ,
Transactions : txs ,
} , nil
}
2019-04-28 03:54:12 -06:00
// ComputeFeeStats computes fee distribution in defined blocks and logs them to log
func ( w * Worker ) ComputeFeeStats ( blockFrom , blockTo int , stopCompute chan os . Signal ) error {
bestheight , _ , err := w . db . GetBestBlock ( )
if err != nil {
return errors . Annotatef ( err , "GetBestBlock" )
}
for block := blockFrom ; block <= blockTo ; block ++ {
hash , err := w . db . GetBlockHash ( uint32 ( block ) )
if err != nil {
return err
}
bi , err := w . chain . GetBlockInfo ( hash )
if err != nil {
return err
}
// process only blocks with enough transactions
if len ( bi . Txids ) > 20 {
dbi := & db . BlockInfo {
Hash : bi . Hash ,
Height : bi . Height ,
Time : bi . Time ,
}
txids := bi . Txids
if w . chainType == bchain . ChainBitcoinType {
// skip the coinbase transaction
txids = txids [ 1 : ]
}
fees := make ( [ ] int64 , len ( txids ) )
sum := int64 ( 0 )
for i , txid := range txids {
select {
case <- stopCompute :
glog . Info ( "ComputeFeeStats interrupted at height " , block )
return db . ErrOperationInterrupted
default :
tx , err := w . txFromTxid ( txid , bestheight , AccountDetailsTxHistoryLight , dbi )
if err != nil {
return err
}
fee := tx . FeesSat . AsInt64 ( )
fees [ i ] = fee
sum += fee
}
}
sort . Slice ( fees , func ( i , j int ) bool { return fees [ i ] < fees [ j ] } )
step := float64 ( len ( fees ) ) / 10
percentils := ""
for i := float64 ( 0 ) ; i < float64 ( len ( fees ) + 1 ) ; i += step {
ii := int ( math . Round ( i ) )
if ii >= len ( fees ) {
ii = len ( fees ) - 1
}
percentils += "," + strconv . FormatInt ( fees [ ii ] , 10 )
}
glog . Info ( block , "," , time . Unix ( bi . Time , 0 ) . Format ( time . RFC3339 ) , "," , len ( bi . Txids ) , "," , sum , "," , float64 ( sum ) / float64 ( len ( bi . Txids ) ) , percentils )
}
}
return nil
}
2018-09-17 03:17:40 -06:00
// GetSystemInfo returns information about system
2018-09-19 07:59:49 -06:00
func ( w * Worker ) GetSystemInfo ( internal bool ) ( * SystemInfo , error ) {
2018-09-17 03:17:40 -06:00
start := time . Now ( )
vi := common . GetVersionInfo ( )
2019-04-16 06:44:50 -06:00
inSync , bestHeight , lastBlockTime := w . is . GetSyncState ( )
inSyncMempool , lastMempoolTime , mempoolSize := w . is . GetMempoolSyncState ( )
2019-06-05 05:29:06 -06:00
ci , err := w . chain . GetChainInfo ( )
var backendError string
if err != nil {
2019-06-10 05:48:46 -06:00
glog . Error ( "GetChainInfo error " , err )
2019-06-05 05:29:06 -06:00
backendError = errors . Annotatef ( err , "GetChainInfo" ) . Error ( )
ci = & bchain . ChainInfo { }
// set not in sync in case of backend error
inSync = false
inSyncMempool = false
}
2019-04-16 06:44:50 -06:00
var columnStats [ ] common . InternalStateColumn
var internalDBSize int64
2018-09-19 07:59:49 -06:00
if internal {
2019-04-16 06:44:50 -06:00
columnStats = w . is . GetAllDBColumnStats ( )
internalDBSize = w . is . DBSizeTotal ( )
2018-09-19 07:59:49 -06:00
}
2019-04-16 06:44:50 -06:00
blockbookInfo := & BlockbookInfo {
2018-09-19 07:59:49 -06:00
Coin : w . is . Coin ,
Host : w . is . Host ,
Version : vi . Version ,
GitCommit : vi . GitCommit ,
BuildTime : vi . BuildTime ,
2018-09-20 04:15:46 -06:00
SyncMode : w . is . SyncMode ,
InitialSync : w . is . InitialSync ,
2019-04-16 06:44:50 -06:00
InSync : inSync ,
BestHeight : bestHeight ,
LastBlockTime : lastBlockTime ,
InSyncMempool : inSyncMempool ,
LastMempoolTime : lastMempoolTime ,
MempoolSize : mempoolSize ,
2018-12-13 06:31:34 -07:00
Decimals : w . chainParser . AmountDecimals ( ) ,
2018-09-19 07:59:49 -06:00
DbSize : w . db . DatabaseSizeOnDisk ( ) ,
2019-04-16 06:44:50 -06:00
DbSizeFromColumns : internalDBSize ,
DbColumns : columnStats ,
2018-10-11 07:38:45 -06:00
About : Text . BlockbookAbout ,
2018-09-17 03:17:40 -06:00
}
2019-04-16 06:44:50 -06:00
backendInfo := & BackendInfo {
2019-06-05 05:29:06 -06:00
BackendError : backendError ,
BestBlockHash : ci . Bestblockhash ,
2019-04-16 06:44:50 -06:00
Blocks : ci . Blocks ,
Chain : ci . Chain ,
Difficulty : ci . Difficulty ,
Headers : ci . Headers ,
ProtocolVersion : ci . ProtocolVersion ,
SizeOnDisk : ci . SizeOnDisk ,
Subversion : ci . Subversion ,
Timeoffset : ci . Timeoffset ,
Version : ci . Version ,
Warnings : ci . Warnings ,
}
2018-09-17 03:17:40 -06:00
glog . Info ( "GetSystemInfo finished in " , time . Since ( start ) )
2019-04-16 06:44:50 -06:00
return & SystemInfo { blockbookInfo , backendInfo } , nil
2018-09-17 03:17:40 -06:00
}
2019-04-02 03:39:38 -06:00
// GetMempool returns a page of mempool txids
func ( w * Worker ) GetMempool ( page int , itemsOnPage int ) ( * MempoolTxids , error ) {
page --
if page < 0 {
page = 0
}
entries := w . mempool . GetAllEntries ( )
2019-04-16 07:18:54 -06:00
pg , from , to , _ := computePaging ( len ( entries ) , page , itemsOnPage )
2019-04-02 03:39:38 -06:00
r := & MempoolTxids {
Paging : pg ,
MempoolSize : len ( entries ) ,
}
r . Mempool = make ( [ ] MempoolTxid , to - from )
for i := from ; i < to ; i ++ {
entry := & entries [ i ]
r . Mempool [ i - from ] = MempoolTxid {
Txid : entry . Txid ,
Time : int64 ( entry . Time ) ,
}
}
return r , nil
}