diff --git a/api/types.go b/api/types.go index ee3c39f2..c753d986 100644 --- a/api/types.go +++ b/api/types.go @@ -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 { diff --git a/api/worker.go b/api/worker.go index 94aa79a9..b7019b98 100644 --- a/api/worker.go +++ b/api/worker.go @@ -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) } diff --git a/server/public.go b/server/public.go index 8134cdd7..9da61b73 100644 --- a/server/public.go +++ b/server/public.go @@ -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 } diff --git a/server/public_test.go b/server/public_test.go index 08595ec7..24cd3a12 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -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"}`, }, }, { diff --git a/server/websocket.go b/server/websocket.go index d676a3f7..560d816f 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -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") } diff --git a/static/templates/address.html b/static/templates/address.html index a9966dd0..508929fe 100644 --- a/static/templates/address.html +++ b/static/templates/address.html @@ -38,7 +38,7 @@ {{$et.Name}} {{formatAmountWithDecimals $et.BalanceSat $et.Decimals}} {{$et.Symbol}} - {{$et.Txs}} + {{$et.Transfers}} {{- end -}} diff --git a/static/test-websocket.html b/static/test-websocket.html index 8e94db61..df41f084 100644 --- a/static/test-websocket.html +++ b/static/test-websocket.html @@ -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 @@
- + - +
+
+ + + +