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 (
// Basic - only that address is indexed and some basic info
Basic GetAddressOption = iota
// Balance - only balances
Balance
// TxidHistory - balances and txids, subject to paging
// Tokens - basic info + tokens
Tokens
// TxidHistory - basic + tokens + txids, subject to paging
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
// TxHistory - balances and full tx data, subject to paging
// TxHistory - basic + tokens + full tx data, subject to paging
TxHistory
)
@ -138,14 +138,17 @@ const XPUBAddressTokenType TokenType = "XPUBAddress"
// Token contains info about tokens held by an address
type Token struct {
Type TokenType `json:"type"`
Contract string `json:"contract"`
Transfers int `json:"transfers"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
BalanceSat *Amount `json:"balance,omitempty"`
ContractIndex string `json:"-"`
Type TokenType `json:"type"`
Name string `json:"name"`
Path string `json:"path,omitempty"`
Contract string `json:"contract,omitempty"`
Transfers int `json:"transfers"`
Symbol string `json:"symbol,omitempty"`
Decimals int `json:"decimals,omitempty"`
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
@ -198,6 +201,9 @@ type Paging struct {
ItemsOnPage int `json:"itemsOnPage,omitempty"`
}
// TokenDetailLevel specifies detail level of tokens returned by GetAddress and GetXpubAddress
type TokenDetailLevel int
const (
// AddressFilterVoutOff disables filtering of transactions by vout
AddressFilterVoutOff = -1
@ -205,6 +211,13 @@ const (
AddressFilterVoutInputs = -2
// AddressFilterVoutOutputs specifies that only txs where the address is as output are returned
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
@ -213,8 +226,7 @@ type AddressFilter struct {
Contract string
FromHeight uint32
ToHeight uint32
// AllTokens set to true will include xpub addresses with zero balance
AllTokens bool
TokenLevel TokenDetailLevel
// OnlyConfirmed set to true will ignore mempool transactions; mempool is also ignored if FromHeight/ToHeight filter is specified
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
// 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 {
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 {
// take only inputs
if index < 0 {
@ -364,7 +364,7 @@ func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool
} else {
to := filter.ToHeight
if to == 0 {
to = ^uint32(0)
to = maxUint32
}
err = w.db.GetAddrDescTransactions(addrDesc, filter.FromHeight, to, callback)
if err != nil {
@ -683,8 +683,8 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetA
ba = &db.AddrBalance{}
page = 0
}
// process mempool, only if blockheight filter is off
if filter.FromHeight == 0 && filter.ToHeight == 0 && !filter.OnlyConfirmed {
// process mempool, only if toHeight is not specified
if 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)
@ -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!
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 {
err = w.db.GetAddrDescTransactions(addrDesc, 0, maxUint32, func(txid string, height uint32, indexes []int32) error {
for _, index := range indexes {
// take only outputs
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
var hash string
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))
if err != nil {
hash = bid

View File

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

View File

@ -385,27 +385,27 @@ const (
// TemplateData is used to transfer data to the templates
type TemplateData struct {
CoinName string
CoinShortcut string
CoinLabel string
InternalExplorer bool
ChainType bchain.ChainType
Address *api.Address
AddrStr string
Tx *api.Tx
Error *api.APIError
Blocks *api.Blocks
Block *api.Block
Info *api.SystemInfo
Page int
PrevPage int
NextPage int
PagingRange []int
PageParams template.URL
TOSLink string
SendTxHex string
Status string
AllTokens bool
CoinName string
CoinShortcut string
CoinLabel string
InternalExplorer bool
ChainType bchain.ChainType
Address *api.Address
AddrStr string
Tx *api.Tx
Error *api.APIError
Blocks *api.Blocks
Block *api.Block
Info *api.SystemInfo
Page int
PrevPage int
NextPage int
PagingRange []int
PageParams template.URL
TOSLink string
SendTxHex string
Status string
NonZeroBalanceTokens bool
}
func (s *PublicServer) parseTemplates() []*template.Template {
@ -600,7 +600,7 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (
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
page, ec := strconv.Atoi(r.URL.Query().Get("page"))
if ec != nil {
@ -624,16 +624,26 @@ func (s *PublicServer) getXpubAddress(r *http.Request, xpub string, pageSize int
if ec != nil {
gap = 0
}
allAddresses, _ := strconv.ParseBool(r.URL.Query().Get("alladdresses"))
return s.api.GetXpubAddress(xpub, page, pageSize, option, &api.AddressFilter{Vout: fn, AllTokens: allAddresses}, gap)
tokenLevel := api.TokenDetailNonzeroBalance
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) {
var address *api.Address
var tokenLevel api.TokenDetailLevel
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc()
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 {
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.Address.Filter = filter
}
allAddresses := r.URL.Query().Get("alladdresses")
data.AllTokens, _ = strconv.ParseBool(allAddresses)
data.NonZeroBalanceTokens = tokenLevel == api.TokenDetailNonzeroBalance
return xpubTpl, data, nil
}
@ -904,7 +913,7 @@ func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, er
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc()
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 {
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) {
var opt api.GetAddressOption
switch req.Details {
case "balance":
opt = api.Balance
case "tokens":
opt = api.Tokens
case "txids":
opt = api.TxidHistory
case "txs":
@ -363,7 +363,7 @@ func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address,
ToHeight: uint32(req.ToHeight),
Contract: req.ContractFilter,
Vout: api.AddressFilterVoutOff,
AllTokens: true,
TokenLevel: api.TokenDetailDiscovered,
}
a, err := s.api.GetXpubAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, 0)
if err != nil {

View File

@ -31,7 +31,7 @@
</tr>
{{- if $addr.TotalTokens -}}
<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;">
<table class="table data-table">
<tbody>
@ -46,12 +46,12 @@
<td class="data ellipsis"><a href="/address/{{$t.Name}}">{{$t.Name}}</a></td>
<td class="data">{{formatAmount $t.BalanceSat}} {{$cs}}</td>
<td class="data">{{$t.Transfers}}</td>
<td>{{$t.Contract}}</td>
<td>{{$t.Path}}</td>
</tr>
{{- end -}}
{{- if not $data.AllTokens -}}
{{- if $data.NonZeroBalanceTokens -}}
<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>
{{- end -}}
</tbody>

View File

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