Get utxo for xpub

xpub
Martin Boehm 2019-02-08 15:50:37 +01:00
parent 273436f109
commit 6b0a4960fd
5 changed files with 268 additions and 174 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}