Merge pull request #337 from trezor/balanceHistory
Add API for account balance history #307pull/340/head
commit
1ce7bc1406
64
api/types.go
64
api/types.go
|
@ -7,6 +7,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -296,6 +297,69 @@ func (a Utxos) Less(i, j int) bool {
|
|||
return hi >= hj
|
||||
}
|
||||
|
||||
// BalanceHistory contains info about one point in time of balance history
|
||||
type BalanceHistory struct {
|
||||
Time uint32 `json:"time"`
|
||||
Txs uint32 `json:"txs"`
|
||||
ReceivedSat *Amount `json:"received"`
|
||||
SentSat *Amount `json:"sent"`
|
||||
FiatRate string `json:"fiatRate,omitempty"`
|
||||
Txid string `json:"txid,omitempty"`
|
||||
}
|
||||
|
||||
// BalanceHistories is array of BalanceHistory
|
||||
type BalanceHistories []BalanceHistory
|
||||
|
||||
func (a BalanceHistories) Len() int { return len(a) }
|
||||
func (a BalanceHistories) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a BalanceHistories) Less(i, j int) bool {
|
||||
ti := a[i].Time
|
||||
tj := a[j].Time
|
||||
if ti == tj {
|
||||
return a[i].Txid < a[j].Txid
|
||||
}
|
||||
return ti < tj
|
||||
}
|
||||
|
||||
// SortAndAggregate sums BalanceHistories to groups defined by parameter groupByTime
|
||||
func (a BalanceHistories) SortAndAggregate(groupByTime uint32) BalanceHistories {
|
||||
bhs := make(BalanceHistories, 0)
|
||||
if len(a) > 0 {
|
||||
bha := BalanceHistory{
|
||||
SentSat: &Amount{},
|
||||
ReceivedSat: &Amount{},
|
||||
}
|
||||
sort.Sort(a)
|
||||
for i := range a {
|
||||
bh := &a[i]
|
||||
time := bh.Time - bh.Time%groupByTime
|
||||
if bha.Time != time {
|
||||
if bha.Time != 0 {
|
||||
// in aggregate, do not return txid as it could multiple of them
|
||||
bha.Txid = ""
|
||||
bhs = append(bhs, bha)
|
||||
}
|
||||
bha = BalanceHistory{
|
||||
Time: time,
|
||||
SentSat: &Amount{},
|
||||
ReceivedSat: &Amount{},
|
||||
}
|
||||
}
|
||||
if bha.Txid != bh.Txid {
|
||||
bha.Txs += bh.Txs
|
||||
bha.Txid = bh.Txid
|
||||
}
|
||||
(*big.Int)(bha.SentSat).Add((*big.Int)(bha.SentSat), (*big.Int)(bh.SentSat))
|
||||
(*big.Int)(bha.ReceivedSat).Add((*big.Int)(bha.ReceivedSat), (*big.Int)(bh.ReceivedSat))
|
||||
}
|
||||
if bha.Txs > 0 {
|
||||
bha.Txid = ""
|
||||
bhs = append(bhs, bha)
|
||||
}
|
||||
}
|
||||
return bhs
|
||||
}
|
||||
|
||||
// Blocks is list of blocks with paging information
|
||||
type Blocks struct {
|
||||
Paging
|
||||
|
|
|
@ -49,3 +49,115 @@ func TestAmount_MarshalJSON(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBalanceHistories_SortAndAggregate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a BalanceHistories
|
||||
groupByTime uint32
|
||||
want BalanceHistories
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
a: []BalanceHistory{},
|
||||
groupByTime: 3600,
|
||||
want: []BalanceHistory{},
|
||||
},
|
||||
{
|
||||
name: "one",
|
||||
a: []BalanceHistory{
|
||||
{
|
||||
ReceivedSat: (*Amount)(big.NewInt(1)),
|
||||
SentSat: (*Amount)(big.NewInt(2)),
|
||||
Time: 1521514812,
|
||||
Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840",
|
||||
Txs: 1,
|
||||
},
|
||||
},
|
||||
groupByTime: 3600,
|
||||
want: []BalanceHistory{
|
||||
{
|
||||
ReceivedSat: (*Amount)(big.NewInt(1)),
|
||||
SentSat: (*Amount)(big.NewInt(2)),
|
||||
Time: 1521514800,
|
||||
Txs: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aggregate",
|
||||
a: []BalanceHistory{
|
||||
{
|
||||
ReceivedSat: (*Amount)(big.NewInt(1)),
|
||||
SentSat: (*Amount)(big.NewInt(2)),
|
||||
Time: 1521504812,
|
||||
Txid: "0011223344556677889900112233445566778899001122334455667788990011",
|
||||
Txs: 1,
|
||||
},
|
||||
{
|
||||
ReceivedSat: (*Amount)(big.NewInt(3)),
|
||||
SentSat: (*Amount)(big.NewInt(4)),
|
||||
Time: 1521504812,
|
||||
Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840",
|
||||
Txs: 1,
|
||||
},
|
||||
{
|
||||
ReceivedSat: (*Amount)(big.NewInt(5)),
|
||||
SentSat: (*Amount)(big.NewInt(6)),
|
||||
Time: 1521514812,
|
||||
Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840",
|
||||
Txs: 1,
|
||||
},
|
||||
{
|
||||
ReceivedSat: (*Amount)(big.NewInt(7)),
|
||||
SentSat: (*Amount)(big.NewInt(8)),
|
||||
Time: 1521504812,
|
||||
Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840",
|
||||
Txs: 1,
|
||||
},
|
||||
{
|
||||
ReceivedSat: (*Amount)(big.NewInt(9)),
|
||||
SentSat: (*Amount)(big.NewInt(10)),
|
||||
Time: 1521534812,
|
||||
Txid: "0011223344556677889900112233445566778899001122334455667788990011",
|
||||
Txs: 1,
|
||||
},
|
||||
{
|
||||
ReceivedSat: (*Amount)(big.NewInt(11)),
|
||||
SentSat: (*Amount)(big.NewInt(12)),
|
||||
Time: 1521534812,
|
||||
Txid: "1122334455667788990011223344556677889900112233445566778899001100",
|
||||
Txs: 1,
|
||||
},
|
||||
},
|
||||
groupByTime: 3600,
|
||||
want: []BalanceHistory{
|
||||
{
|
||||
ReceivedSat: (*Amount)(big.NewInt(11)),
|
||||
SentSat: (*Amount)(big.NewInt(14)),
|
||||
Time: 1521504000,
|
||||
Txs: 2,
|
||||
},
|
||||
{
|
||||
ReceivedSat: (*Amount)(big.NewInt(5)),
|
||||
SentSat: (*Amount)(big.NewInt(6)),
|
||||
Time: 1521514800,
|
||||
Txs: 1,
|
||||
},
|
||||
{
|
||||
ReceivedSat: (*Amount)(big.NewInt(20)),
|
||||
SentSat: (*Amount)(big.NewInt(22)),
|
||||
Time: 1521532800,
|
||||
Txs: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.a.SortAndAggregate(tt.groupByTime); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("BalanceHistories.SortAndAggregate() = %+v, want %+v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
200
api/worker.go
200
api/worker.go
|
@ -802,6 +802,172 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
|
|||
return r, nil
|
||||
}
|
||||
|
||||
func (w *Worker) balanceHistoryHeightsFromTo(fromTime, toTime time.Time) (uint32, uint32, uint32, uint32) {
|
||||
fromUnix := uint32(0)
|
||||
toUnix := maxUint32
|
||||
fromHeight := uint32(0)
|
||||
toHeight := maxUint32
|
||||
if !fromTime.IsZero() {
|
||||
fromUnix = uint32(fromTime.Unix())
|
||||
fromHeight = w.is.GetBlockHeightOfTime(fromUnix)
|
||||
}
|
||||
if !toTime.IsZero() {
|
||||
toUnix = uint32(toTime.Unix())
|
||||
toHeight = w.is.GetBlockHeightOfTime(toUnix)
|
||||
}
|
||||
return fromUnix, fromHeight, toUnix, toHeight
|
||||
}
|
||||
|
||||
func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid string, fromUnix, toUnix uint32) (*BalanceHistory, error) {
|
||||
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)
|
||||
}
|
||||
time = w.is.GetBlockTime(height)
|
||||
if time < fromUnix || time >= toUnix {
|
||||
return nil, nil
|
||||
}
|
||||
bh := BalanceHistory{
|
||||
Time: time,
|
||||
Txs: 1,
|
||||
SentSat: &Amount{},
|
||||
ReceivedSat: &Amount{},
|
||||
Txid: txid,
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &bh, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// GetBalanceHistory returns history of balance for given address
|
||||
func (w *Worker) GetBalanceHistory(address string, fromTime, toTime time.Time, fiat string) (BalanceHistories, error) {
|
||||
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)
|
||||
if fromHeight >= toHeight {
|
||||
return bhs, nil
|
||||
}
|
||||
txs, err := w.getAddressTxids(addrDesc, false, &AddressFilter{Vout: AddressFilterVoutOff, FromHeight: fromHeight, ToHeight: toHeight}, maxInt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for txi := len(txs) - 1; txi >= 0; txi-- {
|
||||
bh, err := w.balanceHistoryForTxid(addrDesc, txs[txi], fromUnix, toUnix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bh != nil {
|
||||
bhs = append(bhs, *bh)
|
||||
}
|
||||
}
|
||||
bha := bhs.SortAndAggregate(3600)
|
||||
if fiat != "" {
|
||||
err = w.setFiatRateToBalanceHistories(bha, fiat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
glog.Info("GetBalanceHistory ", address, ", blocks ", fromHeight, "-", toHeight, ", count ", len(bha), " finished in ", time.Since(start))
|
||||
return bha, nil
|
||||
}
|
||||
|
||||
func (w *Worker) waitForBackendSync() {
|
||||
// wait a short time if blockbook is synchronizing with backend
|
||||
inSync, _, _ := w.is.GetSyncState()
|
||||
|
@ -975,29 +1141,27 @@ func (w *Worker) GetBlocks(page int, blocksOnPage int) (*Blocks, error) {
|
|||
|
||||
// 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)
|
||||
rates := make(map[string]json.Number, 2)
|
||||
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)
|
||||
if rate, found := ticker.Rates[currency]; !found {
|
||||
availableCurrencies := make([]string, 0, len(ticker.Rates))
|
||||
for availableCurrency := range ticker.Rates {
|
||||
availableCurrencies = append(availableCurrencies, availableCurrency)
|
||||
}
|
||||
resultRates[currencySymbol] = ticker.Rates[currencySymbol]
|
||||
if currencySymbol == "usd" && currency == "usd" {
|
||||
break
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
result := &db.ResultTickerAsString{
|
||||
Timestamp: timeFormatted,
|
||||
Rates: resultRates,
|
||||
Rates: rates,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
@ -1007,7 +1171,7 @@ func (w *Worker) GetFiatRatesForBlockID(bid string, currency string) (*db.Result
|
|||
if currency == "" {
|
||||
return nil, NewAPIError("Missing or empty \"currency\" parameter", true)
|
||||
}
|
||||
ticker := &db.CurrencyRatesTicker{}
|
||||
var ticker *db.CurrencyRatesTicker
|
||||
bi, err := w.getBlockInfoFromBlockID(bid)
|
||||
if err != nil {
|
||||
if err == bchain.ErrBlockNotFound {
|
||||
|
|
43
api/xpub.go
43
api/xpub.go
|
@ -589,3 +589,46 @@ func (w *Worker) GetXpubUtxo(xpub string, onlyConfirmed bool, gap int) (Utxos, e
|
|||
glog.Info("GetXpubUtxo ", xpub[:16], ", ", len(r), " utxos, finished in ", time.Since(start))
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// GetXpubBalanceHistory returns history of balance for given xpub
|
||||
func (w *Worker) GetXpubBalanceHistory(xpub string, fromTime, toTime time.Time, fiat string, gap int) (BalanceHistories, error) {
|
||||
bhs := make(BalanceHistories, 0)
|
||||
start := time.Now()
|
||||
fromUnix, fromHeight, toUnix, toHeight := w.balanceHistoryHeightsFromTo(fromTime, toTime)
|
||||
if fromHeight >= toHeight {
|
||||
return bhs, nil
|
||||
}
|
||||
data, _, err := w.getXpubData(xpub, 0, 1, AccountDetailsTxidHistory, &AddressFilter{
|
||||
Vout: AddressFilterVoutOff,
|
||||
OnlyConfirmed: true,
|
||||
FromHeight: fromHeight,
|
||||
ToHeight: toHeight,
|
||||
}, gap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} {
|
||||
for i := range da {
|
||||
ad := &da[i]
|
||||
txids := ad.txids
|
||||
for txi := len(txids) - 1; txi >= 0; txi-- {
|
||||
bh, err := w.balanceHistoryForTxid(ad.addrDesc, txids[txi].txid, fromUnix, toUnix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bh != nil {
|
||||
bhs = append(bhs, *bh)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bha := bhs.SortAndAggregate(3600)
|
||||
if fiat != "" {
|
||||
err = w.setFiatRateToBalanceHistories(bha, fiat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
glog.Info("GetUtxoBalanceHistory ", xpub[:16], ", blocks ", fromHeight, "-", toHeight, ", count ", len(bha), ", finished in ", time.Since(start))
|
||||
return bha, nil
|
||||
}
|
||||
|
|
|
@ -74,9 +74,9 @@ func erc20GetTransfersFromLog(logs []*rpcLog) ([]bchain.Erc20Transfer, error) {
|
|||
return nil, err
|
||||
}
|
||||
r = append(r, bchain.Erc20Transfer{
|
||||
Contract: strings.ToLower(l.Address),
|
||||
From: strings.ToLower(from),
|
||||
To: strings.ToLower(to),
|
||||
Contract: EIP55AddressFromAddress(l.Address),
|
||||
From: EIP55AddressFromAddress(from),
|
||||
To: EIP55AddressFromAddress(to),
|
||||
Tokens: t,
|
||||
})
|
||||
}
|
||||
|
@ -97,9 +97,9 @@ func erc20GetTransfersFromTx(tx *rpcTransaction) ([]bchain.Erc20Transfer, error)
|
|||
return nil, errors.New("Data is not a number")
|
||||
}
|
||||
r = append(r, bchain.Erc20Transfer{
|
||||
Contract: strings.ToLower(tx.To),
|
||||
From: strings.ToLower(tx.From),
|
||||
To: strings.ToLower(to),
|
||||
Contract: EIP55AddressFromAddress(tx.To),
|
||||
From: EIP55AddressFromAddress(tx.From),
|
||||
To: EIP55AddressFromAddress(to),
|
||||
Tokens: t,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -202,6 +202,15 @@ func EIP55Address(addrDesc bchain.AddressDescriptor) string {
|
|||
return string(result)
|
||||
}
|
||||
|
||||
// EIP55AddressFromAddress returns an EIP55-compliant hex string representation of the address
|
||||
func EIP55AddressFromAddress(address string) string {
|
||||
b, err := hex.DecodeString(address)
|
||||
if err != nil {
|
||||
return address
|
||||
}
|
||||
return EIP55Address(b)
|
||||
}
|
||||
|
||||
// GetAddressesFromAddrDesc returns addresses for given address descriptor with flag if the addresses are searchable
|
||||
func (p *EthereumParser) GetAddressesFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]string, bool, error) {
|
||||
return []string{EIP55Address(addrDesc)}, true, nil
|
||||
|
|
|
@ -2,6 +2,7 @@ package common
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
@ -45,6 +46,7 @@ type InternalState struct {
|
|||
IsSynchronized bool `json:"isSynchronized"`
|
||||
BestHeight uint32 `json:"bestHeight"`
|
||||
LastSync time.Time `json:"lastSync"`
|
||||
BlockTimes []uint32 `json:"-"`
|
||||
|
||||
IsMempoolSynchronized bool `json:"isMempoolSynchronized"`
|
||||
MempoolSize int `json:"mempoolSize"`
|
||||
|
@ -164,6 +166,54 @@ func (is *InternalState) DBSizeTotal() int64 {
|
|||
return total
|
||||
}
|
||||
|
||||
// GetBlockTime returns block time if block found or 0
|
||||
func (is *InternalState) GetBlockTime(height uint32) uint32 {
|
||||
is.mux.Lock()
|
||||
defer is.mux.Unlock()
|
||||
if int(height) < len(is.BlockTimes) {
|
||||
return is.BlockTimes[height]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// AppendBlockTime appends block time to BlockTimes
|
||||
func (is *InternalState) AppendBlockTime(time uint32) {
|
||||
is.mux.Lock()
|
||||
defer is.mux.Unlock()
|
||||
is.BlockTimes = append(is.BlockTimes, time)
|
||||
}
|
||||
|
||||
// RemoveLastBlockTimes removes last times from BlockTimes
|
||||
func (is *InternalState) RemoveLastBlockTimes(count int) {
|
||||
is.mux.Lock()
|
||||
defer is.mux.Unlock()
|
||||
if len(is.BlockTimes) < count {
|
||||
count = len(is.BlockTimes)
|
||||
}
|
||||
is.BlockTimes = is.BlockTimes[:len(is.BlockTimes)-count]
|
||||
}
|
||||
|
||||
// GetBlockHeightOfTime returns block height of the first block with time greater or equal to the given time or MaxUint32 if no such block
|
||||
func (is *InternalState) GetBlockHeightOfTime(time uint32) uint32 {
|
||||
is.mux.Lock()
|
||||
defer is.mux.Unlock()
|
||||
height := sort.Search(len(is.BlockTimes), func(i int) bool { return time <= is.BlockTimes[i] })
|
||||
if height == len(is.BlockTimes) {
|
||||
return ^uint32(0)
|
||||
}
|
||||
// as the block times can sometimes be out of order try 20 blocks lower to locate a block with the time greater or equal to the given time
|
||||
max, height := height, height-20
|
||||
if height < 0 {
|
||||
height = 0
|
||||
}
|
||||
for ; height <= max; height++ {
|
||||
if time <= is.BlockTimes[height] {
|
||||
break
|
||||
}
|
||||
}
|
||||
return uint32(height)
|
||||
}
|
||||
|
||||
// Pack marshals internal state to json
|
||||
func (is *InternalState) Pack() ([]byte, error) {
|
||||
is.mux.Lock()
|
||||
|
|
|
@ -381,6 +381,12 @@ func (b *BulkConnect) Close() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
var err error
|
||||
b.d.is.BlockTimes, err = b.d.loadBlockTimes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.d.SetInconsistentState(false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -476,8 +476,11 @@ func (d *RocksDB) ConnectBlock(block *bchain.Block) error {
|
|||
if err := d.storeAddresses(wb, block.Height, addresses); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.db.Write(d.wo, wb)
|
||||
if err := d.db.Write(d.wo, wb); err != nil {
|
||||
return err
|
||||
}
|
||||
d.is.AppendBlockTime(uint32(block.Time))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Addresses index
|
||||
|
@ -1462,6 +1465,7 @@ func (d *RocksDB) DisconnectBlockRangeBitcoinType(lower uint32, higher uint32) e
|
|||
}
|
||||
err := d.db.Write(d.wo, wb)
|
||||
if err == nil {
|
||||
d.is.RemoveLastBlockTimes(int(higher-lower) + 1)
|
||||
glog.Infof("rocksdb: blocks %d-%d disconnected", lower, higher)
|
||||
}
|
||||
return err
|
||||
|
@ -1576,6 +1580,32 @@ func (d *RocksDB) internalDeleteTx(wb *gorocksdb.WriteBatch, key []byte) {
|
|||
// internal state
|
||||
const internalStateKey = "internalState"
|
||||
|
||||
func (d *RocksDB) loadBlockTimes() ([]uint32, error) {
|
||||
var times []uint32
|
||||
it := d.db.NewIteratorCF(d.ro, d.cfh[cfHeight])
|
||||
defer it.Close()
|
||||
counter := uint32(0)
|
||||
time := uint32(0)
|
||||
for it.SeekToFirst(); it.Valid(); it.Next() {
|
||||
height := unpackUint(it.Key().Data())
|
||||
if height > counter {
|
||||
glog.Warning("gap in cfHeight: expecting ", counter, ", got ", height)
|
||||
for ; counter < height; counter++ {
|
||||
times = append(times, time)
|
||||
}
|
||||
}
|
||||
counter++
|
||||
info, err := d.unpackBlockInfo(it.Value().Data())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
time = uint32(info.Time)
|
||||
times = append(times, time)
|
||||
}
|
||||
glog.Info("loaded ", len(times), " block times")
|
||||
return times, nil
|
||||
}
|
||||
|
||||
// LoadInternalState loads from db internal state or initializes a new one if not yet stored
|
||||
func (d *RocksDB) LoadInternalState(rpcCoin string) (*common.InternalState, error) {
|
||||
val, err := d.db.GetCF(d.ro, d.cfh[cfDefault], []byte(internalStateKey))
|
||||
|
@ -1621,6 +1651,10 @@ func (d *RocksDB) LoadInternalState(rpcCoin string) (*common.InternalState, erro
|
|||
}
|
||||
}
|
||||
is.DbColumns = nc
|
||||
is.BlockTimes, err = d.loadBlockTimes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// after load, reset the synchronization data
|
||||
is.IsSynchronized = false
|
||||
is.IsMempoolSynchronized = false
|
||||
|
|
|
@ -455,6 +455,7 @@ func (d *RocksDB) DisconnectBlockRangeEthereumType(lower uint32, higher uint32)
|
|||
d.storeAddressContracts(wb, contracts)
|
||||
err := d.db.Write(d.wo, wb)
|
||||
if err == nil {
|
||||
d.is.RemoveLastBlockTimes(int(higher-lower) + 1)
|
||||
glog.Infof("rocksdb: blocks %d-%d disconnected", lower, higher)
|
||||
}
|
||||
return err
|
||||
|
|
|
@ -167,6 +167,10 @@ func TestRocksDB_Index_EthereumType(t *testing.T) {
|
|||
})
|
||||
defer closeAndDestroyRocksDB(t, d)
|
||||
|
||||
if len(d.is.BlockTimes) != 0 {
|
||||
t.Fatal("Expecting is.BlockTimes 0, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
|
||||
// connect 1st block
|
||||
block1 := dbtestdata.GetTestEthereumTypeBlock1(d.chainParser)
|
||||
if err := d.ConnectBlock(block1); err != nil {
|
||||
|
@ -174,6 +178,10 @@ func TestRocksDB_Index_EthereumType(t *testing.T) {
|
|||
}
|
||||
verifyAfterEthereumTypeBlock1(t, d, false)
|
||||
|
||||
if len(d.is.BlockTimes) != 1 {
|
||||
t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
|
||||
// connect 2nd block
|
||||
block2 := dbtestdata.GetTestEthereumTypeBlock2(d.chainParser)
|
||||
if err := d.ConnectBlock(block2); err != nil {
|
||||
|
@ -181,6 +189,10 @@ func TestRocksDB_Index_EthereumType(t *testing.T) {
|
|||
}
|
||||
verifyAfterEthereumTypeBlock2(t, d)
|
||||
|
||||
if len(d.is.BlockTimes) != 2 {
|
||||
t.Fatal("Expecting is.BlockTimes 2, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
|
||||
// get transactions for various addresses / low-high ranges
|
||||
verifyGetTransactions(t, d, "0x"+dbtestdata.EthAddr55, 0, 10000000, []txidIndex{
|
||||
{"0x" + dbtestdata.EthTxidB2T2, ^2},
|
||||
|
@ -275,10 +287,18 @@ func TestRocksDB_Index_EthereumType(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
if len(d.is.BlockTimes) != 1 {
|
||||
t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
|
||||
// connect block again and verify the state of db
|
||||
if err := d.ConnectBlock(block2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyAfterEthereumTypeBlock2(t, d)
|
||||
|
||||
if len(d.is.BlockTimes) != 2 {
|
||||
t.Fatal("Expecting is.BlockTimes 2, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -182,7 +182,7 @@ func verifyAfterBitcoinTypeBlock1(t *testing.T, d *RocksDB, afterDisconnect bool
|
|||
if err := checkColumn(d, cfHeight, []keyPair{
|
||||
{
|
||||
"000370d5",
|
||||
"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997" + uintToHex(1534858021) + varuintToHex(2) + varuintToHex(1234567),
|
||||
"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997" + uintToHex(1521515026) + varuintToHex(2) + varuintToHex(1234567),
|
||||
nil,
|
||||
},
|
||||
}); err != nil {
|
||||
|
@ -288,12 +288,12 @@ func verifyAfterBitcoinTypeBlock2(t *testing.T, d *RocksDB) {
|
|||
if err := checkColumn(d, cfHeight, []keyPair{
|
||||
{
|
||||
"000370d5",
|
||||
"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997" + uintToHex(1534858021) + varuintToHex(2) + varuintToHex(1234567),
|
||||
"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997" + uintToHex(1521515026) + varuintToHex(2) + varuintToHex(1234567),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"000370d6",
|
||||
"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6" + uintToHex(1534859123) + varuintToHex(4) + varuintToHex(2345678),
|
||||
"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6" + uintToHex(1521595678) + varuintToHex(4) + varuintToHex(2345678),
|
||||
nil,
|
||||
},
|
||||
}); err != nil {
|
||||
|
@ -534,6 +534,10 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) {
|
|||
})
|
||||
defer closeAndDestroyRocksDB(t, d)
|
||||
|
||||
if len(d.is.BlockTimes) != 0 {
|
||||
t.Fatal("Expecting is.BlockTimes 0, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
|
||||
// connect 1st block - will log warnings about missing UTXO transactions in txAddresses column
|
||||
block1 := dbtestdata.GetTestBitcoinTypeBlock1(d.chainParser)
|
||||
if err := d.ConnectBlock(block1); err != nil {
|
||||
|
@ -541,6 +545,10 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) {
|
|||
}
|
||||
verifyAfterBitcoinTypeBlock1(t, d, false)
|
||||
|
||||
if len(d.is.BlockTimes) != 1 {
|
||||
t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
|
||||
// connect 2nd block - use some outputs from the 1st block as the inputs and 1 input uses tx from the same block
|
||||
block2 := dbtestdata.GetTestBitcoinTypeBlock2(d.chainParser)
|
||||
if err := d.ConnectBlock(block2); err != nil {
|
||||
|
@ -548,6 +556,10 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) {
|
|||
}
|
||||
verifyAfterBitcoinTypeBlock2(t, d)
|
||||
|
||||
if len(d.is.BlockTimes) != 2 {
|
||||
t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
|
||||
// get transactions for various addresses / low-high ranges
|
||||
verifyGetTransactions(t, d, dbtestdata.Addr2, 0, 1000000, []txidIndex{
|
||||
{dbtestdata.TxidB2T1, ^1},
|
||||
|
@ -608,7 +620,7 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) {
|
|||
Hash: "00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6",
|
||||
Txs: 4,
|
||||
Size: 2345678,
|
||||
Time: 1534859123,
|
||||
Time: 1521595678,
|
||||
Height: 225494,
|
||||
}
|
||||
if !reflect.DeepEqual(info, iw) {
|
||||
|
@ -651,12 +663,20 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
if len(d.is.BlockTimes) != 1 {
|
||||
t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
|
||||
// connect block again and verify the state of db
|
||||
if err := d.ConnectBlock(block2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyAfterBitcoinTypeBlock2(t, d)
|
||||
|
||||
if len(d.is.BlockTimes) != 2 {
|
||||
t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
|
||||
// test public methods for address balance and tx addresses
|
||||
ab, err := d.GetAddressBalance(dbtestdata.Addr5, AddressBalanceDetailUTXO)
|
||||
if err != nil {
|
||||
|
@ -746,6 +766,10 @@ func Test_BulkConnect_BitcoinType(t *testing.T) {
|
|||
t.Fatal("DB not in DbStateInconsistent")
|
||||
}
|
||||
|
||||
if len(d.is.BlockTimes) != 0 {
|
||||
t.Fatal("Expecting is.BlockTimes 0, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
|
||||
if err := bc.ConnectBlock(dbtestdata.GetTestBitcoinTypeBlock1(d.chainParser), false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -768,6 +792,10 @@ func Test_BulkConnect_BitcoinType(t *testing.T) {
|
|||
}
|
||||
|
||||
verifyAfterBitcoinTypeBlock2(t, d)
|
||||
|
||||
if len(d.is.BlockTimes) != 225495 {
|
||||
t.Fatal("Expecting is.BlockTimes 225495, got ", len(d.is.BlockTimes))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_packBigint_unpackBigint(t *testing.T) {
|
||||
|
|
|
@ -175,6 +175,7 @@ func (s *PublicServer) ConnectFullPublicInterface() {
|
|||
serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock, apiDefault))
|
||||
serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx, apiDefault))
|
||||
serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiDefault))
|
||||
serveMux.HandleFunc(path+"api/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault))
|
||||
// v2 format
|
||||
serveMux.HandleFunc(path+"api/v2/block-index/", s.jsonHandler(s.apiBlockIndex, apiV2))
|
||||
serveMux.HandleFunc(path+"api/v2/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV2))
|
||||
|
@ -186,6 +187,7 @@ func (s *PublicServer) ConnectFullPublicInterface() {
|
|||
serveMux.HandleFunc(path+"api/v2/sendtx/", s.jsonHandler(s.apiSendTx, apiV2))
|
||||
serveMux.HandleFunc(path+"api/v2/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV2))
|
||||
serveMux.HandleFunc(path+"api/v2/feestats/", s.jsonHandler(s.apiFeeStats, apiV2))
|
||||
serveMux.HandleFunc(path+"api/v2/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
|
||||
|
@ -1038,6 +1040,36 @@ func (s *PublicServer) apiUtxo(r *http.Request, apiVersion int) (interface{}, er
|
|||
return utxo, err
|
||||
}
|
||||
|
||||
func (s *PublicServer) apiBalanceHistory(r *http.Request, apiVersion int) (interface{}, error) {
|
||||
var history []api.BalanceHistory
|
||||
var fromTime, toTime time.Time
|
||||
var err error
|
||||
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
|
||||
gap, ec := strconv.Atoi(r.URL.Query().Get("gap"))
|
||||
if ec != nil {
|
||||
gap = 0
|
||||
}
|
||||
t := r.URL.Query().Get("from")
|
||||
if t != "" {
|
||||
fromTime, _ = time.Parse("2006-01-02", t)
|
||||
}
|
||||
t = r.URL.Query().Get("to")
|
||||
if t != "" {
|
||||
// time.RFC3339
|
||||
toTime, _ = time.Parse("2006-01-02", t)
|
||||
}
|
||||
fiat := r.URL.Query().Get("fiatcurrency")
|
||||
history, err = s.api.GetXpubBalanceHistory(r.URL.Path[i+1:], fromTime, toTime, fiat, gap)
|
||||
if err == nil {
|
||||
s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-balancehistory"}).Inc()
|
||||
} else {
|
||||
history, err = s.api.GetBalanceHistory(r.URL.Path[i+1:], fromTime, toTime, fiat)
|
||||
s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-balancehistory"}).Inc()
|
||||
}
|
||||
}
|
||||
return history, err
|
||||
}
|
||||
|
||||
func (s *PublicServer) apiBlock(r *http.Request, apiVersion int) (interface{}, error) {
|
||||
var block *api.Block
|
||||
var err error
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -253,6 +253,36 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs
|
|||
}
|
||||
return
|
||||
},
|
||||
"getBalanceHistory": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) {
|
||||
r := struct {
|
||||
Descriptor string `json:"descriptor"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Fiat string `json:"fiat"`
|
||||
Gap int `json:"gap"`
|
||||
}{}
|
||||
err = json.Unmarshal(req.Params, &r)
|
||||
if err == nil {
|
||||
var fromTime, toTime time.Time
|
||||
if r.From != "" {
|
||||
fromTime, err = time.Parse("2006-01-02", r.From)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if r.To != "" {
|
||||
toTime, err = time.Parse("2006-01-02", r.To)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
rv, err = s.api.GetXpubBalanceHistory(r.Descriptor, fromTime, toTime, r.Fiat, r.Gap)
|
||||
if err != nil {
|
||||
rv, err = s.api.GetBalanceHistory(r.Descriptor, fromTime, toTime, r.Fiat)
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
"getTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) {
|
||||
r := struct {
|
||||
Txid string `json:"txid"`
|
||||
|
|
|
@ -161,6 +161,25 @@
|
|||
});
|
||||
}
|
||||
|
||||
function getBalanceHistory() {
|
||||
const descriptor = document.getElementById('getBalanceHistoryDescriptor').value.trim();
|
||||
const from = document.getElementById("getBalanceHistoryFrom").value.trim();
|
||||
const to = document.getElementById("getBalanceHistoryTo").value.trim();
|
||||
const fiat = document.getElementById("getBalanceHistoryFiat").value.trim();
|
||||
const method = 'getBalanceHistory';
|
||||
const params = {
|
||||
descriptor,
|
||||
from,
|
||||
to,
|
||||
fiat
|
||||
// default gap=20
|
||||
};
|
||||
send(method, params, function (result) {
|
||||
document.getElementById('getBalanceHistoryResult').innerText = JSON.stringify(result).replace(/,/g, ", ");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getTransaction() {
|
||||
const txid = document.getElementById('getTransactionTxid').value.trim();
|
||||
const method = 'getTransaction';
|
||||
|
@ -431,6 +450,26 @@
|
|||
<div class="col" id="getAccountUtxoResult">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="getBalanceHistory" onclick="getBalanceHistory()">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="row" style="margin: 0;">
|
||||
<input type="text" placeholder="descriptor" class="form-control" id="getBalanceHistoryDescriptor" value="0xba98d6a5ac827632e3457de7512d211e4ff7e8bd">
|
||||
</div>
|
||||
<div class="row" style="margin: 0; margin-top: 5px;">
|
||||
<input type="text" placeholder="from YYYY-MM-DD" style="width: 30%;margin-left: 5px;margin-right: 5px;" class="form-control" id="getBalanceHistoryFrom">
|
||||
<input type="text" placeholder="to YYYY-MM-DD" style="width: 30%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getBalanceHistoryTo">
|
||||
<input type="text" placeholder="fiat" style="width: 20%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getBalanceHistoryFiat">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col form-inline"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="getBalanceHistoryResult">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="getTransaction" onclick="getTransaction()">
|
||||
|
@ -492,6 +531,42 @@
|
|||
<div class="row">
|
||||
<div class="col" id="sendTransactionResult"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<input class="btn btn-secondary" type="button" value="get fiat rates for dates" onclick="getFiatRatesForDates()">
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<input type="text" class="form-control" id="getFiatRatesForDatesCurrency" value="usd">
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<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-2">
|
||||
<input class="btn btn-secondary" type="button" value="get current fiat rates" onclick="getCurrentFiatRates()">
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<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-2">
|
||||
<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 block" onclick="subscribeNewBlock()">
|
||||
|
@ -524,52 +599,16 @@
|
|||
<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">
|
||||
<div class="col-3">
|
||||
<input class="btn btn-secondary" type="button" value="subscribe new fiat rates" onclick="subscribeNewFiatRatesTicker()">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="col-1">
|
||||
<span id="subscribeNewFiatRatesTickerId"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="col-1">
|
||||
<input type="text" class="form-control" id="subscribeFiatRatesCurrency" value="usd">
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="col-5">
|
||||
<input class="btn btn-secondary" id="unsubscribeNewFiatRatesTickerButton" style="display: none;" type="button" value="unsubscribe" onclick="unsubscribeNewFiatRatesTicker()">
|
||||
</div>
|
||||
</div>
|
||||
|
@ -577,6 +616,7 @@
|
|||
<div class="col" id="subscribeNewFiatRatesTickerResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
<br><br>
|
||||
</body>
|
||||
<script>
|
||||
document.getElementById('serverAddress').value = window.location.protocol + "//" + window.location.host;
|
||||
|
|
|
@ -68,7 +68,7 @@ func GetTestBitcoinTypeBlock1(parser bchain.BlockChainParser) *bchain.Block {
|
|||
Height: 225493,
|
||||
Hash: "0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997",
|
||||
Size: 1234567,
|
||||
Time: 1534858021,
|
||||
Time: 1521515026,
|
||||
Confirmations: 2,
|
||||
},
|
||||
Txs: []bchain.Tx{
|
||||
|
@ -91,8 +91,8 @@ func GetTestBitcoinTypeBlock1(parser bchain.BlockChainParser) *bchain.Block {
|
|||
ValueSat: *SatB1T1A2,
|
||||
},
|
||||
},
|
||||
Blocktime: 22549300000,
|
||||
Time: 22549300000,
|
||||
Blocktime: 1521515026,
|
||||
Time: 1521515026,
|
||||
Confirmations: 2,
|
||||
},
|
||||
{
|
||||
|
@ -120,8 +120,8 @@ func GetTestBitcoinTypeBlock1(parser bchain.BlockChainParser) *bchain.Block {
|
|||
ValueSat: *SatB1T2A5,
|
||||
},
|
||||
},
|
||||
Blocktime: 22549300001,
|
||||
Time: 22549300001,
|
||||
Blocktime: 1521515026,
|
||||
Time: 1521515026,
|
||||
Confirmations: 2,
|
||||
},
|
||||
},
|
||||
|
@ -135,7 +135,7 @@ func GetTestBitcoinTypeBlock2(parser bchain.BlockChainParser) *bchain.Block {
|
|||
Height: 225494,
|
||||
Hash: "00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6",
|
||||
Size: 2345678,
|
||||
Time: 1534859123,
|
||||
Time: 1521595678,
|
||||
Confirmations: 1,
|
||||
},
|
||||
Txs: []bchain.Tx{
|
||||
|
@ -176,8 +176,8 @@ func GetTestBitcoinTypeBlock2(parser bchain.BlockChainParser) *bchain.Block {
|
|||
ValueSat: *SatZero,
|
||||
},
|
||||
},
|
||||
Blocktime: 22549400000,
|
||||
Time: 22549400000,
|
||||
Blocktime: 1521595678,
|
||||
Time: 1521595678,
|
||||
Confirmations: 1,
|
||||
},
|
||||
{
|
||||
|
@ -210,8 +210,8 @@ func GetTestBitcoinTypeBlock2(parser bchain.BlockChainParser) *bchain.Block {
|
|||
ValueSat: *SatB2T2A9,
|
||||
},
|
||||
},
|
||||
Blocktime: 22549400001,
|
||||
Time: 22549400001,
|
||||
Blocktime: 1521595678,
|
||||
Time: 1521595678,
|
||||
Confirmations: 1,
|
||||
},
|
||||
// transaction from the same address in the previous block
|
||||
|
@ -233,8 +233,8 @@ func GetTestBitcoinTypeBlock2(parser bchain.BlockChainParser) *bchain.Block {
|
|||
ValueSat: *SatB2T3A5,
|
||||
},
|
||||
},
|
||||
Blocktime: 22549400002,
|
||||
Time: 22549400002,
|
||||
Blocktime: 1521595678,
|
||||
Time: 1521595678,
|
||||
Confirmations: 1,
|
||||
},
|
||||
// mining transaction
|
||||
|
@ -259,8 +259,8 @@ func GetTestBitcoinTypeBlock2(parser bchain.BlockChainParser) *bchain.Block {
|
|||
ValueSat: *SatZero,
|
||||
},
|
||||
},
|
||||
Blocktime: 22549400003,
|
||||
Time: 22549400003,
|
||||
Blocktime: 1521595678,
|
||||
Time: 1521595678,
|
||||
Confirmations: 1,
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue