Filter address transactions by height from-to and contract

This commit is contained in:
Martin Boehm 2018-12-14 16:42:35 +01:00
parent 35a13e0647
commit a04b2b67b5
7 changed files with 97 additions and 51 deletions

View File

@ -74,14 +74,14 @@ func (a *Amount) AsBigInt() big.Int {
// ScriptSig contains input script
type ScriptSig struct {
Hex string `json:"hex"`
Hex string `json:"hex,omitempty"`
Asm string `json:"asm,omitempty"`
}
// Vin contains information about single transaction input
type Vin struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Txid string `json:"txid,omitempty"`
Vout uint32 `json:"vout,omitempty"`
Sequence int64 `json:"sequence,omitempty"`
N int `json:"n"`
ScriptSig ScriptSig `json:"scriptSig"`
@ -93,7 +93,7 @@ type Vin struct {
// ScriptPubKey contains output script and addresses derived from it
type ScriptPubKey struct {
Hex string `json:"hex"`
Hex string `json:"hex,omitempty"`
Asm string `json:"asm,omitempty"`
AddrDesc bchain.AddressDescriptor `json:"-"`
Addresses []string `json:"addresses"`
@ -106,7 +106,7 @@ type Vout struct {
ValueSat *Amount `json:"value,omitempty"`
N int `json:"n"`
ScriptPubKey ScriptPubKey `json:"scriptPubKey"`
Spent bool `json:"spent"`
Spent bool `json:"spent,omitempty"`
SpentTxID string `json:"spentTxId,omitempty"`
SpentIndex int `json:"spentIndex,omitempty"`
SpentHeight int `json:"spentHeight,omitempty"`
@ -115,7 +115,7 @@ type Vout struct {
// Erc20Token contains info about ERC20 token held by an address
type Erc20Token struct {
Contract string `json:"contract"`
Txs int `json:"txs"`
Transfers int `json:"transfers"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
@ -155,8 +155,8 @@ type Tx struct {
Confirmations uint32 `json:"confirmations"`
Time int64 `json:"time,omitempty"`
Blocktime int64 `json:"blocktime"`
ValueOutSat *Amount `json:"valueOut,omitempty"`
Size int `json:"size,omitempty"`
ValueOutSat *Amount `json:"valueOut,omitempty"`
ValueInSat *Amount `json:"valueIn,omitempty"`
FeesSat *Amount `json:"fees,omitempty"`
Hex string `json:"hex,omitempty"`
@ -173,14 +173,21 @@ type Paging struct {
ItemsOnPage int `json:"itemsOnPage,omitempty"`
}
// AddressFilterNone disables filtering of transactions
const AddressFilterNone = -1
const (
// AddressFilterVoutOff disables filtering of transactions by vout
AddressFilterVoutOff = -1
// AddressFilterVoutInputs specifies that only txs where the address is as input are returned
AddressFilterVoutInputs = -2
// AddressFilterVoutOutputs specifies that only txs where the address is as output are returned
AddressFilterVoutOutputs = -3
)
// AddressFilterInputs specifies that only txs where the address is as input are returned
const AddressFilterInputs = -2
// AddressFilterOutputs specifies that only txs where the address is as output are returned
const AddressFilterOutputs = -3
type AddressFilter struct {
Vout int
Contract string
FromHeight uint32
ToHeight uint32
}
// Address holds information about address and its transactions
type Address struct {

View File

@ -128,6 +128,7 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height uint32,
}
}
var valInSat, valOutSat, feesSat big.Int
var pValInSat *big.Int
vins := make([]Vin, len(bchainTx.Vin))
for i := range bchainTx.Vin {
bchainVin := &bchainTx.Vin[i]
@ -215,6 +216,7 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height uint32,
if feesSat.Sign() == -1 {
feesSat.SetUint64(0)
}
pValInSat = &valInSat
} else if w.chainType == bchain.ChainEthereumType {
ets, err := eth.GetErc20FromTx(bchainTx)
if err != nil {
@ -251,9 +253,8 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height uint32,
feesSat.Mul(ethTxData.GasPrice, ethTxData.GasUsed)
}
if len(bchainTx.Vout) > 0 {
valInSat = bchainTx.Vout[0].ValueSat
valOutSat = bchainTx.Vout[0].ValueSat
}
valOutSat = valInSat
ethSpecific = &EthereumSpecific{
GasLimit: ethTxData.GasLimit,
GasPrice: (*Amount)(ethTxData.GasPrice),
@ -280,7 +281,7 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height uint32,
Locktime: bchainTx.LockTime,
Time: bchainTx.Time,
Txid: bchainTx.Txid,
ValueInSat: (*Amount)(&valInSat),
ValueInSat: (*Amount)(pValInSat),
ValueOutSat: (*Amount)(&valOutSat),
Version: bchainTx.Version,
Hex: bchainTx.Hex,
@ -294,14 +295,14 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height uint32,
return r, nil
}
func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool, filter int) ([]string, error) {
func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool, filter *AddressFilter) ([]string, error) {
var err error
txids := make([]string, 0, 4)
addFilteredTxid := func(txid string, vout int32, isOutput bool) error {
if filter == AddressFilterNone ||
(filter == AddressFilterInputs && !isOutput) ||
(filter == AddressFilterOutputs && isOutput) ||
(vout == int32(filter)) {
if filter.Vout == AddressFilterVoutOff ||
(filter.Vout == AddressFilterVoutInputs && !isOutput) ||
(filter.Vout == AddressFilterVoutOutputs && isOutput) ||
(vout == int32(filter.Vout)) {
txids = append(txids, txid)
}
return nil
@ -321,7 +322,11 @@ func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool
addFilteredTxid(m.Txid, vout, isOutput)
}
} else {
err = w.db.GetAddrDescTransactions(addrDesc, 0, ^uint32(0), addFilteredTxid)
to := filter.ToHeight
if to == 0 {
to = ^uint32(0)
}
err = w.db.GetAddrDescTransactions(addrDesc, filter.FromHeight, to, addFilteredTxid)
if err != nil {
return nil, err
}
@ -332,7 +337,7 @@ func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool
func (t *Tx) getAddrVoutValue(addrDesc bchain.AddressDescriptor) *big.Int {
var val big.Int
for _, vout := range t.Vout {
if bytes.Equal(vout.ScriptPubKey.AddrDesc, addrDesc) {
if bytes.Equal(vout.ScriptPubKey.AddrDesc, addrDesc) && vout.ValueSat != nil {
val.Add(&val, (*big.Int)(vout.ValueSat))
}
}
@ -342,7 +347,7 @@ func (t *Tx) getAddrVoutValue(addrDesc bchain.AddressDescriptor) *big.Int {
func (t *Tx) getAddrVinValue(addrDesc bchain.AddressDescriptor) *big.Int {
var val big.Int
for _, vin := range t.Vin {
if bytes.Equal(vin.AddrDesc, addrDesc) {
if bytes.Equal(vin.AddrDesc, addrDesc) && vin.ValueSat != nil {
val.Add(&val, (*big.Int)(vin.ValueSat))
}
}
@ -435,7 +440,7 @@ func computePaging(count, page, itemsOnPage int) (Paging, int, int, int) {
}, from, to, page
}
func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, option GetAddressOption) (*db.AddrBalance, []Erc20Token, *bchain.Erc20Contract, uint64, error) {
func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, option GetAddressOption, filter *AddressFilter) (*db.AddrBalance, []Erc20Token, *bchain.Erc20Contract, uint64, error) {
var (
ba *db.AddrBalance
erc20t []Erc20Token
@ -462,8 +467,23 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
if err != nil {
return nil, nil, nil, 0, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc)
}
var filterDesc bchain.AddressDescriptor
if filter.Contract != "" {
filterDesc, err = w.chainParser.GetAddrDescFromAddress(filter.Contract)
if err != nil {
return nil, nil, nil, 0, NewAPIError(fmt.Sprintf("Invalid contract filter, %v", err), true)
}
}
erc20t = make([]Erc20Token, len(ca.Contracts))
var j int
for i, c := range ca.Contracts {
if len(filterDesc) > 0 {
if !bytes.Equal(filterDesc, c.Contract) {
continue
}
// filter only transactions by this contract
filter.Vout = i + 1
}
ci, err := w.chain.EthereumTypeGetErc20ContractInfo(c.Contract)
if err != nil {
return nil, nil, nil, 0, errors.Annotatef(err, "EthereumTypeGetErc20ContractInfo %v", c.Contract)
@ -486,16 +506,18 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
} else {
b = nil
}
erc20t[i] = Erc20Token{
erc20t[j] = Erc20Token{
BalanceSat: (*Amount)(b),
Contract: ci.Contract,
Name: ci.Name,
Symbol: ci.Symbol,
Txs: int(c.Txs),
Transfers: int(c.Txs),
Decimals: ci.Decimals,
ContractIndex: strconv.Itoa(i + 1),
}
j++
}
erc20t = erc20t[:j]
ci, err = w.chain.EthereumTypeGetErc20ContractInfo(addrDesc)
if err != nil {
return nil, nil, nil, 0, err
@ -505,7 +527,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
}
// GetAddress computes address value and gets transactions for given address
func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetAddressOption, filter int) (*Address, error) {
func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetAddressOption, filter *AddressFilter) (*Address, error) {
start := time.Now()
page--
if page < 0 {
@ -529,7 +551,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetA
)
if w.chainType == bchain.ChainEthereumType {
var n uint64
ba, erc20t, erc20c, n, err = w.getEthereumTypeAddressBalances(addrDesc, option)
ba, erc20t, erc20c, n, err = w.getEthereumTypeAddressBalances(addrDesc, option, filter)
if err != nil {
return nil, err
}
@ -598,9 +620,6 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetA
}
}
}
if len(txc) != int(ba.Txs) && w.chainType == bchain.ChainBitcoinType && filter == AddressFilterNone {
glog.Warning("DB inconsistency for address ", address, ": number of txs from column addresses ", len(txc), ", from addressBalance ", ba.Txs)
}
for i := from; i < to; i++ {
txid := txc[i]
if option == TxidHistory {
@ -674,7 +693,7 @@ func (w *Worker) GetAddressUtxo(address string, onlyConfirmed bool) ([]AddressUt
r := make([]AddressUtxo, 0, 8)
if !onlyConfirmed {
// get utxo from mempool
txm, err := w.getAddressTxids(addrDesc, true, AddressFilterNone)
txm, err := w.getAddressTxids(addrDesc, true, &AddressFilter{Vout: AddressFilterVoutOff})
if err != nil {
return nil, errors.Annotatef(err, "getAddressTxids %v true", address)
}

View File

@ -498,7 +498,7 @@ func (s *PublicServer) explorerSpendingTx(w http.ResponseWriter, r *http.Request
func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
var address *api.Address
var filter string
var fn = api.AddressFilterNone
var fn = api.AddressFilterVoutOff
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc()
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
@ -509,18 +509,18 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (
filter = r.URL.Query().Get("filter")
if len(filter) > 0 {
if filter == "inputs" {
fn = api.AddressFilterInputs
fn = api.AddressFilterVoutInputs
} else if filter == "outputs" {
fn = api.AddressFilterOutputs
fn = api.AddressFilterVoutOutputs
} else {
fn, ec = strconv.Atoi(filter)
if ec != nil || fn < 0 {
filter = ""
fn = api.AddressFilterNone
fn = api.AddressFilterVoutOff
}
}
}
address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsOnPage, api.TxHistory, fn)
address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsOnPage, api.TxHistory, &api.AddressFilter{Vout: fn})
if err != nil {
return errorTpl, nil, err
}
@ -608,7 +608,7 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t
http.Redirect(w, r, joinURL("/tx/", tx.Txid), 302)
return noTpl, nil, nil
}
address, err = s.api.GetAddress(q, 0, 1, api.Basic, api.AddressFilterNone)
address, err = s.api.GetAddress(q, 0, 1, api.Basic, &api.AddressFilter{Vout: api.AddressFilterVoutOff})
if err == nil {
http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302)
return noTpl, nil, nil
@ -766,7 +766,7 @@ func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{},
if ec != nil {
page = 0
}
address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, api.TxidHistory, api.AddressFilterNone)
address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, api.TxidHistory, &api.AddressFilter{Vout: api.AddressFilterVoutOff})
if err == nil && apiVersion == apiV1 {
return s.api.AddressToV1(address), nil
}

View File

@ -379,7 +379,7 @@ func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"txid":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":2,"n":0,"scriptSig":{"hex":""},"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"value":"0.00009876"}],"vout":[{"value":"0.00009","n":0,"scriptPubKey":{"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"]},"spent":false}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"time":22549400002,"blocktime":22549400002,"valueOut":"0.00009","valueIn":"0.00009876","fees":"0.00000876","hex":""}`,
`{"txid":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":2,"n":0,"scriptSig":{},"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"value":"0.00009876"}],"vout":[{"value":"0.00009","n":0,"scriptPubKey":{"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"]},"spent":false}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"time":22549400002,"blocktime":22549400002,"valueOut":"0.00009","valueIn":"0.00009876","fees":"0.00000876","hex":""}`,
},
},
{
@ -397,7 +397,7 @@ func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"txid":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":2,"n":0,"scriptSig":{"hex":""},"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"value":"9876"}],"vout":[{"value":"9000","n":0,"scriptPubKey":{"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"]},"spent":false}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"time":22549400002,"blocktime":22549400002,"valueOut":"9000","valueIn":"9876","fees":"876"}`,
`{"txid":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":2,"n":0,"scriptSig":{},"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"value":"9876"}],"vout":[{"value":"9000","n":0,"scriptPubKey":{"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"]}}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"time":22549400002,"blocktime":22549400002,"valueOut":"9000","valueIn":"9876","fees":"876"}`,
},
},
{

View File

@ -284,10 +284,13 @@ func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) {
}
type accountInfoReq struct {
Descriptor string `json:"descriptor"`
Details string `json:"details"`
PageSize int `json:"pageSize"`
Page int `json:"page"`
Descriptor string `json:"descriptor"`
Details string `json:"details"`
PageSize int `json:"pageSize"`
Page int `json:"page"`
FromHeight int `json:"from"`
ToHeight int `json:"to"`
ContractFilter string `json:"contractFilter"`
}
func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) {
@ -312,7 +315,13 @@ func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address,
default:
opt = api.Basic
}
return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, api.AddressFilterNone)
return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, &api.AddressFilter{
FromHeight: uint32(req.FromHeight),
ToHeight: uint32(req.ToHeight),
Contract: req.ContractFilter,
Vout: api.AddressFilterVoutOff,
})
}
return nil, errors.New("Not implemented")
}

View File

@ -38,7 +38,7 @@
<tr>
<td class="data ellipsis"><a href="/address/{{$et.Contract}}">{{$et.Name}}</a></td>
<td class="data">{{formatAmountWithDecimals $et.BalanceSat $et.Decimals}} {{$et.Symbol}}</td>
<td class="data">{{$et.Txs}}</td>
<td class="data">{{$et.Transfers}}</td>
</tr>
{{- end -}}
</tbody>

View File

@ -102,6 +102,9 @@
const selectDetails = document.getElementById('getAccountInfoDetails');
const details = selectDetails.options[selectDetails.selectedIndex].value;
const page = parseInt(document.getElementById("getAccountInfoPage").value);
const from = parseInt(document.getElementById("getAccountInfoFrom").value);
const to = parseInt(document.getElementById("getAccountInfoTo").value);
const contractFilter = document.getElementById("getAccountInfoContract").value.trim();
const pageSize = 10;
const method = 'getAccountInfo';
const params = {
@ -109,6 +112,9 @@
details,
page,
pageSize,
from,
to,
contractFilter
};
send(method, params, function (result) {
document.getElementById('getAccountInfoResult').innerText = JSON.stringify(result).replace(/,/g, ", ");
@ -257,14 +263,19 @@
</div>
<div class="col-8">
<div class="row" style="margin: 0;">
<input type="text" style="width: 67.7%" class="form-control" id="getAccountInfoDescriptor" value="0x103262f243e6f67d12d6a4ea0d45302c1fa4bb0a">
<input type="text" style="width: 79%" class="form-control" id="getAccountInfoDescriptor" value="0x65513ecd11fd3a5b1fefdcc6a500b025008405a2">
<select id="getAccountInfoDetails" style="width: 20%; margin-left: 5px;">
<option value="basic">Basic</option>
<option value="balance">Balance</option>
<option value="txids">Txids</option>
<option value="txs">Transactions</option>
</select>
<input type="text" style="width: 10%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAccountInfoPage" value="0">
</div>
<div class="row" style="margin: 0; margin-top: 5px;">
<input type="text" placeholder="page" style="width: 10%; margin-right: 5px;" class="form-control" id="getAccountInfoPage">
<input type="text" placeholder="from" style="width: 15%;margin-left: 5px;margin-right: 5px;" class="form-control" id="getAccountInfoFrom">
<input type="text" placeholder="to" style="width: 15%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAccountInfoTo">
<input type="text" placeholder="contract" style="width: 55%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAccountInfoContract">
</div>
</div>
<div class="col form-inline"></div>