Control token detail level returned by xpub api

This commit is contained in:
Martin Boehm 2019-02-12 15:15:10 +01:00
parent 63fb910ecb
commit 4846af9f60
7 changed files with 105 additions and 73 deletions

View File

@ -19,13 +19,13 @@ type GetAddressOption int
const ( const (
// Basic - only that address is indexed and some basic info // Basic - only that address is indexed and some basic info
Basic GetAddressOption = iota Basic GetAddressOption = iota
// Balance - only balances // Tokens - basic info + tokens
Balance Tokens
// TxidHistory - balances and txids, subject to paging // TxidHistory - basic + tokens + txids, subject to paging
TxidHistory TxidHistory
// TxHistoryLight - balances and easily obtained tx data (not requiring request to backend), subject to paging // TxHistoryLight - basic + tokens + easily obtained tx data (not requiring request to backend), subject to paging
TxHistoryLight TxHistoryLight
// TxHistory - balances and full tx data, subject to paging // TxHistory - basic + tokens + full tx data, subject to paging
TxHistory TxHistory
) )
@ -138,14 +138,17 @@ const XPUBAddressTokenType TokenType = "XPUBAddress"
// Token contains info about tokens held by an address // Token contains info about tokens held by an address
type Token struct { type Token struct {
Type TokenType `json:"type"` Type TokenType `json:"type"`
Contract string `json:"contract"` Name string `json:"name"`
Transfers int `json:"transfers"` Path string `json:"path,omitempty"`
Name string `json:"name"` Contract string `json:"contract,omitempty"`
Symbol string `json:"symbol"` Transfers int `json:"transfers"`
Decimals int `json:"decimals"` Symbol string `json:"symbol,omitempty"`
BalanceSat *Amount `json:"balance,omitempty"` Decimals int `json:"decimals,omitempty"`
ContractIndex string `json:"-"` BalanceSat *Amount `json:"balance,omitempty"`
TotalReceivedSat *Amount `json:"totalReceived,omitempty"`
TotalSentSat *Amount `json:"totalSent,omitempty"`
ContractIndex string `json:"-"`
} }
// TokenTransfer contains info about a token transfer done in a transaction // TokenTransfer contains info about a token transfer done in a transaction
@ -198,6 +201,9 @@ type Paging struct {
ItemsOnPage int `json:"itemsOnPage,omitempty"` ItemsOnPage int `json:"itemsOnPage,omitempty"`
} }
// TokenDetailLevel specifies detail level of tokens returned by GetAddress and GetXpubAddress
type TokenDetailLevel int
const ( const (
// AddressFilterVoutOff disables filtering of transactions by vout // AddressFilterVoutOff disables filtering of transactions by vout
AddressFilterVoutOff = -1 AddressFilterVoutOff = -1
@ -205,6 +211,13 @@ const (
AddressFilterVoutInputs = -2 AddressFilterVoutInputs = -2
// AddressFilterVoutOutputs specifies that only txs where the address is as output are returned // AddressFilterVoutOutputs specifies that only txs where the address is as output are returned
AddressFilterVoutOutputs = -3 AddressFilterVoutOutputs = -3
// TokenDetailNonzeroBalance - use to return only tokens with nonzero balance
TokenDetailNonzeroBalance TokenDetailLevel = 0
// TokenDetailUsed - use to return tokens with some transfers (even if they have zero balance now)
TokenDetailUsed TokenDetailLevel = 1
// TokenDetailDiscovered - use to return all discovered tokens
TokenDetailDiscovered TokenDetailLevel = 2
) )
// AddressFilter is used to filter data returned from GetAddress api method // AddressFilter is used to filter data returned from GetAddress api method
@ -213,8 +226,7 @@ type AddressFilter struct {
Contract string Contract string
FromHeight uint32 FromHeight uint32
ToHeight uint32 ToHeight uint32
// AllTokens set to true will include xpub addresses with zero balance TokenLevel TokenDetailLevel
AllTokens bool
// OnlyConfirmed set to true will ignore mempool transactions; mempool is also ignored if FromHeight/ToHeight filter is specified // OnlyConfirmed set to true will ignore mempool transactions; mempool is also ignored if FromHeight/ToHeight filter is specified
OnlyConfirmed bool OnlyConfirmed bool
} }

View File

@ -51,7 +51,7 @@ func (w *Worker) getAddressesFromVout(vout *bchain.Vout) (bchain.AddressDescript
// setSpendingTxToVout is helper function, that finds transaction that spent given output and sets it to the output // setSpendingTxToVout is helper function, that finds transaction that spent given output and sets it to the output
// there is no direct index for the operation, it must be found using addresses -> txaddresses -> tx // there is no direct index for the operation, it must be found using addresses -> txaddresses -> tx
func (w *Worker) setSpendingTxToVout(vout *Vout, txid string, height uint32) error { func (w *Worker) setSpendingTxToVout(vout *Vout, txid string, height uint32) error {
err := w.db.GetAddrDescTransactions(vout.AddrDesc, height, ^uint32(0), func(t string, height uint32, indexes []int32) error { err := w.db.GetAddrDescTransactions(vout.AddrDesc, height, maxUint32, func(t string, height uint32, indexes []int32) error {
for _, index := range indexes { for _, index := range indexes {
// take only inputs // take only inputs
if index < 0 { if index < 0 {
@ -364,7 +364,7 @@ func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool
} else { } else {
to := filter.ToHeight to := filter.ToHeight
if to == 0 { if to == 0 {
to = ^uint32(0) to = maxUint32
} }
err = w.db.GetAddrDescTransactions(addrDesc, filter.FromHeight, to, callback) err = w.db.GetAddrDescTransactions(addrDesc, filter.FromHeight, to, callback)
if err != nil { if err != nil {
@ -683,8 +683,8 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetA
ba = &db.AddrBalance{} ba = &db.AddrBalance{}
page = 0 page = 0
} }
// process mempool, only if blockheight filter is off // process mempool, only if toHeight is not specified
if filter.FromHeight == 0 && filter.ToHeight == 0 && !filter.OnlyConfirmed { if filter.ToHeight == 0 && !filter.OnlyConfirmed {
txm, err = w.getAddressTxids(addrDesc, true, filter, maxInt) txm, err = w.getAddressTxids(addrDesc, true, filter, maxInt)
if err != nil { if err != nil {
return nil, errors.Annotatef(err, "getAddressTxids %v true", addrDesc) return nil, errors.Annotatef(err, "getAddressTxids %v true", addrDesc)
@ -825,7 +825,7 @@ func (w *Worker) getAddrDescUtxo(addrDesc bchain.AddressDescriptor, ba *db.AddrB
// ba can be nil if the address is only in mempool! // ba can be nil if the address is only in mempool!
if ba != nil && !IsZeroBigInt(&ba.BalanceSat) { if ba != nil && !IsZeroBigInt(&ba.BalanceSat) {
outpoints := make([]bchain.Outpoint, 0, 8) outpoints := make([]bchain.Outpoint, 0, 8)
err = w.db.GetAddrDescTransactions(addrDesc, 0, ^uint32(0), func(txid string, height uint32, indexes []int32) error { err = w.db.GetAddrDescTransactions(addrDesc, 0, maxUint32, func(txid string, height uint32, indexes []int32) error {
for _, index := range indexes { for _, index := range indexes {
// take only outputs // take only outputs
if index >= 0 { if index >= 0 {
@ -943,7 +943,7 @@ func (w *Worker) GetBlock(bid string, page int, txsOnPage int) (*Block, error) {
// if it's a number, must be less than int32 // if it's a number, must be less than int32
var hash string var hash string
height, err := strconv.Atoi(bid) height, err := strconv.Atoi(bid)
if err == nil && height < int(^uint32(0)) { if err == nil && height < int(maxUint32) {
hash, err = w.db.GetBlockHash(uint32(height)) hash, err = w.db.GetBlockHash(uint32(height))
if err != nil { if err != nil {
hash = bid hash = bid

View File

@ -218,19 +218,23 @@ func (w *Worker) tokenFromXpubAddress(data *xpubData, ad *xpubAddress, changeInd
if len(a) > 0 { if len(a) > 0 {
address = a[0] address = a[0]
} }
var balance *big.Int var balance, totalReceived, totalSent *big.Int
var transfers int var transfers int
if ad.balance != nil { if ad.balance != nil {
balance = &ad.balance.BalanceSat balance = &ad.balance.BalanceSat
totalSent = &ad.balance.SentSat
totalReceived = ad.balance.ReceivedSat()
transfers = int(ad.balance.Txs) transfers = int(ad.balance.Txs)
} }
return Token{ return Token{
Type: XPUBAddressTokenType, Type: XPUBAddressTokenType,
Name: address, Name: address,
Decimals: w.chainParser.AmountDecimals(), Decimals: w.chainParser.AmountDecimals(),
BalanceSat: (*Amount)(balance), BalanceSat: (*Amount)(balance),
Transfers: transfers, TotalReceivedSat: (*Amount)(totalReceived),
Contract: fmt.Sprintf("%s/%d/%d", data.basePath, changeIndex, index), TotalSentSat: (*Amount)(totalSent),
Transfers: transfers,
Path: fmt.Sprintf("%s/%d/%d", data.basePath, changeIndex, index),
} }
} }
@ -371,8 +375,8 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Get
} }
totalResults = -1 totalResults = -1
} }
// process mempool, only if blockheight filter is off // process mempool, only if ToHeight is not specified
if filter.FromHeight == 0 && filter.ToHeight == 0 && !filter.OnlyConfirmed { if filter.ToHeight == 0 && !filter.OnlyConfirmed {
txmMap = make(map[string]*Tx) txmMap = make(map[string]*Tx)
for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} {
for i := range da { for i := range da {
@ -442,19 +446,27 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Get
} }
} }
totalTokens := 0 totalTokens := 0
xpubAddresses := make(map[string]struct{}) var tokens []Token
tokens := make([]Token, 0, 4) var xpubAddresses map[string]struct{}
if option != Basic {
tokens = make([]Token, 0, 4)
xpubAddresses = make(map[string]struct{})
}
for ci, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { for ci, da := range [][]xpubAddress{data.addresses, data.changeAddresses} {
for i := range da { for i := range da {
ad := &da[i] ad := &da[i]
token := w.tokenFromXpubAddress(data, ad, ci, i)
if ad.balance != nil { if ad.balance != nil {
totalTokens++ totalTokens++
if filter.AllTokens || !IsZeroBigInt(&ad.balance.BalanceSat) { }
if option != Basic {
token := w.tokenFromXpubAddress(data, ad, ci, i)
if filter.TokenLevel == TokenDetailDiscovered ||
filter.TokenLevel == TokenDetailUsed && ad.balance != nil ||
filter.TokenLevel == TokenDetailNonzeroBalance && ad.balance != nil && !IsZeroBigInt(&ad.balance.BalanceSat) {
tokens = append(tokens, token) tokens = append(tokens, token)
} }
xpubAddresses[token.Name] = struct{}{}
} }
xpubAddresses[token.Name] = struct{}{}
} }
} }
var totalReceived big.Int var totalReceived big.Int
@ -483,7 +495,6 @@ func (w *Worker) GetXpubUtxo(xpub string, onlyConfirmed bool, gap int) (Utxos, e
start := time.Now() start := time.Now()
data, _, err := w.getXpubData(xpub, 0, 1, Basic, &AddressFilter{ data, _, err := w.getXpubData(xpub, 0, 1, Basic, &AddressFilter{
Vout: AddressFilterVoutOff, Vout: AddressFilterVoutOff,
AllTokens: false,
OnlyConfirmed: onlyConfirmed, OnlyConfirmed: onlyConfirmed,
}, gap) }, gap)
if err != nil { if err != nil {
@ -509,7 +520,7 @@ func (w *Worker) GetXpubUtxo(xpub string, onlyConfirmed bool, gap int) (Utxos, e
for j := range utxos { for j := range utxos {
a := &utxos[j] a := &utxos[j]
a.Address = t.Name a.Address = t.Name
a.Path = t.Contract a.Path = t.Path
} }
r = append(r, utxos...) r = append(r, utxos...)
} }

View File

@ -385,27 +385,27 @@ const (
// TemplateData is used to transfer data to the templates // TemplateData is used to transfer data to the templates
type TemplateData struct { type TemplateData struct {
CoinName string CoinName string
CoinShortcut string CoinShortcut string
CoinLabel string CoinLabel string
InternalExplorer bool InternalExplorer bool
ChainType bchain.ChainType ChainType bchain.ChainType
Address *api.Address Address *api.Address
AddrStr string AddrStr string
Tx *api.Tx Tx *api.Tx
Error *api.APIError Error *api.APIError
Blocks *api.Blocks Blocks *api.Blocks
Block *api.Block Block *api.Block
Info *api.SystemInfo Info *api.SystemInfo
Page int Page int
PrevPage int PrevPage int
NextPage int NextPage int
PagingRange []int PagingRange []int
PageParams template.URL PageParams template.URL
TOSLink string TOSLink string
SendTxHex string SendTxHex string
Status string Status string
AllTokens bool NonZeroBalanceTokens bool
} }
func (s *PublicServer) parseTemplates() []*template.Template { func (s *PublicServer) parseTemplates() []*template.Template {
@ -600,7 +600,7 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (
return addressTpl, data, nil return addressTpl, data, nil
} }
func (s *PublicServer) getXpubAddress(r *http.Request, xpub string, pageSize int, option api.GetAddressOption) (*api.Address, error) { func (s *PublicServer) getXpubAddress(r *http.Request, xpub string, pageSize int, option api.GetAddressOption) (*api.Address, api.TokenDetailLevel, error) {
var fn = api.AddressFilterVoutOff var fn = api.AddressFilterVoutOff
page, ec := strconv.Atoi(r.URL.Query().Get("page")) page, ec := strconv.Atoi(r.URL.Query().Get("page"))
if ec != nil { if ec != nil {
@ -624,16 +624,26 @@ func (s *PublicServer) getXpubAddress(r *http.Request, xpub string, pageSize int
if ec != nil { if ec != nil {
gap = 0 gap = 0
} }
allAddresses, _ := strconv.ParseBool(r.URL.Query().Get("alladdresses")) tokenLevel := api.TokenDetailNonzeroBalance
return s.api.GetXpubAddress(xpub, page, pageSize, option, &api.AddressFilter{Vout: fn, AllTokens: allAddresses}, gap) switch r.URL.Query().Get("tokenlevel") {
case "discovered":
tokenLevel = api.TokenDetailDiscovered
case "used":
tokenLevel = api.TokenDetailUsed
case "nonzero":
tokenLevel = api.TokenDetailNonzeroBalance
}
a, err := s.api.GetXpubAddress(xpub, page, pageSize, option, &api.AddressFilter{Vout: fn, TokenLevel: tokenLevel}, gap)
return a, tokenLevel, err
} }
func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
var address *api.Address var address *api.Address
var tokenLevel api.TokenDetailLevel
var err error var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc() s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc()
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
address, err = s.getXpubAddress(r, r.URL.Path[i+1:], txsOnPage, api.TxHistoryLight) address, tokenLevel, err = s.getXpubAddress(r, r.URL.Path[i+1:], txsOnPage, api.TxHistoryLight)
if err != nil { if err != nil {
return errorTpl, nil, err return errorTpl, nil, err
} }
@ -648,8 +658,7 @@ func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl
data.PageParams = template.URL("&filter=" + filter) data.PageParams = template.URL("&filter=" + filter)
data.Address.Filter = filter data.Address.Filter = filter
} }
allAddresses := r.URL.Query().Get("alladdresses") data.NonZeroBalanceTokens = tokenLevel == api.TokenDetailNonzeroBalance
data.AllTokens, _ = strconv.ParseBool(allAddresses)
return xpubTpl, data, nil return xpubTpl, data, nil
} }
@ -904,7 +913,7 @@ func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, er
var err error var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc() s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc()
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
address, err = s.getXpubAddress(r, r.URL.Path[i+1:], txsInAPI, api.TxidHistory) address, _, err = s.getXpubAddress(r, r.URL.Path[i+1:], txsInAPI, api.TxidHistory)
if err == nil && apiVersion == apiV1 { if err == nil && apiVersion == apiV1 {
return s.api.AddressToV1(address), nil return s.api.AddressToV1(address), nil
} }

View File

@ -349,8 +349,8 @@ func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) {
func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address, err error) { func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address, err error) {
var opt api.GetAddressOption var opt api.GetAddressOption
switch req.Details { switch req.Details {
case "balance": case "tokens":
opt = api.Balance opt = api.Tokens
case "txids": case "txids":
opt = api.TxidHistory opt = api.TxidHistory
case "txs": case "txs":
@ -363,7 +363,7 @@ func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address,
ToHeight: uint32(req.ToHeight), ToHeight: uint32(req.ToHeight),
Contract: req.ContractFilter, Contract: req.ContractFilter,
Vout: api.AddressFilterVoutOff, Vout: api.AddressFilterVoutOff,
AllTokens: true, TokenLevel: api.TokenDetailDiscovered,
} }
a, err := s.api.GetXpubAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, 0) a, err := s.api.GetXpubAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, 0)
if err != nil { if err != nil {

View File

@ -31,7 +31,7 @@
</tr> </tr>
{{- if $addr.TotalTokens -}} {{- if $addr.TotalTokens -}}
<tr> <tr>
<td>{{if $data.AllTokens}}XPUB Addresses{{else}}XPUB Addresses with Balance{{end}}</td> <td>{{if $data.NonZeroBalanceTokens}}XPUB Addresses with Balance{{else}}XPUB Addresses{{end}}</td>
<td style="padding: 0;"> <td style="padding: 0;">
<table class="table data-table"> <table class="table data-table">
<tbody> <tbody>
@ -46,12 +46,12 @@
<td class="data ellipsis"><a href="/address/{{$t.Name}}">{{$t.Name}}</a></td> <td class="data ellipsis"><a href="/address/{{$t.Name}}">{{$t.Name}}</a></td>
<td class="data">{{formatAmount $t.BalanceSat}} {{$cs}}</td> <td class="data">{{formatAmount $t.BalanceSat}} {{$cs}}</td>
<td class="data">{{$t.Transfers}}</td> <td class="data">{{$t.Transfers}}</td>
<td>{{$t.Contract}}</td> <td>{{$t.Path}}</td>
</tr> </tr>
{{- end -}} {{- end -}}
{{- if not $data.AllTokens -}} {{- if $data.NonZeroBalanceTokens -}}
<tr> <tr>
<td colspan="4"><a href="?alladdresses=true">Show all XPUB addresses</a></td> <td colspan="4"><a href="?tokenlevel=used">Show all XPUB addresses</a></td>
</tr> </tr>
{{- end -}} {{- end -}}
</tbody> </tbody>

View File

@ -304,7 +304,7 @@
<input type="text" placeholder="descriptor" style="width: 79%" class="form-control" id="getAccountInfoDescriptor" value="0xba98d6a5ac827632e3457de7512d211e4ff7e8bd"> <input type="text" placeholder="descriptor" style="width: 79%" class="form-control" id="getAccountInfoDescriptor" value="0xba98d6a5ac827632e3457de7512d211e4ff7e8bd">
<select id="getAccountInfoDetails" style="width: 20%; margin-left: 5px;"> <select id="getAccountInfoDetails" style="width: 20%; margin-left: 5px;">
<option value="basic">Basic</option> <option value="basic">Basic</option>
<option value="balance">Balance</option> <option value="tokens">Tokens</option>
<option value="txids">Txids</option> <option value="txids">Txids</option>
<option value="txs">Transactions</option> <option value="txs">Transactions</option>
</select> </select>