diff --git a/docs/rpc.md b/docs/rpc.md index ba025c07d..5bf369f21 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -56,6 +56,7 @@ which would yield the response: | `gettxout` | | `getunclaimed` | | `getunspents` | +| `getutxotransfers` | | `getvalidators` | | `getversion` | | `invoke` | @@ -102,6 +103,164 @@ and we're not accepting issues related to them. Some additional extensions are implemented as a part of this RPC server. +#### Limits and paging for getnep5transfers and getutxotransfers + +Both `getnep5transfers` and `getutxotransfers` RPC calls never return more than +1000 results for one request (within specified time frame). You can pass your +own limit via an additional parameter and then use paging to request the next +batch of transfers. + +Example requesting 10 events for address AYC7wn4xb8SEeYpgPXHHjLr3gBuWbgAC3Q +within 0-1600094189 timestamps: + +```json +{ "jsonrpc": "2.0", "id": 5, "method": "getnep5transfers", "params": +["AYC7wn4xb8SEeYpgPXHHjLr3gBuWbgAC3Q", 0, 1600094189, 10] } +``` + +Get the next 10 transfers for the same account within the same time frame: + +```json +{ "jsonrpc": "2.0", "id": 5, "method": "getnep5transfers", "params": +["AYC7wn4xb8SEeYpgPXHHjLr3gBuWbgAC3Q", 0, 1600094189, 10, 1] } +``` + +#### getalltransfertx call + +In addition to regular `getnep5transfers` and `getutxotransfers` RPC calls +`getalltransfertx` is provided to return both NEP5 and UTXO events for account +in a single stream of events. These events are grouped by transaction and an +additional metadata like fees is provided. It has the same parameters as +`getnep5transfers`, but limits and paging is applied to transactions instead +of transfer events. UTXO inputs and outputs are provided by `elements` array, +while NEP5 transfer events are contained in `events` array. + +Example request: + +```json +{ "jsonrpc": "2.0", "id": 5, "method": "getalltransfertx", "params": +["AYC7wn4xb8SEeYpgPXHHjLr3gBuWbgAC3Q", 0, 1600094189, 2] } + +``` + +Reply: + +```json +{ + "jsonrpc" : "2.0", + "result" : [ + { + "txid" : "0x1cb7e089bb52cabb35c480de9d99c41c6fea7f5a276b41d71ab3fc7c470dcb74", + "events" : [ + { + "type" : "send", + "asset" : "3a4acd3647086e7c44398aac0349802e6a171129", + "value" : "20000000000", + "address" : "ALuZLuuDssJqG2E4foANKwbLamYHuffFjg" + } + ], + "net_fee" : 0, + "block_index" : 6163114, + "timestamp" : 1600094117, + "sys_fee" : 0 + }, + { + "block_index" : 6162995, + "net_fee" : 0, + "timestamp" : 1600092165, + "events" : [ + { + "address" : "ALuZLuuDssJqG2E4foANKwbLamYHuffFjg", + "value" : "20000000000", + "type" : "receive", + "asset" : "3a4acd3647086e7c44398aac0349802e6a171129" + } + ], + "txid" : "0xc8b45480ade5395a4a239bb44eea6d86113f32090c4854b0c4aeee1b9485edab", + "sys_fee" : 0 + } + ], + "id" : 5 +} + +``` + +Another request: + +```json +{ "jsonrpc": "2.0", "id": 5, "method": "getalltransfertx", "params": +["AKJL9HwrFGdic9GTTXrdaHuNYa5oxqioRY", 0, 1600079056, 2, 13] } +``` + +Reply: + +```json +{ + "result" : [ + { + "timestamp" : 1561566911, + "net_fee" : 1, + "events" : [ + { + "address" : "AZCcft1uYtmZXxzHPr5tY7L6M85zG7Dsrv", + "asset" : "1578103c13e39df15d0d29826d957e85d770d8c9", + "type" : "receive", + "value" : "2380844141430" + } + ], + "elements" : [ + { + "asset" : "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", + "address" : "AZCcft1uYtmZXxzHPr5tY7L6M85zG7Dsrv", + "value" : "0.00000831", + "type" : "input" + }, + { + "address" : "AZCcft1uYtmZXxzHPr5tY7L6M85zG7Dsrv", + "asset" : "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", + "type" : "output", + "value" : "0.0000083" + } + ], + "sys_fee" : 0, + "txid" : "0xb4f1bdb466d8bd3524502008a0bc1f9342356b4eea67be19d384845c670442a6", + "block_index" : 3929554 + }, + { + "elements" : [ + { + "value" : "0.00000838", + "type" : "input", + "asset" : "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", + "address" : "AZCcft1uYtmZXxzHPr5tY7L6M85zG7Dsrv" + }, + { + "asset" : "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", + "address" : "AZCcft1uYtmZXxzHPr5tY7L6M85zG7Dsrv", + "value" : "0.00000837", + "type" : "output" + } + ], + "events" : [ + { + "asset" : "1578103c13e39df15d0d29826d957e85d770d8c9", + "address" : "AZCcft1uYtmZXxzHPr5tY7L6M85zG7Dsrv", + "value" : "2100000000", + "type" : "receive" + } + ], + "timestamp" : 1561566300, + "net_fee" : 1, + "block_index" : 3929523, + "sys_fee" : 0, + "txid" : "0xc045c0612b34218b7e5eaee973114af3eff925f859adf23cf953930f667cdc93" + } + ], + "id" : 5, + "jsonrpc" : "2.0" +} +``` + #### Websocket server This server accepts websocket connections on `ws://$BASE_URL/ws` address. You diff --git a/pkg/rpc/client/doc.go b/pkg/rpc/client/doc.go index c21d3ebf4..511414842 100644 --- a/pkg/rpc/client/doc.go +++ b/pkg/rpc/client/doc.go @@ -19,6 +19,7 @@ TODO: Supported methods getaccountstate + getalltransfertx getapplicationlog getassetstate getbestblockhash @@ -40,6 +41,7 @@ Supported methods gettxout getunclaimed getunspents + getutxotransfers getvalidators getversion invoke diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 5a9411075..075abcae9 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -28,6 +28,20 @@ func (c *Client) GetAccountState(address string) (*result.AccountState, error) { return resp, nil } +// GetAllTransferTx returns all transfer transactions for a given account within +// specified timestamps (by block time) with specified output limits and page. It +// only works with neo-go 0.78.0+ servers. +func (c *Client) GetAllTransferTx(acc util.Uint160, start, end uint32, limit, page int) ([]result.TransferTx, error) { + var ( + params = request.NewRawParams(acc.StringLE(), start, end, limit, page) + resp = new([]result.TransferTx) + ) + if err := c.performRequest("getalltransfertx", params, resp); err != nil { + return nil, err + } + return *resp, nil +} + // GetApplicationLog returns the contract log based on the specified txid. func (c *Client) GetApplicationLog(hash util.Uint256) (*result.ApplicationLog, error) { var ( @@ -231,9 +245,31 @@ func (c *Client) GetNEP5Balances(address util.Uint160) (*result.NEP5Balances, er return resp, nil } -// GetNEP5Transfers is a wrapper for getnep5transfers RPC. -func (c *Client) GetNEP5Transfers(address string) (*result.NEP5Transfers, error) { +// GetNEP5Transfers is a wrapper for getnep5transfers RPC. Address parameter +// is mandatory, while all the others are optional. Start and stop parameters +// are supported since neo-go 0.77.0 and limit and page since neo-go 0.78.0. +// These parameters are positional in the JSON-RPC call, you can't specify limit +// and not specify start/stop for example. +func (c *Client) GetNEP5Transfers(address string, start, stop *uint32, limit, page *int) (*result.NEP5Transfers, error) { params := request.NewRawParams(address) + if start != nil { + params.Values = append(params.Values, *start) + if stop != nil { + params.Values = append(params.Values, *stop) + if limit != nil { + params.Values = append(params.Values, *limit) + if page != nil { + params.Values = append(params.Values, *page) + } + } else if page != nil { + return nil, errors.New("bad parameters") + } + } else if limit != nil || page != nil { + return nil, errors.New("bad parameters") + } + } else if stop != nil || limit != nil || page != nil { + return nil, errors.New("bad parameters") + } resp := new(result.NEP5Transfers) if err := c.performRequest("getnep5transfers", params, resp); err != nil { return nil, err @@ -368,6 +404,38 @@ func (c *Client) GetUnspents(address string) (*result.Unspents, error) { return resp, nil } +// GetUTXOTransfers is a wrapper for getutxoransfers RPC. Address parameter +// is mandatory, while all the others are optional. It's only supported since +// neo-go 0.77.0 with limit and page parameters only since neo-go 0.78.0. +// These parameters are positional in the JSON-RPC call, you can't specify limit +// and not specify start/stop for example. +func (c *Client) GetUTXOTransfers(address string, start, stop *uint32, limit, page *int) (*result.GetUTXO, error) { + params := request.NewRawParams(address) + if start != nil { + params.Values = append(params.Values, *start) + if stop != nil { + params.Values = append(params.Values, *stop) + if limit != nil { + params.Values = append(params.Values, *limit) + if page != nil { + params.Values = append(params.Values, *page) + } + } else if page != nil { + return nil, errors.New("bad parameters") + } + } else if limit != nil || page != nil { + return nil, errors.New("bad parameters") + } + } else if stop != nil || limit != nil || page != nil { + return nil, errors.New("bad parameters") + } + resp := new(result.GetUTXO) + if err := c.performRequest("getutxotransfers", params, resp); err != nil { + return nil, err + } + return resp, nil +} + // GetValidators returns the current NEO consensus nodes information and voting status. func (c *Client) GetValidators() ([]result.Validator, error) { var ( diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index dbe8cba23..8369f2fb2 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -481,7 +481,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ { name: "positive", invoke: func(c *Client) (interface{}, error) { - return c.GetNEP5Transfers("AbHgdBaWEnHkCiLtDZXjhvhaAK2cwFh5pF") + return c.GetNEP5Transfers("AbHgdBaWEnHkCiLtDZXjhvhaAK2cwFh5pF", nil, nil, nil, nil) }, serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"sent":[],"received":[{"timestamp":1555651816,"asset_hash":"600c4f5200db36177e3e8a09e9f18e2fc7d12a0f","transfer_address":"AYwgBNMepiv5ocGcyNT4mA8zPLTQ8pDBis","amount":"1000000","block_index":436036,"transfer_notify_index":0,"tx_hash":"df7683ece554ecfb85cf41492c5f143215dd43ef9ec61181a28f922da06aba58"}],"address":"AbHgdBaWEnHkCiLtDZXjhvhaAK2cwFh5pF"}}`, result: func(c *Client) interface{} { @@ -1138,7 +1138,7 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{ { name: "getnep5transfers_invalid_params_error", invoke: func(c *Client) (interface{}, error) { - return c.GetNEP5Transfers("") + return c.GetNEP5Transfers("", nil, nil, nil, nil) }, }, { @@ -1320,7 +1320,7 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{ { name: "getnep5transfers_unmarshalling_error", invoke: func(c *Client) (interface{}, error) { - return c.GetNEP5Transfers("") + return c.GetNEP5Transfers("", nil, nil, nil, nil) }, }, { diff --git a/pkg/rpc/response/result/nep5.go b/pkg/rpc/response/result/nep5.go index 897c82345..7b3a2a773 100644 --- a/pkg/rpc/response/result/nep5.go +++ b/pkg/rpc/response/result/nep5.go @@ -69,3 +69,24 @@ func (b *NEP5Balance) UnmarshalJSON(data []byte) error { b.LastUpdated = s.LastUpdated return nil } + +// TransferTx is a type used to represent and element of `getalltransfertx` +// result. It combines transaction's inputs/outputs with NEP5 events. +type TransferTx struct { + TxID util.Uint256 `json:"txid"` + Timestamp uint32 `json:"timestamp"` + Index uint32 `json:"block_index"` + SystemFee int64 `json:"sys_fee"` + NetworkFee int64 `json:"net_fee"` + Elements []TransferTxEvent `json:"elements,omitempty"` + Events []TransferTxEvent `json:"events,omitempty"` +} + +// TransferTxEvent is an event used for elements or events of TransferTx, it's +// either a single input/output, or a nep5 transfer. +type TransferTxEvent struct { + Address string `json:"address"` + Type string `json:"type"` + Value string `json:"value"` + Asset string `json:"asset"` +} diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index e05671399..e8afaef9f 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -74,10 +74,14 @@ const ( // treated like subscriber, so technically it's a limit on websocket // connections. maxSubscribers = 64 + + // Maximum number of elements for get*transfers requests. + maxTransfersLimit = 1000 ) var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){ "getaccountstate": (*Server).getAccountState, + "getalltransfertx": (*Server).getAllTransferTx, "getapplicationlog": (*Server).getApplicationLog, "getassetstate": (*Server).getAssetState, "getbestblockhash": (*Server).getBestBlockHash, @@ -450,34 +454,54 @@ func (s *Server) getVersion(_ request.Params) (interface{}, *response.Error) { }, nil } -func getTimestampsAndLimit(p1, p2, p3 *request.Param) (uint32, uint32, int, error) { +func getTimestampsAndLimit(ps request.Params, index int) (uint32, uint32, int, int, error) { var start, end uint32 - var limit int - if p1 != nil { - val, err := p1.GetInt() + var limit, page int + + limit = maxTransfersLimit + pStart, pEnd, pLimit, pPage := ps.Value(index), ps.Value(index+1), ps.Value(index+2), ps.Value(index+3) + if pPage != nil { + p, err := pPage.GetInt() if err != nil { - return 0, 0, 0, err + return 0, 0, 0, 0, err } - start = uint32(val) - } - if p2 != nil { - val, err := p2.GetInt() - if err != nil { - return 0, 0, 0, err + if p < 0 { + return 0, 0, 0, 0, errors.New("can't use negative page") } - end = uint32(val) + page = p } - if p3 != nil { - l, err := p3.GetInt() + if pLimit != nil { + l, err := pLimit.GetInt() if err != nil { - return 0, 0, 0, err + return 0, 0, 0, 0, err } if l <= 0 { - return 0, 0, 0, errors.New("can't use negative or zero limit") + return 0, 0, 0, 0, errors.New("can't use negative or zero limit") + } + if l > maxTransfersLimit { + return 0, 0, 0, 0, errors.New("too big limit requested") } limit = l } - return start, end, limit, nil + if pEnd != nil { + val, err := pEnd.GetInt() + if err != nil { + return 0, 0, 0, 0, err + } + end = uint32(val) + } else { + end = uint32(time.Now().Unix()) + } + if pStart != nil { + val, err := pStart.GetInt() + if err != nil { + return 0, 0, 0, 0, err + } + start = uint32(val) + } else { + start = uint32(time.Now().Add(-time.Hour * 24 * 7).Unix()) + } + return start, end, limit, page, nil } func getAssetMaps(name string) (map[util.Uint256]*result.AssetUTXO, map[util.Uint256]*result.AssetUTXO, error) { @@ -528,38 +552,33 @@ func (s *Server) getUTXOTransfers(ps request.Params) (interface{}, *response.Err index++ } - p1, p2, p3 := ps.Value(index), ps.Value(index+1), ps.Value(index+2) - start, end, limit, err := getTimestampsAndLimit(p1, p2, p3) + start, end, limit, page, err := getTimestampsAndLimit(ps, index) if err != nil { return nil, response.NewInvalidParamsError("", err) } - if p2 == nil { - end = uint32(time.Now().Unix()) - if p1 == nil { - start = uint32(time.Now().Add(-time.Hour * 24 * 7).Unix()) - } - } sent, recv, err := getAssetMaps(assetName) if err != nil { return nil, response.NewInvalidParamsError("", err) } tr := new(state.Transfer) + var resCount, frameCount int err = s.chain.ForEachTransfer(addr, tr, func() (bool, error) { + // Iterating from newest to oldest, not yet reached required + // time frame, continue looping. if tr.Timestamp > end { return true, nil } - var count int - for _, res := range sent { - count += len(res.Transactions) - } - for _, res := range recv { - count += len(res.Transactions) - } - if tr.Timestamp < start || - (limit != 0 && count >= limit) { + // Iterating from newest to oldest, moved past required + // time frame, stop looping. + if tr.Timestamp < start { return false, nil } + frameCount++ + // Using limits, not yet reached required page. + if limit != 0 && page*limit >= frameCount { + return true, nil + } assetID := core.GoverningTokenID() if !tr.IsGoverning { assetID = core.UtilityTokenID() @@ -578,6 +597,11 @@ func (s *Server) getUTXOTransfers(ps request.Params) (interface{}, *response.Err }) a.TotalAmount += tr.Amount } + resCount++ + // Using limits, reached limit. + if limit != 0 && resCount >= limit { + return false, nil + } return true, nil }) if err != nil { @@ -743,17 +767,10 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err return nil, response.ErrInvalidParams } - p1, p2, p3 := ps.Value(1), ps.Value(2), ps.Value(3) - start, end, limit, err := getTimestampsAndLimit(p1, p2, p3) + start, end, limit, page, err := getTimestampsAndLimit(ps, 1) if err != nil { return nil, response.NewInvalidParamsError("", err) } - if p2 == nil { - end = uint32(time.Now().Unix()) - if p1 == nil { - start = uint32(time.Now().Add(-time.Hour * 24 * 7).Unix()) - } - } bs := &result.NEP5Transfers{ Address: address.Uint160ToString(u), @@ -761,14 +778,23 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err Sent: []result.NEP5Transfer{}, } tr := new(state.NEP5Transfer) + var resCount, frameCount int err = s.chain.ForEachNEP5Transfer(u, tr, func() (bool, error) { + // Iterating from newest to oldest, not yet reached required + // time frame, continue looping. if tr.Timestamp > end { return true, nil } - if tr.Timestamp < start || - (limit != 0 && (len(bs.Received)+len(bs.Sent) >= limit)) { + // Iterating from newest to oldest, moved past required + // time frame, stop looping. + if tr.Timestamp < start { return false, nil } + frameCount++ + // Using limits, not yet reached required page. + if limit != 0 && page*limit >= frameCount { + return true, nil + } transfer := result.NEP5Transfer{ Timestamp: tr.Timestamp, Asset: tr.Asset, @@ -783,14 +809,18 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err transfer.Address = address.Uint160ToString(tr.From) } bs.Received = append(bs.Received, transfer) - return true, nil + } else { + transfer.Amount = strconv.FormatInt(-tr.Amount, 10) + if !tr.To.Equals(util.Uint160{}) { + transfer.Address = address.Uint160ToString(tr.To) + } + bs.Sent = append(bs.Sent, transfer) } - - transfer.Amount = strconv.FormatInt(-tr.Amount, 10) - if !tr.To.Equals(util.Uint160{}) { - transfer.Address = address.Uint160ToString(tr.To) + resCount++ + // Using limits, reached limit. + if limit != 0 && resCount >= limit { + return false, nil } - bs.Sent = append(bs.Sent, transfer) return true, nil }) if err != nil { @@ -799,6 +829,194 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err return bs, nil } +func (s *Server) getAllTransferTx(ps request.Params) (interface{}, *response.Error) { + var respErr *response.Error + + u, err := ps.Value(0).GetUint160FromAddressOrHex() + if err != nil { + return nil, response.ErrInvalidParams + } + + start, end, limit, page, err := getTimestampsAndLimit(ps, 1) + if err != nil { + return nil, response.NewInvalidParamsError("", err) + } + + var ( + utxoCont = make(chan bool) + nep5Cont = make(chan bool) + utxoTrs = make(chan state.Transfer) + nep5Trs = make(chan state.NEP5Transfer) + ) + + go func() { + tr := new(state.Transfer) + _ = s.chain.ForEachTransfer(u, tr, func() (bool, error) { + var cont bool + + // Iterating from newest to oldest, not yet reached required + // time frame, continue looping. + if tr.Timestamp > end { + return true, nil + } + // Iterating from newest to oldest, moved past required + // time frame, stop looping. + if tr.Timestamp < start { + return false, nil + } + utxoTrs <- *tr + cont = <-utxoCont + return cont, nil + }) + close(utxoTrs) + }() + + go func() { + tr := new(state.NEP5Transfer) + _ = s.chain.ForEachNEP5Transfer(u, tr, func() (bool, error) { + var cont bool + + // Iterating from newest to oldest, not yet reached required + // time frame, continue looping. + if tr.Timestamp > end { + return true, nil + } + // Iterating from newest to oldest, moved past required + // time frame, stop looping. + if tr.Timestamp < start { + return false, nil + } + nep5Trs <- *tr + cont = <-nep5Cont + return cont, nil + }) + close(nep5Trs) + }() + + var ( + res = make([]result.TransferTx, 0, limit) + frameCount int + utxoLast state.Transfer + nep5Last state.NEP5Transfer + haveUtxo, haveNep5 bool + ) + + utxoLast, haveUtxo = <-utxoTrs + if haveUtxo { + utxoCont <- true + } + nep5Last, haveNep5 = <-nep5Trs + if haveNep5 { + nep5Cont <- true + } + for len(res) < limit { + if !haveUtxo && !haveNep5 { + break + } + var isNep5 = haveNep5 && (!haveUtxo || (nep5Last.Timestamp > utxoLast.Timestamp)) + var transfer result.TransferTx + if isNep5 { + transfer.TxID = nep5Last.Tx + transfer.Timestamp = nep5Last.Timestamp + transfer.Index = nep5Last.Block + } else { + transfer.TxID = utxoLast.Tx + transfer.Timestamp = utxoLast.Timestamp + transfer.Index = utxoLast.Block + } + frameCount++ + // Using limits, not yet reached required page. But still need + // to drain inputs for this tx. + skipTx := page*limit >= frameCount + + if !skipTx { + tx, _, err := s.chain.GetTransaction(transfer.TxID) + if err != nil { + respErr = response.NewInternalServerError("invalid NEP5 transfer log", err) + break + } + transfer.NetworkFee = int64(s.chain.NetworkFee(tx)) + transfer.SystemFee = int64(s.chain.SystemFee(tx)) + + inouts, err := s.chain.References(tx) + if err != nil { + respErr = response.NewInternalServerError("invalid tx", err) + break + } + for _, inout := range inouts { + var event result.TransferTxEvent + + event.Address = address.Uint160ToString(inout.Out.ScriptHash) + event.Type = "input" + event.Value = inout.Out.Amount.String() + event.Asset = inout.Out.AssetID.StringLE() + transfer.Elements = append(transfer.Elements, event) + } + for _, out := range tx.Outputs { + var event result.TransferTxEvent + + event.Address = address.Uint160ToString(out.ScriptHash) + event.Type = "output" + event.Value = out.Amount.String() + event.Asset = out.AssetID.StringLE() + transfer.Elements = append(transfer.Elements, event) + } + } + // Pick all NEP5 events for this transaction, if there are any. + for haveNep5 && nep5Last.Tx.Equals(transfer.TxID) { + if !skipTx { + var event result.TransferTxEvent + event.Asset = nep5Last.Asset.StringLE() + if nep5Last.Amount > 0 { // token was received + event.Value = strconv.FormatInt(nep5Last.Amount, 10) + event.Type = "receive" + if !nep5Last.From.Equals(util.Uint160{}) { + event.Address = address.Uint160ToString(nep5Last.From) + } + } else { + event.Value = strconv.FormatInt(-nep5Last.Amount, 10) + event.Type = "send" + if !nep5Last.To.Equals(util.Uint160{}) { + event.Address = address.Uint160ToString(nep5Last.To) + } + } + transfer.Events = append(transfer.Events, event) + } + nep5Last, haveNep5 = <-nep5Trs + if haveNep5 { + nep5Cont <- true + } + } + + // Skip UTXO events, we've already got them from inputs and outputs. + for haveUtxo && utxoLast.Tx.Equals(transfer.TxID) { + utxoLast, haveUtxo = <-utxoTrs + if haveUtxo { + utxoCont <- true + } + } + if !skipTx { + res = append(res, transfer) + } + } + if haveUtxo { + _, ok := <-utxoTrs + if ok { + utxoCont <- false + } + } + if haveNep5 { + _, ok := <-nep5Trs + if ok { + nep5Cont <- false + } + } + if respErr != nil { + return nil, respErr + } + return res, nil +} + func (s *Server) getMinimumNetworkFee(ps request.Params) (interface{}, *response.Error) { return s.chain.GetConfig().MinimumNetworkFee, nil } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index ce08abaeb..e1673ab42 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -1164,24 +1164,28 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] }) t.Run("getutxotransfers", func(t *testing.T) { - testGetUTXO := func(t *testing.T, asset string, start, stop int) { + testGetUTXO := func(t *testing.T, asset string, start, stop, limit, page int, present []int) { ps := []string{`"AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs"`} if asset != "" { ps = append(ps, fmt.Sprintf("%q", asset)) } - if start >= 0 { - if start > int(e.chain.HeaderHeight()) { - ps = append(ps, strconv.Itoa(int(time.Now().Unix()))) - } else { - b, err := e.chain.GetHeader(e.chain.GetHeaderHash(start)) - require.NoError(t, err) - ps = append(ps, strconv.Itoa(int(b.Timestamp))) - } - if stop != 0 { - b, err := e.chain.GetHeader(e.chain.GetHeaderHash(stop)) - require.NoError(t, err) - ps = append(ps, strconv.Itoa(int(b.Timestamp))) - } + if start > int(e.chain.HeaderHeight()) { + ps = append(ps, strconv.Itoa(int(time.Now().Unix()))) + } else { + b, err := e.chain.GetHeader(e.chain.GetHeaderHash(start)) + require.NoError(t, err) + ps = append(ps, strconv.Itoa(int(b.Timestamp))) + } + if stop != 0 { + b, err := e.chain.GetHeader(e.chain.GetHeaderHash(stop)) + require.NoError(t, err) + ps = append(ps, strconv.Itoa(int(b.Timestamp))) + } + if limit != 0 { + ps = append(ps, strconv.Itoa(limit)) + } + if page != 0 { + ps = append(ps, strconv.Itoa(page)) } p := strings.Join(ps, ", ") rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getutxotransfers", "params": [%s]}`, p) @@ -1189,11 +1193,17 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] res := checkErrGetResult(t, body, false) actual := new(result.GetUTXO) require.NoError(t, json.Unmarshal(res, actual)) - checkTransfers(t, e, actual, asset, start, stop) + checkTransfers(t, e, actual, present) } - t.Run("RestrictByAsset", func(t *testing.T) { testGetUTXO(t, "neo", 0, 0) }) - t.Run("TooBigStart", func(t *testing.T) { testGetUTXO(t, "", 300, 0) }) - t.Run("RestrictAll", func(t *testing.T) { testGetUTXO(t, "", 202, 203) }) + // See `checkTransfers` for the last parameter values. + t.Run("All", func(t *testing.T) { testGetUTXO(t, "", 0, 207, 0, 0, []int{0, 1, 2, 3, 4, 5, 6, 7}) }) + t.Run("RestrictByAsset", func(t *testing.T) { testGetUTXO(t, "neo", 0, 0, 0, 0, []int{0, 1, 2, 6, 7}) }) + t.Run("TooBigStart", func(t *testing.T) { testGetUTXO(t, "", 300, 0, 0, 0, []int{}) }) + t.Run("RestrictAll", func(t *testing.T) { testGetUTXO(t, "", 202, 203, 0, 0, []int{1, 2, 3}) }) + t.Run("Limit", func(t *testing.T) { testGetUTXO(t, "neo", 0, 207, 2, 0, []int{7, 6}) }) + t.Run("Limit 2", func(t *testing.T) { testGetUTXO(t, "", 0, 204, 1, 0, []int{5}) }) + t.Run("Limit with page", func(t *testing.T) { testGetUTXO(t, "", 0, 204, 1, 1, []int{4}) }) + t.Run("Limit with page 2", func(t *testing.T) { testGetUTXO(t, "", 0, 204, 2, 2, []int{1, 0}) }) }) t.Run("getnep5transfers", func(t *testing.T) { @@ -1213,6 +1223,108 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] require.NoError(t, json.Unmarshal(res, actual)) checkNep5TransfersAux(t, e, actual, true) }) + + t.Run("getalltransfertx", func(t *testing.T) { + testGetTxs := func(t *testing.T, asset string, start, stop, limit, page int, present []util.Uint256) { + ps := []string{`"AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs"`} + ps = append(ps, strconv.Itoa(start)) + ps = append(ps, strconv.Itoa(stop)) + if limit != 0 { + ps = append(ps, strconv.Itoa(limit)) + } + if page != 0 { + ps = append(ps, strconv.Itoa(page)) + } + p := strings.Join(ps, ", ") + rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getalltransfertx", "params": [%s]}`, p) + body := doRPCCall(rpc, httpSrv.URL, t) + res := checkErrGetResult(t, body, false) + actualp := new([]result.TransferTx) + require.NoError(t, json.Unmarshal(res, actualp)) + actual := *actualp + require.Equal(t, len(present), len(actual)) + for _, id := range present { + var isThere bool + var ttx result.TransferTx + for i := range actual { + if id.Equals(actual[i].TxID) { + ttx = actual[i] + isThere = true + break + } + } + require.True(t, isThere) + tx, h, err := e.chain.GetTransaction(id) + require.NoError(t, err) + require.Equal(t, h, ttx.Index) + require.Equal(t, int64(e.chain.SystemFee(tx)), ttx.SystemFee) + require.Equal(t, int64(e.chain.NetworkFee(tx)), ttx.NetworkFee) + require.Equal(t, len(tx.Inputs)+len(tx.Outputs), len(ttx.Elements)) + } + } + b, err := e.chain.GetBlock(e.chain.GetHeaderHash(1)) + require.NoError(t, err) + txMoveNeo := b.Transactions[1].Hash() + b, err = e.chain.GetBlock(e.chain.GetHeaderHash(202)) + require.NoError(t, err) + txNeoRT := b.Transactions[1].Hash() + b, err = e.chain.GetBlock(e.chain.GetHeaderHash(203)) + require.NoError(t, err) + txGasClaim := b.Transactions[1].Hash() + b, err = e.chain.GetBlock(e.chain.GetHeaderHash(204)) + require.NoError(t, err) + txDeploy := b.Transactions[1].Hash() + ts204 := int(b.Timestamp) + b, err = e.chain.GetBlock(e.chain.GetHeaderHash(206)) + require.NoError(t, err) + txNeoTo1 := b.Transactions[1].Hash() + b, err = e.chain.GetBlock(e.chain.GetHeaderHash(207)) + require.NoError(t, err) + txNep5Tr := b.Transactions[2].Hash() + b, err = e.chain.GetBlock(e.chain.GetHeaderHash(208)) + require.NoError(t, err) + txNep5To1 := b.Transactions[1].Hash() + ts208 := int(b.Timestamp) + b, err = e.chain.GetBlock(e.chain.GetHeaderHash(209)) + require.NoError(t, err) + txMigrate := b.Transactions[1].Hash() + b, err = e.chain.GetBlock(e.chain.GetHeaderHash(210)) + require.NoError(t, err) + txNep5To0 := b.Transactions[1].Hash() + lastTs := int(b.Timestamp) + t.Run("All", func(t *testing.T) { + testGetTxs(t, "", 0, lastTs, 0, 0, []util.Uint256{ + txMoveNeo, txNeoRT, txGasClaim, txDeploy, txNeoTo1, txNep5Tr, + txNep5To1, txMigrate, txNep5To0, + }) + }) + t.Run("last 3", func(t *testing.T) { + testGetTxs(t, "", 0, lastTs, 3, 0, []util.Uint256{ + txNep5To1, txMigrate, txNep5To0, + }) + }) + t.Run("3, page 1", func(t *testing.T) { + testGetTxs(t, "", 0, lastTs, 3, 1, []util.Uint256{ + txDeploy, txNeoTo1, txNep5Tr, + }) + }) + t.Run("3, page 2", func(t *testing.T) { + testGetTxs(t, "", 0, lastTs, 3, 2, []util.Uint256{ + txMoveNeo, txNeoRT, txGasClaim, + }) + }) + t.Run("3, page 3", func(t *testing.T) { + testGetTxs(t, "", 0, lastTs, 3, 3, []util.Uint256{}) + }) + t.Run("no dates", func(t *testing.T) { + testGetTxs(t, "", 0, 1000000, 0, 0, []util.Uint256{}) + }) + t.Run("204-208", func(t *testing.T) { + testGetTxs(t, "", ts204, ts208, 0, 0, []util.Uint256{ + txDeploy, txNeoTo1, txNep5Tr, txNep5To1, + }) + }) + }) } func (tc rpcTestCase) getResultPair(e *executor) (expected interface{}, res interface{}) { @@ -1312,43 +1424,43 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, onlyFirst require.Equal(t, uint32(0), res.Sent[0].NotifyIndex) } -func checkTransfers(t *testing.T, e *executor, acc interface{}, asset string, start, stop int) { +func checkTransfers(t *testing.T, e *executor, acc interface{}, checked []int) { + type transfer struct { + sent bool + asset string + index uint32 + amount int64 + } + + var transfers = []transfer{ + {false, "neo", 1, 99999000}, // NEO to us. + {false, "neo", 202, 99999000}, // NEO roundtrip for GAS claim. + {true, "neo", 202, 99999000}, // NEO roundtrip for GAS claim. + {false, "gas", 203, 160798392000}, // GAS claim. + {false, "gas", 204, 150798392000}, // Remainder from contract deployment. + {true, "gas", 204, 160798392000}, // Contract deployment. + {false, "neo", 206, 99998000}, // Remainder of NEO sent. + {true, "neo", 206, 99999000}, // NEO to another validator. + } res := acc.(*result.GetUTXO) require.Equal(t, res.Address, "AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs") - // transfer from multisig address to us - u := getUTXOForBlock(res, false, "neo", 1) - if start <= 1 && (stop == 0 || stop >= 1) && (asset == "neo" || asset == "") { - require.NotNil(t, u) - require.EqualValues(t, int64(99999000), u.Amount) - } else { - require.Nil(t, u) - } + for i, tr := range transfers { + var present bool - // gas claim - u = getUTXOForBlock(res, false, "gas", 203) - if start <= 203 && (stop == 0 || stop >= 203) && (asset == "gas" || asset == "") { - require.NotNil(t, u) - require.EqualValues(t, int64(160798392000), u.Amount) - } else { - require.Nil(t, u) - } - - // transfer from us to another validator - u = getUTXOForBlock(res, true, "neo", 206) - if start <= 206 && (stop == 0 || stop >= 206) && (asset == "neo" || asset == "") { - require.NotNil(t, u) - require.EqualValues(t, int64(99999000), u.Amount) - } else { - require.Nil(t, u) - } - - u = getUTXOForBlock(res, false, "neo", 206) - if start <= 206 && (stop == 0 || stop >= 206) && (asset == "neo" || asset == "") { - require.NotNil(t, u) - require.EqualValues(t, int64(99998000), u.Amount) - } else { - require.Nil(t, u) + u := getUTXOForBlock(res, tr.sent, tr.asset, tr.index) + for j := range checked { + if checked[j] == i { + present = true + break + } + } + if present { + require.NotNil(t, u) + require.EqualValues(t, tr.amount, u.Amount) + } else { + require.Nil(t, u) + } } }