2019-01-30 09:56:15 -07:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"blockbook/bchain"
|
|
|
|
"blockbook/db"
|
|
|
|
"fmt"
|
|
|
|
"math/big"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/golang/glog"
|
|
|
|
"github.com/juju/errors"
|
|
|
|
)
|
|
|
|
|
|
|
|
const xpubLen = 111
|
2019-01-31 00:30:18 -07:00
|
|
|
const defaultAddressesGap = 20
|
2019-01-30 09:56:15 -07:00
|
|
|
|
|
|
|
var cachedXpubs = make(map[string]*xpubData)
|
|
|
|
var cachedXpubsMux sync.Mutex
|
|
|
|
|
|
|
|
type txHeight struct {
|
|
|
|
txid string
|
|
|
|
height uint32
|
|
|
|
addrIndex uint32
|
|
|
|
}
|
|
|
|
|
|
|
|
type xpubAddress struct {
|
|
|
|
addrDesc bchain.AddressDescriptor
|
|
|
|
balance *db.AddrBalance
|
|
|
|
bottomHeight uint32
|
|
|
|
}
|
|
|
|
|
|
|
|
type xpubData struct {
|
2019-01-31 00:30:18 -07:00
|
|
|
gap int
|
2019-01-30 09:56:15 -07:00
|
|
|
dataHeight uint32
|
|
|
|
dataHash string
|
|
|
|
txs uint32
|
|
|
|
sentSat big.Int
|
|
|
|
balanceSat big.Int
|
|
|
|
addresses []xpubAddress
|
|
|
|
changeAddresses []xpubAddress
|
|
|
|
txids []txHeight
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Worker) getAddressTxHeights(addrDesc bchain.AddressDescriptor, addrIndex uint32, mempool bool, filter *AddressFilter, maxResults int) ([]txHeight, error) {
|
|
|
|
var err error
|
|
|
|
txHeights := make([]txHeight, 0, 4)
|
|
|
|
var callback db.GetTransactionsCallback
|
|
|
|
if filter.Vout == AddressFilterVoutOff {
|
|
|
|
callback = func(txid string, height uint32, indexes []int32) error {
|
|
|
|
txHeights = append(txHeights, txHeight{txid, height, addrIndex})
|
|
|
|
// take all txs in the last found block even if it exceeds maxResults
|
|
|
|
if len(txHeights) >= maxResults && txHeights[len(txHeights)-1].height != height {
|
|
|
|
return &db.StopIteration{}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
callback = func(txid string, height uint32, indexes []int32) error {
|
|
|
|
for _, index := range indexes {
|
|
|
|
vout := index
|
|
|
|
if vout < 0 {
|
|
|
|
vout = ^vout
|
|
|
|
}
|
|
|
|
if (filter.Vout == AddressFilterVoutInputs && index < 0) ||
|
|
|
|
(filter.Vout == AddressFilterVoutOutputs && index >= 0) ||
|
|
|
|
(vout == int32(filter.Vout)) {
|
|
|
|
txHeights = append(txHeights, txHeight{txid, height, addrIndex})
|
|
|
|
if len(txHeights) >= maxResults {
|
|
|
|
return &db.StopIteration{}
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if mempool {
|
|
|
|
uniqueTxs := make(map[string]struct{})
|
|
|
|
o, err := w.chain.GetMempoolTransactionsForAddrDesc(addrDesc)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
for _, m := range o {
|
|
|
|
if _, found := uniqueTxs[m.Txid]; !found {
|
|
|
|
l := len(txHeights)
|
|
|
|
callback(m.Txid, 0, []int32{m.Vout})
|
|
|
|
if len(txHeights) > l {
|
|
|
|
uniqueTxs[m.Txid] = struct{}{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
to := filter.ToHeight
|
|
|
|
if to == 0 {
|
|
|
|
to = ^uint32(0)
|
|
|
|
}
|
|
|
|
err = w.db.GetAddrDescTransactions(addrDesc, filter.FromHeight, to, callback)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return txHeights, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Worker) derivedAddressBalance(data *xpubData, ad *xpubAddress) (bool, error) {
|
|
|
|
var err error
|
|
|
|
if ad.balance, err = w.db.GetAddrDescBalance(ad.addrDesc); err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
if ad.balance != nil {
|
|
|
|
data.txs += ad.balance.Txs
|
|
|
|
data.sentSat.Add(&data.sentSat, &ad.balance.SentSat)
|
|
|
|
data.balanceSat.Add(&data.balanceSat, &ad.balance.BalanceSat)
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
2019-01-31 00:30:18 -07:00
|
|
|
func (w *Worker) scanAddresses(xpub string, data *xpubData, addresses []xpubAddress, gap int, change int, minDerivedIndex int, fork bool) (int, []xpubAddress, error) {
|
|
|
|
// rescan known addresses
|
|
|
|
lastUsed := 0
|
|
|
|
for i := range addresses {
|
|
|
|
ad := &addresses[i]
|
|
|
|
if fork {
|
|
|
|
ad.bottomHeight = 0
|
|
|
|
}
|
|
|
|
used, err := w.derivedAddressBalance(data, ad)
|
|
|
|
if err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
|
|
|
if used {
|
|
|
|
lastUsed = i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// derive new addresses as necessary
|
|
|
|
missing := len(addresses) - lastUsed
|
|
|
|
for missing < gap {
|
|
|
|
from := len(addresses)
|
|
|
|
to := from + gap - missing
|
|
|
|
if to < minDerivedIndex {
|
|
|
|
to = minDerivedIndex
|
|
|
|
}
|
|
|
|
descriptors, err := w.chainParser.DeriveAddressDescriptorsFromTo(xpub, uint32(change), uint32(from), uint32(to))
|
|
|
|
if err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
|
|
|
for i, a := range descriptors {
|
|
|
|
ad := xpubAddress{addrDesc: a}
|
|
|
|
used, err := w.derivedAddressBalance(data, &ad)
|
|
|
|
if err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
|
|
|
if used {
|
|
|
|
lastUsed = i + from
|
|
|
|
}
|
|
|
|
addresses = append(addresses, ad)
|
|
|
|
}
|
|
|
|
missing = len(addresses) - lastUsed
|
|
|
|
}
|
|
|
|
return lastUsed, addresses, nil
|
|
|
|
}
|
|
|
|
|
2019-01-30 09:56:15 -07:00
|
|
|
func (w *Worker) tokenFromXpubAddress(ad *xpubAddress, changeIndex int, index int) Token {
|
|
|
|
a, _, _ := w.chainParser.GetAddressesFromAddrDesc(ad.addrDesc)
|
|
|
|
var address string
|
|
|
|
if len(a) > 0 {
|
|
|
|
address = a[0]
|
|
|
|
}
|
|
|
|
return Token{
|
|
|
|
Type: XPUBAddressTokenType,
|
|
|
|
Name: address,
|
|
|
|
Decimals: w.chainParser.AmountDecimals(),
|
|
|
|
BalanceSat: (*Amount)(&ad.balance.BalanceSat),
|
|
|
|
Transfers: int(ad.balance.Txs),
|
|
|
|
Contract: fmt.Sprintf("%d/%d", changeIndex, index),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetAddressForXpub computes address value and gets transactions for given address
|
2019-01-31 00:30:18 -07:00
|
|
|
func (w *Worker) GetAddressForXpub(xpub string, page int, txsOnPage int, option GetAddressOption, filter *AddressFilter, gap int) (*Address, error) {
|
2019-01-30 09:56:15 -07:00
|
|
|
if w.chainType != bchain.ChainBitcoinType || len(xpub) != xpubLen {
|
|
|
|
return nil, ErrUnsupportedXpub
|
|
|
|
}
|
|
|
|
start := time.Now()
|
2019-01-31 00:30:18 -07:00
|
|
|
if gap <= 0 {
|
|
|
|
gap = defaultAddressesGap
|
|
|
|
}
|
|
|
|
// gap is increased one as there must be gap of empty addresses before the derivation is stopped
|
|
|
|
gap++
|
2019-01-30 09:56:15 -07:00
|
|
|
var processedHash string
|
|
|
|
cachedXpubsMux.Lock()
|
|
|
|
data, found := cachedXpubs[xpub]
|
|
|
|
cachedXpubsMux.Unlock()
|
|
|
|
// to load all data for xpub may take some time, perform 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")
|
|
|
|
}
|
|
|
|
if besthash == processedHash {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
fork := false
|
2019-01-31 00:30:18 -07:00
|
|
|
if !found || data.gap != gap {
|
|
|
|
data = &xpubData{gap: gap}
|
2019-01-30 09:56:15 -07:00
|
|
|
} else {
|
|
|
|
hash, err := w.db.GetBlockHash(data.dataHeight)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if hash != data.dataHash {
|
|
|
|
// in case of for reset all cached txids
|
|
|
|
fork = true
|
|
|
|
data.txids = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
processedHash = besthash
|
|
|
|
if data.dataHeight < bestheight {
|
|
|
|
data.dataHeight = bestheight
|
|
|
|
data.dataHash = besthash
|
2019-01-31 00:30:18 -07:00
|
|
|
var lastUsedIndex int
|
|
|
|
lastUsedIndex, data.addresses, err = w.scanAddresses(xpub, data, data.addresses, gap, 0, 0, fork)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-01-30 09:56:15 -07:00
|
|
|
}
|
2019-01-31 00:30:18 -07:00
|
|
|
_, data.changeAddresses, err = w.scanAddresses(xpub, data, data.changeAddresses, gap, 1, lastUsedIndex, fork)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-01-30 09:56:15 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cachedXpubsMux.Lock()
|
|
|
|
cachedXpubs[xpub] = data
|
|
|
|
cachedXpubsMux.Unlock()
|
|
|
|
tokens := make([]Token, 0, 4)
|
2019-01-31 00:30:18 -07:00
|
|
|
for i := range data.addresses {
|
|
|
|
ad := &data.addresses[i]
|
2019-01-30 09:56:15 -07:00
|
|
|
if ad.balance != nil {
|
2019-01-31 00:30:18 -07:00
|
|
|
tokens = append(tokens, w.tokenFromXpubAddress(ad, 0, i))
|
2019-01-30 09:56:15 -07:00
|
|
|
}
|
2019-01-31 00:30:18 -07:00
|
|
|
}
|
|
|
|
for i := range data.changeAddresses {
|
|
|
|
ad := &data.changeAddresses[i]
|
|
|
|
if ad.balance != nil {
|
|
|
|
tokens = append(tokens, w.tokenFromXpubAddress(ad, 1, i))
|
2019-01-30 09:56:15 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
var totalReceived big.Int
|
|
|
|
totalReceived.Add(&data.balanceSat, &data.sentSat)
|
|
|
|
addr := Address{
|
|
|
|
// Paging: pg,
|
|
|
|
AddrStr: xpub,
|
|
|
|
BalanceSat: (*Amount)(&data.balanceSat),
|
|
|
|
TotalReceivedSat: (*Amount)(&totalReceived),
|
|
|
|
TotalSentSat: (*Amount)(&data.sentSat),
|
|
|
|
Txs: int(data.txs),
|
|
|
|
// UnconfirmedBalanceSat: (*Amount)(&uBalSat),
|
|
|
|
// UnconfirmedTxs: len(txm),
|
|
|
|
// Transactions: txs,
|
|
|
|
// Txids: txids,
|
|
|
|
Tokens: tokens,
|
|
|
|
// Erc20Contract: erc20c,
|
|
|
|
// Nonce: nonce,
|
|
|
|
}
|
2019-01-31 00:30:18 -07:00
|
|
|
glog.Info("GetAddressForXpub ", xpub[:10], ", ", len(data.addresses)+len(data.changeAddresses), " derived addresses, ", data.txs, " total txs finished in ", time.Since(start))
|
2019-01-30 09:56:15 -07:00
|
|
|
return &addr, nil
|
|
|
|
}
|