diff --git a/api/worker.go b/api/worker.go index d0888e27..86396f8c 100644 --- a/api/worker.go +++ b/api/worker.go @@ -801,14 +801,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco return r, nil } -// GetBalanceHistory returns history of balance for given address -func (w *Worker) GetBalanceHistory(address string, fromTime, toTime time.Time) (BalanceHistories, error) { - bhs := make(BalanceHistories, 0) - start := time.Now() - addrDesc, _, err := w.getAddrDescAndNormalizeAddress(address) - if err != nil { - return nil, err - } +func (w *Worker) balanceHistoryHeightsFromTo(fromTime, toTime time.Time) (uint32, uint32, uint32, uint32) { fromUnix := uint32(0) toUnix := maxUint32 fromHeight := uint32(0) @@ -821,6 +814,53 @@ func (w *Worker) GetBalanceHistory(address string, fromTime, toTime time.Time) ( 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) { + 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 + } + time := w.is.GetBlockTime(ta.Height) + if time < fromUnix || time >= toUnix { + return nil, nil + } + bh := BalanceHistory{ + Time: time, + Txs: 1, + SentSat: &Amount{}, + ReceivedSat: &Amount{}, + Txid: txid, + } + 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) + } + } + return &bh, nil +} + +// GetBalanceHistory returns history of balance for given address +func (w *Worker) GetBalanceHistory(address string, fromTime, toTime time.Time) (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 } @@ -829,41 +869,16 @@ func (w *Worker) GetBalanceHistory(address string, fromTime, toTime time.Time) ( return nil, err } for txi := len(txs) - 1; txi >= 0; txi-- { - ta, err := w.db.GetTxAddresses(txs[txi]) + bh, err := w.balanceHistoryForTxid(addrDesc, txs[txi], fromUnix, toUnix) if err != nil { return nil, err } - if ta == nil { - glog.Warning("DB inconsistency: tx ", txs[txi], ": not found in txAddresses") - continue + if bh != nil { + bhs = append(bhs, *bh) } - time := w.is.GetBlockTime(ta.Height) - if time < fromUnix || time >= toUnix { - continue - } - bh := BalanceHistory{ - Time: time, - Txs: 1, - SentSat: &Amount{}, - ReceivedSat: &Amount{}, - Txid: txs[txi], - } - 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) - } - } - bhs = append(bhs, bh) } bha := bhs.SortAndAggregate(3600) - glog.Info("GetBalanceHistory ", address, ", count ", len(bha), " finished in ", time.Since(start)) + glog.Info("GetBalanceHistory ", address, ", blocks ", fromHeight, "-", toHeight, ", count ", len(bha), " finished in ", time.Since(start)) return bha, nil } diff --git a/api/xpub.go b/api/xpub.go index 85522b3d..5e014b96 100644 --- a/api/xpub.go +++ b/api/xpub.go @@ -591,6 +591,38 @@ func (w *Worker) GetXpubUtxo(xpub string, onlyConfirmed bool, gap int) (Utxos, e } // GetUtxoBalanceHistory returns history of balance for given xpub -func (w *Worker) GetUtxoBalanceHistory(xpub string, gap int) ([]BalanceHistory, error) { - return nil, errors.New("not implemented") +func (w *Worker) GetUtxoBalanceHistory(xpub string, fromTime, toTime time.Time, 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) + glog.Info("GetUtxoBalanceHistory ", xpub[:16], ", blocks ", fromHeight, "-", toHeight, ", count ", len(bha), ", finished in ", time.Since(start)) + return bha, nil } diff --git a/server/public.go b/server/public.go index 44e53115..c4fdc8c2 100644 --- a/server/public.go +++ b/server/public.go @@ -1051,7 +1051,7 @@ func (s *PublicServer) apiBalanceHistory(r *http.Request, apiVersion int) (inter // time.RFC3339 toTime, _ = time.Parse("2006-01-02", t) } - history, err = s.api.GetUtxoBalanceHistory(r.URL.Path[i+1:], gap) + history, err = s.api.GetUtxoBalanceHistory(r.URL.Path[i+1:], fromTime, toTime, gap) if err == nil { s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-balancehistory"}).Inc() } else { diff --git a/server/public_test.go b/server/public_test.go index bbf7432a..fdeab6c7 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -636,6 +636,33 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { `[{"blockTime":1521514800,"txs":1,"received":"12345","sent":"0"}]`, }, }, + { + name: "apiBalanceHistory xpub v2", + r: newGetRequest(ts.URL + "/api/v2/balancehistory/" + dbtestdata.Xpub), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `[{"blockTime":1521514800,"txs":1,"received":"1","sent":"0"},{"blockTime":1521594000,"txs":1,"received":"118641975500","sent":"1"}]`, + }, + }, + { + name: "apiBalanceHistory xpub v2 from=2018-03-20&to=2018-03-21", + r: newGetRequest(ts.URL + "/api/v2/balancehistory/" + dbtestdata.Xpub + "?from=2018-03-20&to=2018-03-21"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `[{"blockTime":1521514800,"txs":1,"received":"1","sent":"0"}]`, + }, + }, + { + name: "apiBalanceHistory xpub v2 from=2018-03-21", + r: newGetRequest(ts.URL + "/api/v2/balancehistory/" + dbtestdata.Xpub + "?from=2018-03-21"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `[{"blockTime":1521594000,"txs":1,"received":"118641975500","sent":"1"}]`, + }, + }, { name: "apiSendTx", r: newGetRequest(ts.URL + "/api/v2/sendtx/1234567890"),