diff --git a/api/types.go b/api/types.go index 86a48a03..2efd46ae 100644 --- a/api/types.go +++ b/api/types.go @@ -213,7 +213,10 @@ type AddressFilter struct { Contract string FromHeight uint32 ToHeight uint32 - AllTokens bool + // AllTokens set to true will include xpub addresses with zero balance + AllTokens bool + // OnlyConfirmed set to true will ignore mempool transactions; mempool is also ignored if FromHeight/ToHeight filter is specified + OnlyConfirmed bool } // Address holds information about address and its transactions @@ -238,13 +241,33 @@ type Address struct { XPubAddresses map[string]struct{} `json:"-"` } -// AddressUtxo holds information about address and its transactions -type AddressUtxo struct { +// Utxo is one unspent transaction output +type Utxo struct { Txid string `json:"txid"` Vout int32 `json:"vout"` AmountSat *Amount `json:"value"` Height int `json:"height,omitempty"` Confirmations int `json:"confirmations"` + Address string `json:"address,omitempty"` + Path string `json:"path,omitempty"` +} + +// Utxos is array of Utxo +type Utxos []Utxo + +func (a Utxos) Len() int { return len(a) } +func (a Utxos) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a Utxos) Less(i, j int) bool { + // sort in reverse order, unconfirmed (height==0) utxos on top + hi := a[i].Height + hj := a[j].Height + if hi == 0 { + hi = maxInt + } + if hj == 0 { + hj = maxInt + } + return hi >= hj } // Blocks is list of blocks with paging information diff --git a/api/typesv1.go b/api/typesv1.go index 52703d49..bcdad73f 100644 --- a/api/typesv1.go +++ b/api/typesv1.go @@ -192,7 +192,7 @@ func (w *Worker) AddressToV1(a *Address) *AddressV1 { } // AddressUtxoToV1 converts []AddressUtxo to []AddressUtxoV1 -func (w *Worker) AddressUtxoToV1(au []AddressUtxo) []AddressUtxoV1 { +func (w *Worker) AddressUtxoToV1(au Utxos) []AddressUtxoV1 { d := w.chainParser.AmountDecimals() v1 := make([]AddressUtxoV1, len(au)) for i := range au { diff --git a/api/worker.go b/api/worker.go index 7b088560..5e084f7e 100644 --- a/api/worker.go +++ b/api/worker.go @@ -684,7 +684,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetA page = 0 } // process mempool, only if blockheight filter is off - if filter.FromHeight == 0 && filter.ToHeight == 0 { + if filter.FromHeight == 0 && filter.ToHeight == 0 && !filter.OnlyConfirmed { txm, err = w.getAddressTxids(addrDesc, true, filter, maxInt) if err != nil { return nil, errors.Annotatef(err, "getAddressTxids %v true", addrDesc) @@ -766,126 +766,138 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetA return r, nil } +func (w *Worker) getAddrDescUtxo(addrDesc bchain.AddressDescriptor, ba *db.AddrBalance, onlyConfirmed bool, onlyMempool bool) (Utxos, error) { + var err error + r := make(Utxos, 0, 8) + spentInMempool := make(map[string]struct{}) + if !onlyConfirmed { + // get utxo from mempool + txm, err := w.getAddressTxids(addrDesc, true, &AddressFilter{Vout: AddressFilterVoutOff}, maxInt) + if err != nil { + return nil, err + } + 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{}{} + } + } + } + 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 { + r = append(r, Utxo{ + Txid: bchainTx.Txid, + Vout: int32(i), + AmountSat: (*Amount)(&vout.ValueSat), + }) + } + } + } + } + } + } + } + if !onlyMempool { + // get utxo from index + if ba == nil { + ba, err = w.db.GetAddrDescBalance(addrDesc) + if err != nil { + return nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true) + } + } + // ba can be nil if the address is only in mempool! + if ba != nil && !IsZeroBigInt(&ba.BalanceSat) { + outpoints := make([]bchain.Outpoint, 0, 8) + err = w.db.GetAddrDescTransactions(addrDesc, 0, ^uint32(0), func(txid string, height uint32, indexes []int32) error { + for _, index := range indexes { + // take only outputs + if index >= 0 { + outpoints = append(outpoints, bchain.Outpoint{Txid: txid, Vout: index}) + } + } + return nil + }) + if err != nil { + return nil, err + } + var lastTxid string + var ta *db.TxAddresses + var checksum big.Int + checksum.Set(&ba.BalanceSat) + b, _, err := w.db.GetBestBlock() + if err != nil { + return nil, err + } + bestheight := int(b) + for i := len(outpoints) - 1; i >= 0 && checksum.Int64() > 0; i-- { + o := outpoints[i] + if lastTxid != o.Txid { + ta, err = w.db.GetTxAddresses(o.Txid) + if err != nil { + return nil, err + } + lastTxid = o.Txid + } + if ta == nil { + glog.Warning("DB inconsistency: tx ", o.Txid, ": not found in txAddresses") + } else { + if len(ta.Outputs) <= int(o.Vout) { + glog.Warning("DB inconsistency: txAddresses ", o.Txid, " does not have enough outputs") + } else { + if !ta.Outputs[o.Vout].Spent { + v := ta.Outputs[o.Vout].ValueSat + // report only outpoints that are not spent in mempool + _, e := spentInMempool[o.Txid+strconv.Itoa(int(o.Vout))] + if !e { + r = append(r, Utxo{ + Txid: o.Txid, + Vout: o.Vout, + AmountSat: (*Amount)(&v), + Height: int(ta.Height), + Confirmations: bestheight - int(ta.Height) + 1, + }) + } + checksum.Sub(&checksum, &v) + } + } + } + } + if checksum.Uint64() != 0 { + glog.Warning("DB inconsistency: ", addrDesc, ": checksum is not zero") + } + } + } + return r, nil +} + // GetAddressUtxo returns unspent outputs for given address -func (w *Worker) GetAddressUtxo(address string, onlyConfirmed bool) ([]AddressUtxo, error) { +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", err), true) - } - spentInMempool := make(map[string]struct{}) - r := make([]AddressUtxo, 0, 8) - if !onlyConfirmed { - // get utxo from mempool - txm, err := w.getAddressTxids(addrDesc, true, &AddressFilter{Vout: AddressFilterVoutOff}, maxInt) - if err != nil { - return nil, errors.Annotatef(err, "getAddressTxids %v true", address) - } - 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{}{} - } - } - } - 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 { - r = append(r, AddressUtxo{ - Txid: bchainTx.Txid, - Vout: int32(i), - AmountSat: (*Amount)(&vout.ValueSat), - }) - } - } - } - } - } - } - // get utxo from index - ba, err := w.db.GetAddrDescBalance(addrDesc) - if err != nil { - return nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true) - } - var checksum big.Int - // ba can be nil if the address is only in mempool! - if ba != nil && !IsZeroBigInt(&ba.BalanceSat) { - outpoints := make([]bchain.Outpoint, 0, 8) - err = w.db.GetAddrDescTransactions(addrDesc, 0, ^uint32(0), func(txid string, height uint32, indexes []int32) error { - for _, index := range indexes { - // take only outputs - if index >= 0 { - outpoints = append(outpoints, bchain.Outpoint{Txid: txid, Vout: index}) - } - } - return nil - }) - if err != nil { - return nil, err - } - var lastTxid string - var ta *db.TxAddresses - checksum = ba.BalanceSat - b, _, err := w.db.GetBestBlock() - if err != nil { - return nil, err - } - bestheight := int(b) - for i := len(outpoints) - 1; i >= 0 && checksum.Int64() > 0; i-- { - o := outpoints[i] - if lastTxid != o.Txid { - ta, err = w.db.GetTxAddresses(o.Txid) - if err != nil { - return nil, err - } - lastTxid = o.Txid - } - if ta == nil { - glog.Warning("DB inconsistency: tx ", o.Txid, ": not found in txAddresses") - } else { - if len(ta.Outputs) <= int(o.Vout) { - glog.Warning("DB inconsistency: txAddresses ", o.Txid, " does not have enough outputs") - } else { - if !ta.Outputs[o.Vout].Spent { - v := ta.Outputs[o.Vout].ValueSat - // report only outpoints that are not spent in mempool - _, e := spentInMempool[o.Txid+strconv.Itoa(int(o.Vout))] - if !e { - r = append(r, AddressUtxo{ - Txid: o.Txid, - Vout: o.Vout, - AmountSat: (*Amount)(&v), - Height: int(ta.Height), - Confirmations: bestheight - int(ta.Height) + 1, - }) - } - checksum.Sub(&checksum, &v) - } - } - } - } - } - if checksum.Uint64() != 0 { - glog.Warning("DB inconsistency: ", address, ": checksum is not zero") + return nil, NewAPIError(fmt.Sprintf("Invalid address '%v', %v", address, err), true) } + r, err := w.getAddrDescUtxo(addrDesc, nil, onlyConfirmed, false) glog.Info("GetAddressUtxo ", address, ", ", len(r), " utxos, finished in ", time.Since(start)) return r, nil } diff --git a/api/xpub.go b/api/xpub.go index bc15da54..6fa86932 100644 --- a/api/xpub.go +++ b/api/xpub.go @@ -19,7 +19,7 @@ const defaultAddressesGap = 20 const txInput = 1 const txOutput = 2 -var cachedXpubs = make(map[string]*xpubData) +var cachedXpubs = make(map[string]xpubData) var cachedXpubsMux sync.Mutex type xpubTxid struct { @@ -216,63 +216,52 @@ func (w *Worker) tokenFromXpubAddress(data *xpubData, ad *xpubAddress, changeInd if len(a) > 0 { address = a[0] } + var balance *big.Int + var transfers int + if ad.balance != nil { + balance = &ad.balance.BalanceSat + transfers = int(ad.balance.Txs) + } return Token{ Type: XPUBAddressTokenType, Name: address, Decimals: w.chainParser.AmountDecimals(), - BalanceSat: (*Amount)(&ad.balance.BalanceSat), - Transfers: int(ad.balance.Txs), + BalanceSat: (*Amount)(balance), + Transfers: transfers, Contract: fmt.Sprintf("%s/%d/%d", data.basePath, changeIndex, index), } } -// GetAddressForXpub computes address value and gets transactions for given address -func (w *Worker) GetAddressForXpub(xpub string, page int, txsOnPage int, option GetAddressOption, filter *AddressFilter, gap int) (*Address, error) { +func (w *Worker) getXpubData(xpub string, page int, txsOnPage int, option GetAddressOption, filter *AddressFilter, gap int) (*xpubData, uint32, error) { if w.chainType != bchain.ChainBitcoinType || len(xpub) != xpubLen { - return nil, ErrUnsupportedXpub + return nil, 0, ErrUnsupportedXpub } - start := time.Now() + var ( + err error + bestheight uint32 + besthash string + ) if gap <= 0 { gap = defaultAddressesGap } // gap is increased one as there must be gap of empty addresses before the derivation is stopped gap++ - page-- - if page < 0 { - page = 0 - } var processedHash string cachedXpubsMux.Lock() data, found := cachedXpubs[xpub] cachedXpubsMux.Unlock() - type mempoolMap struct { - tx *Tx - inputOutput byte - } - var ( - txc xpubTxids - txmMap map[string]*Tx - txs []*Tx - txids []string - pg Paging - totalResults int - err error - bestheight uint32 - besthash string - uBalSat big.Int - ) // to load all data for xpub may take some time, do it in a loop to process a possible new block for { bestheight, besthash, err = w.db.GetBestBlock() if err != nil { - return nil, errors.Annotatef(err, "GetBestBlock") + return nil, 0, errors.Annotatef(err, "GetBestBlock") } if besthash == processedHash { break } fork := false if !found || data.gap != gap { - data = &xpubData{gap: gap} + data = xpubData{gap: gap} data.basePath, err = w.chainParser.DerivationBasePath(xpub) if err != nil { glog.Warning("DerivationBasePath error", err) @@ -281,7 +270,7 @@ func (w *Worker) GetAddressForXpub(xpub string, page int, txsOnPage int, option } else { hash, err := w.db.GetBlockHash(data.dataHeight) if err != nil { - return nil, err + return nil, 0, err } if hash != data.dataHash { // in case of for reset all cached data @@ -296,21 +285,20 @@ func (w *Worker) GetAddressForXpub(xpub string, page int, txsOnPage int, option data.sentSat = *new(big.Int) data.txs = 0 var lastUsedIndex int - lastUsedIndex, data.addresses, err = w.xpubScanAddresses(xpub, data, data.addresses, gap, 0, 0, fork) + lastUsedIndex, data.addresses, err = w.xpubScanAddresses(xpub, &data, data.addresses, gap, 0, 0, fork) if err != nil { - return nil, err + return nil, 0, err } - _, data.changeAddresses, err = w.xpubScanAddresses(xpub, data, data.changeAddresses, gap, 1, lastUsedIndex, fork) + _, data.changeAddresses, err = w.xpubScanAddresses(xpub, &data, data.changeAddresses, gap, 1, lastUsedIndex, fork) if err != nil { - return nil, err + return nil, 0, err } - glog.Info("Scanned ", len(data.addresses)+len(data.changeAddresses), " addresses in ", time.Since(start)) } if option >= TxidHistory { for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { for i := range da { if err = w.xpubCheckAndLoadTxids(&da[i], filter, bestheight, (page+1)*txsOnPage); err != nil { - return nil, err + return nil, 0, err } } } @@ -319,6 +307,34 @@ func (w *Worker) GetAddressForXpub(xpub string, page int, txsOnPage int, option cachedXpubsMux.Lock() cachedXpubs[xpub] = data cachedXpubsMux.Unlock() + return &data, bestheight, nil +} + +// GetXpubAddress computes address value and gets transactions for given address +func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option GetAddressOption, filter *AddressFilter, gap int) (*Address, error) { + start := time.Now() + page-- + if page < 0 { + page = 0 + } + type mempoolMap struct { + tx *Tx + inputOutput byte + } + var ( + txc xpubTxids + txmMap map[string]*Tx + txs []*Tx + txids []string + pg Paging + totalResults int + err error + uBalSat big.Int + ) + data, bestheight, err := w.getXpubData(xpub, page, txsOnPage, option, filter, gap) + if err != nil { + return nil, err + } // setup filtering of txids var useTxids func(txid *xpubTxid, ad *xpubAddress) bool var addTxids func(ad *xpubAddress) @@ -354,7 +370,7 @@ func (w *Worker) GetAddressForXpub(xpub string, page int, txsOnPage int, option totalResults = -1 } // process mempool, only if blockheight filter is off - if filter.FromHeight == 0 && filter.ToHeight == 0 { + if filter.FromHeight == 0 && filter.ToHeight == 0 && !filter.OnlyConfirmed { txmMap = make(map[string]*Tx) for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { for i := range da { @@ -429,22 +445,14 @@ func (w *Worker) GetAddressForXpub(xpub string, page int, txsOnPage int, option for ci, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { for i := range da { ad := &da[i] - var t *Token + token := w.tokenFromXpubAddress(data, ad, ci, i) if ad.balance != nil { totalTokens++ if filter.AllTokens || !IsZeroBigInt(&ad.balance.BalanceSat) { - token := w.tokenFromXpubAddress(data, ad, ci, i) tokens = append(tokens, token) - t = &token - xpubAddresses[t.Name] = struct{}{} - } - } - if t == nil { - a, _, _ := w.chainParser.GetAddressesFromAddrDesc(ad.addrDesc) - if len(a) > 0 { - xpubAddresses[a[0]] = struct{}{} } } + xpubAddresses[token.Name] = struct{}{} } } var totalReceived big.Int @@ -464,6 +472,48 @@ func (w *Worker) GetAddressForXpub(xpub string, page int, txsOnPage int, option Tokens: tokens, XPubAddresses: xpubAddresses, } - glog.Info("GetAddressForXpub ", xpub[:16], ", ", len(data.addresses)+len(data.changeAddresses), " derived addresses, ", data.txs, " total txs, loaded ", len(txc), " txids, finished in ", time.Since(start)) + glog.Info("GetXpubAddress ", xpub[:16], ", ", len(data.addresses)+len(data.changeAddresses), " derived addresses, ", data.txs, " total txs, loaded ", len(txc), " txids, finished in ", time.Since(start)) return &addr, nil } + +// GetXpubUtxo returns unspent outputs for given xpub +func (w *Worker) GetXpubUtxo(xpub string, onlyConfirmed bool, gap int) (Utxos, error) { + start := time.Now() + data, _, err := w.getXpubData(xpub, 0, 1, Basic, &AddressFilter{ + Vout: AddressFilterVoutOff, + AllTokens: false, + OnlyConfirmed: onlyConfirmed, + }, gap) + if err != nil { + return nil, err + } + r := make(Utxos, 0, 8) + for ci, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + for i := range da { + ad := &da[i] + onlyMempool := false + if ad.balance == nil { + if onlyConfirmed { + continue + } + onlyMempool = true + } + utxos, err := w.getAddrDescUtxo(ad.addrDesc, ad.balance, onlyConfirmed, onlyMempool) + if err != nil { + return nil, err + } + if len(utxos) > 0 { + t := w.tokenFromXpubAddress(data, ad, ci, i) + for j := range utxos { + a := &utxos[j] + a.Address = t.Name + a.Path = t.Contract + } + r = append(r, utxos...) + } + } + } + sort.Stable(r) + glog.Info("GetXpubUtxo ", xpub[:16], ", ", len(r), " utxos, finished in ", time.Since(start)) + return r, nil +} diff --git a/server/public.go b/server/public.go index ecad94d0..e1ffec45 100644 --- a/server/public.go +++ b/server/public.go @@ -157,7 +157,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/v1/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV1)) serveMux.HandleFunc(path+"api/v1/tx/", s.jsonHandler(s.apiTx, apiV1)) serveMux.HandleFunc(path+"api/v1/address/", s.jsonHandler(s.apiAddress, apiV1)) - serveMux.HandleFunc(path+"api/v1/utxo/", s.jsonHandler(s.apiAddressUtxo, apiV1)) + serveMux.HandleFunc(path+"api/v1/utxo/", s.jsonHandler(s.apiUtxo, apiV1)) serveMux.HandleFunc(path+"api/v1/block/", s.jsonHandler(s.apiBlock, apiV1)) serveMux.HandleFunc(path+"api/v1/sendtx/", s.jsonHandler(s.apiSendTx, apiV1)) serveMux.HandleFunc(path+"api/v1/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV1)) @@ -167,7 +167,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx, apiDefault)) serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress, apiDefault)) serveMux.HandleFunc(path+"api/xpub/", s.jsonHandler(s.apiXpub, apiDefault)) - serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiAddressUtxo, apiDefault)) + serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiUtxo, apiDefault)) 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)) @@ -177,7 +177,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/v2/tx/", s.jsonHandler(s.apiTx, apiV2)) serveMux.HandleFunc(path+"api/v2/address/", s.jsonHandler(s.apiAddress, apiV2)) serveMux.HandleFunc(path+"api/v2/xpub/", s.jsonHandler(s.apiXpub, apiV2)) - serveMux.HandleFunc(path+"api/v2/utxo/", s.jsonHandler(s.apiAddressUtxo, apiV2)) + serveMux.HandleFunc(path+"api/v2/utxo/", s.jsonHandler(s.apiUtxo, apiV2)) serveMux.HandleFunc(path+"api/v2/block/", s.jsonHandler(s.apiBlock, apiV2)) serveMux.HandleFunc(path+"api/v2/sendtx/", s.jsonHandler(s.apiSendTx, apiV2)) serveMux.HandleFunc(path+"api/v2/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV2)) @@ -625,7 +625,7 @@ func (s *PublicServer) getAddressForXpub(r *http.Request, xpub string, pageSize gap = 0 } allAddresses, _ := strconv.ParseBool(r.URL.Query().Get("alladdresses")) - return s.api.GetAddressForXpub(xpub, page, pageSize, option, &api.AddressFilter{Vout: fn, AllTokens: allAddresses}, gap) + return s.api.GetXpubAddress(xpub, page, pageSize, option, &api.AddressFilter{Vout: fn, AllTokens: allAddresses}, gap) } func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { @@ -714,7 +714,7 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t var err error s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc() if len(q) > 0 { - address, err = s.api.GetAddressForXpub(q, 0, 1, api.Basic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, 0) + address, err = s.api.GetXpubAddress(q, 0, 1, api.Basic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, 0) if err == nil { http.Redirect(w, r, joinURL("/xpub/", address.AddrStr), 302) return noTpl, nil, nil @@ -912,10 +912,9 @@ func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, er return address, err } -func (s *PublicServer) apiAddressUtxo(r *http.Request, apiVersion int) (interface{}, error) { - var utxo []api.AddressUtxo +func (s *PublicServer) apiUtxo(r *http.Request, apiVersion int) (interface{}, error) { + var utxo []api.Utxo var err error - s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc() if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { onlyConfirmed := false c := r.URL.Query().Get("confirmed") @@ -925,7 +924,17 @@ func (s *PublicServer) apiAddressUtxo(r *http.Request, apiVersion int) (interfac return nil, api.NewAPIError("Parameter 'confirmed' cannot be converted to boolean", true) } } - utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed) + gap, ec := strconv.Atoi(r.URL.Query().Get("gap")) + if ec != nil { + gap = 0 + } + utxo, err = s.api.GetXpubUtxo(r.URL.Path[i+1:], onlyConfirmed, gap) + if err == nil { + s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-utxo"}).Inc() + } else { + utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed) + s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-utxo"}).Inc() + } if err == nil && apiVersion == apiV1 { return s.api.AddressUtxoToV1(utxo), nil }