github.com/trezor/blockbook@v0.4.1-0.20240328132726-e9a08582ee2c/api/xpub.go (about)

     1  package api
     2  
     3  import (
     4  	"fmt"
     5  	"math/big"
     6  	"sort"
     7  	"strconv"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/golang/glog"
    12  	"github.com/juju/errors"
    13  	"github.com/trezor/blockbook/bchain"
    14  	"github.com/trezor/blockbook/db"
    15  )
    16  
    17  const defaultAddressesGap = 20
    18  const maxAddressesGap = 10000
    19  
    20  const txInput = 1
    21  const txOutput = 2
    22  
    23  const xpubCacheExpirationSeconds = 3600
    24  
    25  var cachedXpubs map[string]xpubData
    26  var cachedXpubsMux sync.Mutex
    27  
    28  const xpubLogPrefix = 30
    29  
    30  type xpubTxid struct {
    31  	txid        string
    32  	height      uint32
    33  	inputOutput byte
    34  }
    35  
    36  type xpubTxids []xpubTxid
    37  
    38  func (a xpubTxids) Len() int      { return len(a) }
    39  func (a xpubTxids) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
    40  func (a xpubTxids) Less(i, j int) bool {
    41  	// if the heights are equal, make inputs less than outputs
    42  	hi := a[i].height
    43  	hj := a[j].height
    44  	if hi == hj {
    45  		return (a[i].inputOutput & txInput) >= (a[j].inputOutput & txInput)
    46  	}
    47  	return hi > hj
    48  }
    49  
    50  type xpubAddress struct {
    51  	addrDesc  bchain.AddressDescriptor
    52  	balance   *db.AddrBalance
    53  	txs       uint32
    54  	maxHeight uint32
    55  	complete  bool
    56  	txids     xpubTxids
    57  }
    58  
    59  type xpubData struct {
    60  	descriptor      *bchain.XpubDescriptor
    61  	gap             int
    62  	accessed        int64
    63  	basePath        string
    64  	dataHeight      uint32
    65  	dataHash        string
    66  	txCountEstimate uint32
    67  	sentSat         big.Int
    68  	balanceSat      big.Int
    69  	addresses       [][]xpubAddress
    70  }
    71  
    72  func (w *Worker) initXpubCache() {
    73  	cachedXpubsMux.Lock()
    74  	if cachedXpubs == nil {
    75  		cachedXpubs = make(map[string]xpubData)
    76  		go func() {
    77  			for {
    78  				time.Sleep(20 * time.Second)
    79  				w.evictXpubCacheItems()
    80  			}
    81  		}()
    82  	}
    83  	cachedXpubsMux.Unlock()
    84  }
    85  
    86  func (w *Worker) evictXpubCacheItems() {
    87  	cachedXpubsMux.Lock()
    88  	defer cachedXpubsMux.Unlock()
    89  	threshold := time.Now().Unix() - xpubCacheExpirationSeconds
    90  	count := 0
    91  	for k, v := range cachedXpubs {
    92  		if v.accessed < threshold {
    93  			delete(cachedXpubs, k)
    94  			count++
    95  		}
    96  	}
    97  	w.metrics.XPubCacheSize.Set(float64(len(cachedXpubs)))
    98  	glog.Info("Evicted ", count, " items from xpub cache, cache size ", len(cachedXpubs))
    99  }
   100  
   101  func (w *Worker) xpubGetAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool, fromHeight, toHeight uint32, maxResults int) ([]xpubTxid, bool, error) {
   102  	var err error
   103  	complete := true
   104  	txs := make([]xpubTxid, 0, 4)
   105  	var callback db.GetTransactionsCallback
   106  	callback = func(txid string, height uint32, indexes []int32) error {
   107  		// take all txs in the last found block even if it exceeds maxResults
   108  		if len(txs) >= maxResults && txs[len(txs)-1].height != height {
   109  			complete = false
   110  			return &db.StopIteration{}
   111  		}
   112  		inputOutput := byte(0)
   113  		for _, index := range indexes {
   114  			if index < 0 {
   115  				inputOutput |= txInput
   116  			} else {
   117  				inputOutput |= txOutput
   118  			}
   119  		}
   120  		txs = append(txs, xpubTxid{txid, height, inputOutput})
   121  		return nil
   122  	}
   123  	if mempool {
   124  		uniqueTxs := make(map[string]int)
   125  		o, err := w.mempool.GetAddrDescTransactions(addrDesc)
   126  		if err != nil {
   127  			return nil, false, err
   128  		}
   129  		for _, m := range o {
   130  			if l, found := uniqueTxs[m.Txid]; !found {
   131  				l = len(txs)
   132  				callback(m.Txid, 0, []int32{m.Vout})
   133  				if len(txs) > l {
   134  					uniqueTxs[m.Txid] = l
   135  				}
   136  			} else {
   137  				if m.Vout < 0 {
   138  					txs[l].inputOutput |= txInput
   139  				} else {
   140  					txs[l].inputOutput |= txOutput
   141  				}
   142  			}
   143  		}
   144  	} else {
   145  		err = w.db.GetAddrDescTransactions(addrDesc, fromHeight, toHeight, callback)
   146  		if err != nil {
   147  			return nil, false, err
   148  		}
   149  	}
   150  	return txs, complete, nil
   151  }
   152  
   153  func (w *Worker) xpubCheckAndLoadTxids(ad *xpubAddress, filter *AddressFilter, maxHeight uint32, maxResults int) error {
   154  	// skip if not used
   155  	if ad.balance == nil {
   156  		return nil
   157  	}
   158  	// if completely loaded, check if there are not some new txs and load if necessary
   159  	if ad.complete {
   160  		if ad.balance.Txs != ad.txs {
   161  			newTxids, _, err := w.xpubGetAddressTxids(ad.addrDesc, false, ad.maxHeight+1, maxHeight, maxInt)
   162  			if err == nil {
   163  				ad.txids = append(newTxids, ad.txids...)
   164  				ad.maxHeight = maxHeight
   165  				ad.txs = uint32(len(ad.txids))
   166  				if ad.txs != ad.balance.Txs {
   167  					glog.Warning("xpubCheckAndLoadTxids inconsistency ", ad.addrDesc, ", ad.txs=", ad.txs, ", ad.balance.Txs=", ad.balance.Txs)
   168  				}
   169  			}
   170  			return err
   171  		}
   172  		return nil
   173  	}
   174  	// load all txids to get paging correctly
   175  	newTxids, complete, err := w.xpubGetAddressTxids(ad.addrDesc, false, 0, maxHeight, maxInt)
   176  	if err != nil {
   177  		return err
   178  	}
   179  	ad.txids = newTxids
   180  	ad.complete = complete
   181  	ad.maxHeight = maxHeight
   182  	if complete {
   183  		ad.txs = uint32(len(ad.txids))
   184  		if ad.txs != ad.balance.Txs {
   185  			glog.Warning("xpubCheckAndLoadTxids inconsistency ", ad.addrDesc, ", ad.txs=", ad.txs, ", ad.balance.Txs=", ad.balance.Txs)
   186  		}
   187  	}
   188  	return nil
   189  }
   190  
   191  func (w *Worker) xpubDerivedAddressBalance(data *xpubData, ad *xpubAddress) (bool, error) {
   192  	var err error
   193  	if ad.balance, err = w.db.GetAddrDescBalance(ad.addrDesc, db.AddressBalanceDetailUTXO); err != nil {
   194  		return false, err
   195  	}
   196  	if ad.balance != nil {
   197  		data.txCountEstimate += ad.balance.Txs
   198  		data.sentSat.Add(&data.sentSat, &ad.balance.SentSat)
   199  		data.balanceSat.Add(&data.balanceSat, &ad.balance.BalanceSat)
   200  		return true, nil
   201  	}
   202  	return false, nil
   203  }
   204  
   205  func (w *Worker) xpubScanAddresses(xd *bchain.XpubDescriptor, data *xpubData, addresses []xpubAddress, gap int, change uint32, minDerivedIndex int, fork bool) (int, []xpubAddress, error) {
   206  	// rescan known addresses
   207  	lastUsed := 0
   208  	for i := range addresses {
   209  		ad := &addresses[i]
   210  		if fork {
   211  			// reset the cached data
   212  			ad.txs = 0
   213  			ad.maxHeight = 0
   214  			ad.complete = false
   215  			ad.txids = nil
   216  		}
   217  		used, err := w.xpubDerivedAddressBalance(data, ad)
   218  		if err != nil {
   219  			return 0, nil, err
   220  		}
   221  		if used {
   222  			lastUsed = i
   223  		}
   224  	}
   225  	// derive new addresses as necessary
   226  	missing := len(addresses) - lastUsed
   227  	for missing < gap {
   228  		from := len(addresses)
   229  		to := from + gap - missing
   230  		if to < minDerivedIndex {
   231  			to = minDerivedIndex
   232  		}
   233  		descriptors, err := w.chainParser.DeriveAddressDescriptorsFromTo(xd, change, uint32(from), uint32(to))
   234  		if err != nil {
   235  			return 0, nil, err
   236  		}
   237  		for i, a := range descriptors {
   238  			ad := xpubAddress{addrDesc: a}
   239  			used, err := w.xpubDerivedAddressBalance(data, &ad)
   240  			if err != nil {
   241  				return 0, nil, err
   242  			}
   243  			if used {
   244  				lastUsed = i + from
   245  			}
   246  			addresses = append(addresses, ad)
   247  		}
   248  		missing = len(addresses) - lastUsed
   249  	}
   250  	return lastUsed, addresses, nil
   251  }
   252  
   253  func (w *Worker) tokenFromXpubAddress(data *xpubData, ad *xpubAddress, changeIndex int, index int, option AccountDetails) Token {
   254  	a, _, _ := w.chainParser.GetAddressesFromAddrDesc(ad.addrDesc)
   255  	var address string
   256  	if len(a) > 0 {
   257  		address = a[0]
   258  	}
   259  	var balance, totalReceived, totalSent *big.Int
   260  	var transfers int
   261  	if ad.balance != nil {
   262  		transfers = int(ad.balance.Txs)
   263  		if option >= AccountDetailsTokenBalances {
   264  			balance = &ad.balance.BalanceSat
   265  			totalSent = &ad.balance.SentSat
   266  			totalReceived = ad.balance.ReceivedSat()
   267  		}
   268  	}
   269  	return Token{
   270  		Type:             bchain.XPUBAddressTokenType,
   271  		Name:             address,
   272  		Decimals:         w.chainParser.AmountDecimals(),
   273  		BalanceSat:       (*Amount)(balance),
   274  		TotalReceivedSat: (*Amount)(totalReceived),
   275  		TotalSentSat:     (*Amount)(totalSent),
   276  		Transfers:        transfers,
   277  		Path:             fmt.Sprintf("%s/%d/%d", data.basePath, changeIndex, index),
   278  	}
   279  }
   280  
   281  // returns true if addresses are "own", i.e. the address belongs to the xpub
   282  func isOwnAddresses(xpubAddresses map[string]struct{}, addresses []string) bool {
   283  	if len(addresses) == 1 {
   284  		_, found := xpubAddresses[addresses[0]]
   285  		return found
   286  	}
   287  	return false
   288  }
   289  
   290  func setIsOwnAddresses(txs []*Tx, xpubAddresses map[string]struct{}) {
   291  	for i := range txs {
   292  		tx := txs[i]
   293  		for j := range tx.Vin {
   294  			vin := &tx.Vin[j]
   295  			if isOwnAddresses(xpubAddresses, vin.Addresses) {
   296  				vin.IsOwn = true
   297  			}
   298  		}
   299  		for j := range tx.Vout {
   300  			vout := &tx.Vout[j]
   301  			if isOwnAddresses(xpubAddresses, vout.Addresses) {
   302  				vout.IsOwn = true
   303  			}
   304  		}
   305  	}
   306  }
   307  
   308  func (w *Worker) getXpubData(xd *bchain.XpubDescriptor, page int, txsOnPage int, option AccountDetails, filter *AddressFilter, gap int) (*xpubData, uint32, bool, error) {
   309  	if w.chainType != bchain.ChainBitcoinType {
   310  		return nil, 0, false, ErrUnsupportedXpub
   311  	}
   312  	var (
   313  		err        error
   314  		bestheight uint32
   315  		besthash   string
   316  	)
   317  	if gap <= 0 {
   318  		gap = defaultAddressesGap
   319  	} else if gap > maxAddressesGap {
   320  		// limit the maximum gap to protect against unreasonably big values that could cause high load of the server
   321  		gap = maxAddressesGap
   322  	}
   323  	// gap is increased one as there must be gap of empty addresses before the derivation is stopped
   324  	gap++
   325  	var processedHash string
   326  	cachedXpubsMux.Lock()
   327  	data, inCache := cachedXpubs[xd.XpubDescriptor]
   328  	cachedXpubsMux.Unlock()
   329  	// to load all data for xpub may take some time, do it in a loop to process a possible new block
   330  	for {
   331  		bestheight, besthash, err = w.db.GetBestBlock()
   332  		if err != nil {
   333  			return nil, 0, inCache, errors.Annotatef(err, "GetBestBlock")
   334  		}
   335  		if besthash == processedHash {
   336  			break
   337  		}
   338  		fork := false
   339  		if !inCache || data.gap != gap {
   340  			data = xpubData{
   341  				gap:       gap,
   342  				addresses: make([][]xpubAddress, len(xd.ChangeIndexes)),
   343  			}
   344  			data.basePath, err = w.chainParser.DerivationBasePath(xd)
   345  			if err != nil {
   346  				return nil, 0, inCache, err
   347  			}
   348  		} else {
   349  			hash, err := w.db.GetBlockHash(data.dataHeight)
   350  			if err != nil {
   351  				return nil, 0, inCache, err
   352  			}
   353  			if hash != data.dataHash {
   354  				// in case of for reset all cached data
   355  				fork = true
   356  			}
   357  		}
   358  		processedHash = besthash
   359  		if data.dataHeight < bestheight || fork {
   360  			data.dataHeight = bestheight
   361  			data.dataHash = besthash
   362  			data.balanceSat = *new(big.Int)
   363  			data.sentSat = *new(big.Int)
   364  			data.txCountEstimate = 0
   365  			var minDerivedIndex int
   366  			for i, change := range xd.ChangeIndexes {
   367  				minDerivedIndex, data.addresses[i], err = w.xpubScanAddresses(xd, &data, data.addresses[i], gap, change, minDerivedIndex, fork)
   368  				if err != nil {
   369  					return nil, 0, inCache, err
   370  				}
   371  			}
   372  		}
   373  		if option >= AccountDetailsTxidHistory {
   374  			for _, da := range data.addresses {
   375  				for i := range da {
   376  					if err = w.xpubCheckAndLoadTxids(&da[i], filter, bestheight, (page+1)*txsOnPage); err != nil {
   377  						return nil, 0, inCache, err
   378  					}
   379  				}
   380  			}
   381  		}
   382  	}
   383  	data.accessed = time.Now().Unix()
   384  	cachedXpubsMux.Lock()
   385  	cachedXpubs[xd.XpubDescriptor] = data
   386  	cachedXpubsMux.Unlock()
   387  	return &data, bestheight, inCache, nil
   388  }
   389  
   390  // GetXpubAddress computes address value and gets transactions for given address
   391  func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter, gap int, secondaryCoin string) (*Address, error) {
   392  	start := time.Now()
   393  	page--
   394  	if page < 0 {
   395  		page = 0
   396  	}
   397  	type mempoolMap struct {
   398  		tx          *Tx
   399  		inputOutput byte
   400  	}
   401  	var (
   402  		txc            xpubTxids
   403  		txmMap         map[string]*Tx
   404  		txCount        int
   405  		txs            []*Tx
   406  		txids          []string
   407  		pg             Paging
   408  		filtered       bool
   409  		uBalSat        big.Int
   410  		unconfirmedTxs int
   411  	)
   412  	xd, err := w.chainParser.ParseXpub(xpub)
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  	data, bestheight, inCache, err := w.getXpubData(xd, page, txsOnPage, option, filter, gap)
   417  	if err != nil {
   418  		return nil, err
   419  	}
   420  	// setup filtering of txids
   421  	var txidFilter func(txid *xpubTxid, ad *xpubAddress) bool
   422  	if !(filter.FromHeight == 0 && filter.ToHeight == 0 && filter.Vout == AddressFilterVoutOff) {
   423  		toHeight := maxUint32
   424  		if filter.ToHeight != 0 {
   425  			toHeight = filter.ToHeight
   426  		}
   427  		txidFilter = func(txid *xpubTxid, ad *xpubAddress) bool {
   428  			if txid.height < filter.FromHeight || txid.height > toHeight {
   429  				return false
   430  			}
   431  			if filter.Vout != AddressFilterVoutOff {
   432  				if filter.Vout == AddressFilterVoutInputs && txid.inputOutput&txInput == 0 ||
   433  					filter.Vout == AddressFilterVoutOutputs && txid.inputOutput&txOutput == 0 {
   434  					return false
   435  				}
   436  			}
   437  			return true
   438  		}
   439  		filtered = true
   440  	}
   441  	addresses := w.newAddressesMapForAliases()
   442  	// process mempool, only if ToHeight is not specified
   443  	if filter.ToHeight == 0 && !filter.OnlyConfirmed {
   444  		txmMap = make(map[string]*Tx)
   445  		mempoolEntries := make(bchain.MempoolTxidEntries, 0)
   446  		for _, da := range data.addresses {
   447  			for i := range da {
   448  				ad := &da[i]
   449  				newTxids, _, err := w.xpubGetAddressTxids(ad.addrDesc, true, 0, 0, maxInt)
   450  				if err != nil {
   451  					return nil, err
   452  				}
   453  				for _, txid := range newTxids {
   454  					// the same tx can have multiple addresses from the same xpub, get it from backend it only once
   455  					tx, foundTx := txmMap[txid.txid]
   456  					if !foundTx {
   457  						tx, err = w.getTransaction(txid.txid, false, true, addresses)
   458  						// mempool transaction may fail
   459  						if err != nil || tx == nil {
   460  							glog.Warning("GetTransaction in mempool: ", err)
   461  							continue
   462  						}
   463  						txmMap[txid.txid] = tx
   464  					}
   465  					// skip already confirmed txs, mempool may be out of sync
   466  					if tx.Confirmations == 0 {
   467  						if !foundTx {
   468  							unconfirmedTxs++
   469  						}
   470  						uBalSat.Add(&uBalSat, tx.getAddrVoutValue(ad.addrDesc))
   471  						uBalSat.Sub(&uBalSat, tx.getAddrVinValue(ad.addrDesc))
   472  						// mempool txs are returned only on the first page, uniquely and filtered
   473  						if page == 0 && !foundTx && (txidFilter == nil || txidFilter(&txid, ad)) {
   474  							mempoolEntries = append(mempoolEntries, bchain.MempoolTxidEntry{Txid: txid.txid, Time: uint32(tx.Blocktime)})
   475  						}
   476  					}
   477  				}
   478  			}
   479  		}
   480  		// sort the entries by time descending
   481  		sort.Sort(mempoolEntries)
   482  		for _, entry := range mempoolEntries {
   483  			if option == AccountDetailsTxidHistory {
   484  				txids = append(txids, entry.Txid)
   485  			} else if option >= AccountDetailsTxHistoryLight {
   486  				txs = append(txs, txmMap[entry.Txid])
   487  			}
   488  		}
   489  	}
   490  	if option >= AccountDetailsTxidHistory {
   491  		txcMap := make(map[string]bool)
   492  		txc = make(xpubTxids, 0, 32)
   493  		for _, da := range data.addresses {
   494  			for i := range da {
   495  				ad := &da[i]
   496  				for _, txid := range ad.txids {
   497  					added, foundTx := txcMap[txid.txid]
   498  					// count txs regardless of filter but only once
   499  					if !foundTx {
   500  						txCount++
   501  					}
   502  					// add tx only once
   503  					if !added {
   504  						add := txidFilter == nil || txidFilter(&txid, ad)
   505  						txcMap[txid.txid] = add
   506  						if add {
   507  							txc = append(txc, txid)
   508  						}
   509  					}
   510  				}
   511  			}
   512  		}
   513  		sort.Stable(txc)
   514  		txCount = len(txcMap)
   515  		totalResults := txCount
   516  		if filtered {
   517  			totalResults = -1
   518  		}
   519  		var from, to int
   520  		pg, from, to, page = computePaging(len(txc), page, txsOnPage)
   521  		if len(txc) >= txsOnPage {
   522  			if totalResults < 0 {
   523  				pg.TotalPages = -1
   524  			} else {
   525  				pg, _, _, _ = computePaging(totalResults, page, txsOnPage)
   526  			}
   527  		}
   528  		// get confirmed transactions
   529  		for i := from; i < to; i++ {
   530  			xpubTxid := &txc[i]
   531  			if option == AccountDetailsTxidHistory {
   532  				txids = append(txids, xpubTxid.txid)
   533  			} else {
   534  				tx, err := w.txFromTxid(xpubTxid.txid, bestheight, option, nil, addresses)
   535  				if err != nil {
   536  					return nil, err
   537  				}
   538  				txs = append(txs, tx)
   539  			}
   540  		}
   541  	} else {
   542  		txCount = int(data.txCountEstimate)
   543  	}
   544  	addrTxCount := int(data.txCountEstimate)
   545  	usedTokens := 0
   546  	var tokens []Token
   547  	var xpubAddresses map[string]struct{}
   548  	if option > AccountDetailsBasic {
   549  		tokens = make([]Token, 0, 4)
   550  		xpubAddresses = make(map[string]struct{})
   551  	}
   552  	for ci, da := range data.addresses {
   553  		for i := range da {
   554  			ad := &da[i]
   555  			if ad.balance != nil {
   556  				usedTokens++
   557  			}
   558  			if option > AccountDetailsBasic {
   559  				token := w.tokenFromXpubAddress(data, ad, int(xd.ChangeIndexes[ci]), i, option)
   560  				if filter.TokensToReturn == TokensToReturnDerived ||
   561  					filter.TokensToReturn == TokensToReturnUsed && ad.balance != nil ||
   562  					filter.TokensToReturn == TokensToReturnNonzeroBalance && ad.balance != nil && !IsZeroBigInt(&ad.balance.BalanceSat) {
   563  					tokens = append(tokens, token)
   564  				}
   565  				xpubAddresses[token.Name] = struct{}{}
   566  			}
   567  		}
   568  	}
   569  	setIsOwnAddresses(txs, xpubAddresses)
   570  	var totalReceived big.Int
   571  	totalReceived.Add(&data.balanceSat, &data.sentSat)
   572  
   573  	var secondaryValue float64
   574  	if secondaryCoin != "" {
   575  		ticker := w.fiatRates.GetCurrentTicker("", "")
   576  		balance, err := strconv.ParseFloat((*Amount)(&data.balanceSat).DecimalString(w.chainParser.AmountDecimals()), 64)
   577  		if ticker != nil && err == nil {
   578  			r, found := ticker.Rates[secondaryCoin]
   579  			if found {
   580  				secondaryRate := float64(r)
   581  				secondaryValue = secondaryRate * balance
   582  			}
   583  		}
   584  	}
   585  
   586  	addr := Address{
   587  		Paging:                pg,
   588  		AddrStr:               xpub,
   589  		BalanceSat:            (*Amount)(&data.balanceSat),
   590  		TotalReceivedSat:      (*Amount)(&totalReceived),
   591  		TotalSentSat:          (*Amount)(&data.sentSat),
   592  		Txs:                   txCount,
   593  		AddrTxCount:           addrTxCount,
   594  		UnconfirmedBalanceSat: (*Amount)(&uBalSat),
   595  		UnconfirmedTxs:        unconfirmedTxs,
   596  		Transactions:          txs,
   597  		Txids:                 txids,
   598  		UsedTokens:            usedTokens,
   599  		Tokens:                tokens,
   600  		SecondaryValue:        secondaryValue,
   601  		XPubAddresses:         xpubAddresses,
   602  		AddressAliases:        w.getAddressAliases(addresses),
   603  	}
   604  	glog.Info("GetXpubAddress ", xpub[:xpubLogPrefix], ", cache ", inCache, ", ", txCount, " txs, ", time.Since(start))
   605  	return &addr, nil
   606  }
   607  
   608  // GetXpubUtxo returns unspent outputs for given xpub
   609  func (w *Worker) GetXpubUtxo(xpub string, onlyConfirmed bool, gap int) (Utxos, error) {
   610  	start := time.Now()
   611  	xd, err := w.chainParser.ParseXpub(xpub)
   612  	if err != nil {
   613  		return nil, err
   614  	}
   615  	data, _, inCache, err := w.getXpubData(xd, 0, 1, AccountDetailsBasic, &AddressFilter{
   616  		Vout:          AddressFilterVoutOff,
   617  		OnlyConfirmed: onlyConfirmed,
   618  	}, gap)
   619  	if err != nil {
   620  		return nil, err
   621  	}
   622  	r := make(Utxos, 0, 8)
   623  	for ci, da := range data.addresses {
   624  		for i := range da {
   625  			ad := &da[i]
   626  			onlyMempool := false
   627  			if ad.balance == nil {
   628  				if onlyConfirmed {
   629  					continue
   630  				}
   631  				onlyMempool = true
   632  			}
   633  			utxos, err := w.getAddrDescUtxo(ad.addrDesc, ad.balance, onlyConfirmed, onlyMempool)
   634  			if err != nil {
   635  				return nil, err
   636  			}
   637  			if len(utxos) > 0 {
   638  				t := w.tokenFromXpubAddress(data, ad, ci, i, AccountDetailsTokens)
   639  				for j := range utxos {
   640  					a := &utxos[j]
   641  					a.Address = t.Name
   642  					a.Path = t.Path
   643  				}
   644  				r = append(r, utxos...)
   645  			}
   646  		}
   647  	}
   648  	sort.Stable(r)
   649  	glog.Info("GetXpubUtxo ", xpub[:xpubLogPrefix], ", cache ", inCache, ", ", len(r), " utxos,  ", time.Since(start))
   650  	return r, nil
   651  }
   652  
   653  // GetXpubBalanceHistory returns history of balance for given xpub
   654  func (w *Worker) GetXpubBalanceHistory(xpub string, fromTimestamp, toTimestamp int64, currencies []string, gap int, groupBy uint32) (BalanceHistories, error) {
   655  	bhs := make(BalanceHistories, 0)
   656  	start := time.Now()
   657  	fromUnix, fromHeight, toUnix, toHeight := w.balanceHistoryHeightsFromTo(fromTimestamp, toTimestamp)
   658  	if fromHeight >= toHeight {
   659  		return bhs, nil
   660  	}
   661  	xd, err := w.chainParser.ParseXpub(xpub)
   662  	if err != nil {
   663  		return nil, err
   664  	}
   665  	data, _, inCache, err := w.getXpubData(xd, 0, 1, AccountDetailsTxidHistory, &AddressFilter{
   666  		Vout:          AddressFilterVoutOff,
   667  		OnlyConfirmed: true,
   668  		FromHeight:    fromHeight,
   669  		ToHeight:      toHeight,
   670  	}, gap)
   671  	if err != nil {
   672  		return nil, err
   673  	}
   674  	selfAddrDesc := make(map[string]struct{})
   675  	for _, da := range data.addresses {
   676  		for i := range da {
   677  			selfAddrDesc[string(da[i].addrDesc)] = struct{}{}
   678  		}
   679  	}
   680  	for _, da := range data.addresses {
   681  		for i := range da {
   682  			ad := &da[i]
   683  			txids := ad.txids
   684  			for txi := len(txids) - 1; txi >= 0; txi-- {
   685  				bh, err := w.balanceHistoryForTxid(ad.addrDesc, txids[txi].txid, fromUnix, toUnix, selfAddrDesc)
   686  				if err != nil {
   687  					return nil, err
   688  				}
   689  				if bh != nil {
   690  					bhs = append(bhs, *bh)
   691  				}
   692  			}
   693  		}
   694  	}
   695  	bha := bhs.SortAndAggregate(groupBy)
   696  	err = w.setFiatRateToBalanceHistories(bha, currencies)
   697  	if err != nil {
   698  		return nil, err
   699  	}
   700  	glog.Info("GetUtxoBalanceHistory ", xpub[:xpubLogPrefix], ", cache ", inCache, ", blocks ", fromHeight, "-", toHeight, ", count ", len(bha), ",  ", time.Since(start))
   701  	return bha, nil
   702  }