diff --git a/docs/rpc.md b/docs/rpc.md index 6d0b2f235..a16d81cee 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -101,6 +101,28 @@ 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 + +`getnep5transfers` RPC call never returns 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 NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc +within 0-1600094189 timestamps: + +```json +{ "jsonrpc": "2.0", "id": 5, "method": "getnep5transfers", "params": +["NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc", 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": +["NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc", 0, 1600094189, 10, 1] } +``` + #### Websocket server This server accepts websocket connections on `ws://$BASE_URL/ws` address. You diff --git a/pkg/consensus/consensus.go b/pkg/consensus/consensus.go index 1909397e4..9d5638eca 100644 --- a/pkg/consensus/consensus.go +++ b/pkg/consensus/consensus.go @@ -133,6 +133,19 @@ func NewService(cfg Config) (Service, error) { return nil, err } + // Check that wallet password is correct for at least one account. + var ok bool + for _, acc := range srv.wallet.Accounts { + err := acc.Decrypt(srv.Config.Wallet.Password) + if err == nil { + ok = true + break + } + } + if !ok { + return nil, errors.New("no account with provided password was found") + } + defer srv.wallet.Close() srv.dbft = dbft.New( diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index b6582c6a4..0e07669ee 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -784,20 +784,23 @@ func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, h util.Uint256, b * } // ForEachNEP5Transfer executes f for each nep5 transfer in log. -func (bc *Blockchain) ForEachNEP5Transfer(acc util.Uint160, f func(*state.NEP5Transfer) error) error { +func (bc *Blockchain) ForEachNEP5Transfer(acc util.Uint160, f func(*state.NEP5Transfer) (bool, error)) error { balances, err := bc.dao.GetNEP5Balances(acc) if err != nil { return nil } - for i := uint32(0); i <= balances.NextTransferBatch; i++ { - lg, err := bc.dao.GetNEP5TransferLog(acc, i) + for i := int(balances.NextTransferBatch); i >= 0; i-- { + lg, err := bc.dao.GetNEP5TransferLog(acc, uint32(i)) if err != nil { return nil } - err = lg.ForEach(f) + cont, err := lg.ForEach(f) if err != nil { return err } + if !cont { + break + } } return nil } diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 2fae4c2b5..b02ff1316 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -31,7 +31,7 @@ type Blockchainer interface { GetContractScriptHash(id int32) (util.Uint160, error) GetEnrollments() ([]state.Validator, error) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) - ForEachNEP5Transfer(util.Uint160, func(*state.NEP5Transfer) error) error + ForEachNEP5Transfer(util.Uint160, func(*state.NEP5Transfer) (bool, error)) error GetHeaderHash(int) util.Uint256 GetHeader(hash util.Uint256) (*block.Header, error) CurrentHeaderHash() util.Uint256 diff --git a/pkg/core/dao/cacheddao.go b/pkg/core/dao/cacheddao.go index 8c8bf1aa6..9cfa8992e 100644 --- a/pkg/core/dao/cacheddao.go +++ b/pkg/core/dao/cacheddao.go @@ -95,7 +95,7 @@ func (cd *Cached) AppendNEP5Transfer(acc util.Uint160, index uint32, tr *state.N if err := lg.Append(tr); err != nil { return false, err } - return lg.Size() >= nep5TransferBatchSize, cd.PutNEP5TransferLog(acc, index, lg) + return lg.Size() >= state.NEP5TransferBatchSize, cd.PutNEP5TransferLog(acc, index, lg) } // Persist flushes all the changes made into the (supposedly) persistent diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index af746cef4..f4fa5050d 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -208,8 +208,6 @@ func (dao *Simple) putNEP5Balances(acc util.Uint160, bs *state.NEP5Balances, buf // -- start transfer log. -const nep5TransferBatchSize = 128 - func getNEP5TransferLogKey(acc util.Uint160, index uint32) []byte { key := make([]byte, 1+util.Uint160Size+4) key[0] = byte(storage.STNEP5Transfers) @@ -250,7 +248,7 @@ func (dao *Simple) AppendNEP5Transfer(acc util.Uint160, index uint32, tr *state. if err := lg.Append(tr); err != nil { return false, err } - return lg.Size() >= nep5TransferBatchSize, dao.PutNEP5TransferLog(acc, index, lg) + return lg.Size() >= state.NEP5TransferBatchSize, dao.PutNEP5TransferLog(acc, index, lg) } // -- end transfer log. diff --git a/pkg/core/mpt/hash.go b/pkg/core/mpt/hash.go index 42519a1ac..565f720ca 100644 --- a/pkg/core/mpt/hash.go +++ b/pkg/core/mpt/hash.go @@ -36,7 +36,7 @@ func (h *HashNode) Hash() util.Uint256 { return h.hash } -// IsEmpty returns true iff h is an empty node i.e. contains no hash. +// IsEmpty returns true if h is an empty node i.e. contains no hash. func (h *HashNode) IsEmpty() bool { return !h.hashValid } // Bytes returns serialized HashNode. diff --git a/pkg/core/state/nep5.go b/pkg/core/state/nep5.go index b3dc38715..84fcb07b6 100644 --- a/pkg/core/state/nep5.go +++ b/pkg/core/state/nep5.go @@ -8,6 +8,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/util" ) +// NEP5TransferBatchSize is the maximum number of entries for NEP5TransferLog. +const NEP5TransferBatchSize = 128 + // NEP5Tracker contains info about a single account in a NEP5 contract. type NEP5Tracker struct { // Balance is the current balance of the account. @@ -20,8 +23,6 @@ type NEP5Tracker struct { // NEP5TransferLog is a log of NEP5 token transfers for the specific command. type NEP5TransferLog struct { Raw []byte - // size is the number of NEP5Transfers written into Raw - size int } // NEP5Transfer represents a single NEP5 Transfer event. @@ -85,37 +86,52 @@ func (bs *NEP5Balances) EncodeBinary(w *io.BinWriter) { // Append appends single transfer to a log. func (lg *NEP5TransferLog) Append(tr *NEP5Transfer) error { w := io.NewBufBinWriter() + // The first entry, set up counter. + if len(lg.Raw) == 0 { + w.WriteB(1) + } tr.EncodeBinary(w.BinWriter) if w.Err != nil { return w.Err } + if len(lg.Raw) != 0 { + lg.Raw[0]++ + } lg.Raw = append(lg.Raw, w.Bytes()...) - lg.size++ return nil } // ForEach iterates over transfer log returning on first error. -func (lg *NEP5TransferLog) ForEach(f func(*NEP5Transfer) error) error { - if lg == nil { - return nil +func (lg *NEP5TransferLog) ForEach(f func(*NEP5Transfer) (bool, error)) (bool, error) { + if lg == nil || len(lg.Raw) == 0 { + return true, nil } - tr := new(NEP5Transfer) - var bytesRead int - for i := 0; i < len(lg.Raw); i += bytesRead { - r := io.NewBinReaderFromBuf(lg.Raw[i:]) - bytesRead = tr.DecodeBinaryReturnCount(r) - if r.Err != nil { - return r.Err - } else if err := f(tr); err != nil { - return nil + transfers := make([]NEP5Transfer, lg.Size()) + r := io.NewBinReaderFromBuf(lg.Raw[1:]) + for i := 0; i < lg.Size(); i++ { + transfers[i].DecodeBinary(r) + } + if r.Err != nil { + return false, r.Err + } + for i := len(transfers) - 1; i >= 0; i-- { + cont, err := f(&transfers[i]) + if err != nil { + return false, err + } + if !cont { + return false, nil } } - return nil + return true, nil } // Size returns an amount of transfer written in log. func (lg *NEP5TransferLog) Size() int { - return lg.size + if len(lg.Raw) == 0 { + return 0 + } + return int(lg.Raw[0]) } // EncodeBinary implements io.Serializable interface. @@ -138,27 +154,18 @@ func (t *NEP5Transfer) EncodeBinary(w *io.BinWriter) { w.WriteBytes(t.To[:]) w.WriteU32LE(t.Block) w.WriteU64LE(t.Timestamp) - amountBytes := bigint.ToBytes(&t.Amount) - w.WriteU64LE(uint64(len(amountBytes))) - w.WriteBytes(amountBytes) + amount := bigint.ToBytes(&t.Amount) + w.WriteVarBytes(amount) } // DecodeBinary implements io.Serializable interface. func (t *NEP5Transfer) DecodeBinary(r *io.BinReader) { - _ = t.DecodeBinaryReturnCount(r) -} - -// DecodeBinaryReturnCount decodes NEP5Transfer and returns the number of bytes read. -func (t *NEP5Transfer) DecodeBinaryReturnCount(r *io.BinReader) int { t.Asset = int32(r.ReadU32LE()) r.ReadBytes(t.Tx[:]) r.ReadBytes(t.From[:]) r.ReadBytes(t.To[:]) t.Block = r.ReadU32LE() t.Timestamp = r.ReadU64LE() - amountLen := r.ReadU64LE() - amountBytes := make([]byte, amountLen) - r.ReadBytes(amountBytes) - t.Amount = *bigint.FromBytes(amountBytes) - return 4 + util.Uint160Size*2 + 8 + 4 + (8 + len(amountBytes)) + +util.Uint256Size + amount := r.ReadVarBytes(bigint.MaxBytesLen) + t.Amount = *bigint.FromBytes(amount) } diff --git a/pkg/core/state/nep5_test.go b/pkg/core/state/nep5_test.go index 507e848c0..41734c58c 100644 --- a/pkg/core/state/nep5_test.go +++ b/pkg/core/state/nep5_test.go @@ -8,7 +8,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/internal/random" "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/stretchr/testify/require" ) @@ -29,14 +28,14 @@ func TestNEP5TransferLog_Append(t *testing.T) { require.Equal(t, len(expected), lg.Size()) - i := 0 - err := lg.ForEach(func(tr *NEP5Transfer) error { + i := len(expected) - 1 + cont, err := lg.ForEach(func(tr *NEP5Transfer) (bool, error) { require.Equal(t, expected[i], tr) - i++ - return nil + i-- + return true, nil }) require.NoError(t, err) - + require.True(t, cont) } func TestNEP5Tracker_EncodeBinary(t *testing.T) { @@ -62,18 +61,6 @@ func TestNEP5Transfer_DecodeBinary(t *testing.T) { testserdes.EncodeDecodeBinary(t, expected, new(NEP5Transfer)) } -func TestNEP5TransferSize(t *testing.T) { - tr := randomTransfer(rand.New(rand.NewSource(0))) - size := io.GetVarSize(tr) - w := io.NewBufBinWriter() - tr.EncodeBinary(w.BinWriter) - require.NoError(t, w.Err) - r := io.NewBinReaderFromBuf(w.Bytes()) - actualTr := &NEP5Transfer{} - actual := actualTr.DecodeBinaryReturnCount(r) - require.EqualValues(t, actual, size) -} - func randomTransfer(r *rand.Rand) *NEP5Transfer { return &NEP5Transfer{ Amount: *big.NewInt(int64(r.Uint64())), diff --git a/pkg/encoding/bigint/bigint.go b/pkg/encoding/bigint/bigint.go index 33cdc00c2..9b77fae65 100644 --- a/pkg/encoding/bigint/bigint.go +++ b/pkg/encoding/bigint/bigint.go @@ -6,8 +6,12 @@ import ( "math/bits" ) -// wordSizeBytes is a size of a big.Word (uint) in bytes.` -const wordSizeBytes = bits.UintSize / 8 +const ( + // MaxBytesLen is the maximum length of serialized integer suitable for Neo VM. + MaxBytesLen = 33 // 32 bytes for 256-bit integer plus 1 if padding needed + // wordSizeBytes is a size of a big.Word (uint) in bytes.` + wordSizeBytes = bits.UintSize / 8 +) // FromBytes converts data in little-endian format to // an integer. diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index d8c41f64c..38428daea 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -95,7 +95,7 @@ func (chain testChain) GetHeader(hash util.Uint256) (*block.Header, error) { func (chain testChain) GetNextBlockValidators() ([]*keys.PublicKey, error) { panic("TODO") } -func (chain testChain) ForEachNEP5Transfer(util.Uint160, func(*state.NEP5Transfer) error) error { +func (chain testChain) ForEachNEP5Transfer(util.Uint160, func(*state.NEP5Transfer) (bool, error)) error { panic("TODO") } func (chain testChain) GetNEP5Balances(util.Uint160) *state.NEP5Balances { diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index d7039c2ec..47c3c5219 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -214,9 +214,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 diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 9817e03ab..617fd32b1 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -420,7 +420,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,"assethash":"600c4f5200db36177e3e8a09e9f18e2fc7d12a0f","transferaddress":"AYwgBNMepiv5ocGcyNT4mA8zPLTQ8pDBis","amount":"1000000","blockindex":436036,"transfernotifyindex":0,"txhash":"df7683ece554ecfb85cf41492c5f143215dd43ef9ec61181a28f922da06aba58"}],"address":"AbHgdBaWEnHkCiLtDZXjhvhaAK2cwFh5pF"}}`, result: func(c *Client) interface{} { @@ -948,7 +948,30 @@ 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) + }, + }, + { + name: "getnep5transfers_invalid_params_error 2", + invoke: func(c *Client) (interface{}, error) { + var stop uint32 + return c.GetNEP5Transfers("NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc", nil, &stop, nil, nil) + }, + }, + { + name: "getnep5transfers_invalid_params_error 3", + invoke: func(c *Client) (interface{}, error) { + var start uint32 + var limit int + return c.GetNEP5Transfers("NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc", &start, nil, &limit, nil) + }, + }, + { + name: "getnep5transfers_invalid_params_error 4", + invoke: func(c *Client) (interface{}, error) { + var start, stop uint32 + var page int + return c.GetNEP5Transfers("NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc", &start, &stop, nil, &page) }, }, { @@ -1112,7 +1135,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/server/server.go b/pkg/rpc/server/server.go index d9708b289..50de230ac 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -77,6 +77,9 @@ 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){ @@ -545,23 +548,54 @@ func (s *Server) getNEP5Balances(ps request.Params) (interface{}, *response.Erro return bs, nil } -func getTimestamps(p1, p2 *request.Param) (uint64, uint64, error) { +func getTimestampsAndLimit(ps request.Params, index int) (uint64, uint64, int, int, error) { var start, end uint64 - 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, err + return 0, 0, 0, 0, err } - start = uint64(val) + if p < 0 { + return 0, 0, 0, 0, errors.New("can't use negative page") + } + page = p } - if p2 != nil { - val, err := p2.GetInt() + if pLimit != nil { + l, err := pLimit.GetInt() if err != nil { - return 0, 0, err + return 0, 0, 0, 0, err + } + if l <= 0 { + 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 + } + if pEnd != nil { + val, err := pEnd.GetInt() + if err != nil { + return 0, 0, 0, 0, err } end = uint64(val) + } else { + end = uint64(time.Now().Unix() * 1000) } - return start, end, nil + if pStart != nil { + val, err := pStart.GetInt() + if err != nil { + return 0, 0, 0, 0, err + } + start = uint64(val) + } else { + start = uint64(time.Now().Add(-time.Hour*24*7).Unix() * 1000) + } + return start, end, limit, page, nil } func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Error) { @@ -570,17 +604,10 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err return nil, response.ErrInvalidParams } - p1, p2 := ps.Value(1), ps.Value(2) - start, end, err := getTimestamps(p1, p2) + start, end, limit, page, err := getTimestampsAndLimit(ps, 1) if err != nil { return nil, response.NewInvalidParamsError(err.Error(), err) } - if p2 == nil { - end = uint64(time.Now().Unix() * 1000) - if p1 == nil { - start = uint64(time.Now().Add(-time.Hour*24*7).Unix() * 1000) - } - } bs := &result.NEP5Transfers{ Address: address.Uint160ToString(u), @@ -588,14 +615,29 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err Sent: []result.NEP5Transfer{}, } cache := make(map[int32]decimals) - err = s.chain.ForEachNEP5Transfer(u, func(tr *state.NEP5Transfer) error { - if tr.Timestamp < start || tr.Timestamp > end { - return nil + var resCount, frameCount int + err = s.chain.ForEachNEP5Transfer(u, func(tr *state.NEP5Transfer) (bool, error) { + // 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 + } + frameCount++ + // Using limits, not yet reached required page. + if limit != 0 && page*limit >= frameCount { + return true, nil + } + d, err := s.getDecimals(tr.Asset, cache) if err != nil { - return nil + return false, err } + transfer := result.NEP5Transfer{ Timestamp: tr.Timestamp, Asset: d.Hash, @@ -608,15 +650,20 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err transfer.Address = address.Uint160ToString(tr.From) } bs.Received = append(bs.Received, transfer) - return nil + } else { + transfer.Amount = amountToString(new(big.Int).Neg(&tr.Amount), d.Value) + if !tr.To.Equals(util.Uint160{}) { + transfer.Address = address.Uint160ToString(tr.To) + } + bs.Sent = append(bs.Sent, transfer) } - transfer.Amount = amountToString(new(big.Int).Neg(&tr.Amount), d.Value) - 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 nil + return true, nil }) if err != nil { return nil, response.NewInternalServerError("invalid NEP5 transfer log", err) diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 295de0640..cb1df850a 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -160,6 +160,36 @@ var rpcTestCases = map[string][]rpcTestCase{ params: `["` + testchain.PrivateKeyByID(0).Address() + `", "notanumber"]`, fail: true, }, + { + name: "invalid stop timestamp", + params: `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "blah"]`, + fail: true, + }, + { + name: "invalid limit", + params: `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "2", "0"]`, + fail: true, + }, + { + name: "invalid limit 2", + params: `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "2", "bleh"]`, + fail: true, + }, + { + name: "invalid limit 3", + params: `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "2", "100500"]`, + fail: true, + }, + { + name: "invalid page", + params: `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "2", "3", "-1"]`, + fail: true, + }, + { + name: "invalid page 2", + params: `["` + testchain.PrivateKeyByID(0).Address() + `", "1", "2", "3", "jajaja"]`, + fail: true, + }, { name: "positive", params: `["` + testchain.PrivateKeyByID(0).Address() + `", 0]`, @@ -914,21 +944,48 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] }) t.Run("getnep5transfers", func(t *testing.T) { - ps := []string{`"` + testchain.PrivateKeyByID(0).Address() + `"`} - h, err := e.chain.GetHeader(e.chain.GetHeaderHash(4)) - require.NoError(t, err) - ps = append(ps, strconv.FormatUint(h.Timestamp, 10)) - h, err = e.chain.GetHeader(e.chain.GetHeaderHash(5)) - require.NoError(t, err) - ps = append(ps, strconv.FormatUint(h.Timestamp, 10)) - - p := strings.Join(ps, ", ") - rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getnep5transfers", "params": [%s]}`, p) - body := doRPCCall(rpc, httpSrv.URL, t) - res := checkErrGetResult(t, body, false) - actual := new(result.NEP5Transfers) - require.NoError(t, json.Unmarshal(res, actual)) - checkNep5TransfersAux(t, e, actual, 4, 5) + testNEP5T := func(t *testing.T, start, stop, limit, page int, sent, rcvd []int) { + ps := []string{`"` + testchain.PrivateKeyByID(0).Address() + `"`} + if start != 0 { + h, err := e.chain.GetHeader(e.chain.GetHeaderHash(start)) + var ts uint64 + if err == nil { + ts = h.Timestamp + } else { + ts = uint64(time.Now().UnixNano() / 1_000_000) + } + ps = append(ps, strconv.FormatUint(ts, 10)) + } + if stop != 0 { + h, err := e.chain.GetHeader(e.chain.GetHeaderHash(stop)) + var ts uint64 + if err == nil { + ts = h.Timestamp + } else { + ts = uint64(time.Now().UnixNano() / 1_000_000) + } + ps = append(ps, strconv.FormatUint(ts, 10)) + } + if limit != 0 { + ps = append(ps, strconv.FormatInt(int64(limit), 10)) + } + if page != 0 { + ps = append(ps, strconv.FormatInt(int64(page), 10)) + } + p := strings.Join(ps, ", ") + rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getnep5transfers", "params": [%s]}`, p) + body := doRPCCall(rpc, httpSrv.URL, t) + res := checkErrGetResult(t, body, false) + actual := new(result.NEP5Transfers) + require.NoError(t, json.Unmarshal(res, actual)) + checkNep5TransfersAux(t, e, actual, sent, rcvd) + } + t.Run("time frame only", func(t *testing.T) { testNEP5T(t, 4, 5, 0, 0, []int{3, 4, 5, 6}, []int{0, 1}) }) + t.Run("no res", func(t *testing.T) { testNEP5T(t, 100, 100, 0, 0, []int{}, []int{}) }) + t.Run("limit", func(t *testing.T) { testNEP5T(t, 1, 7, 3, 0, []int{0, 1, 2}, []int{}) }) + t.Run("limit 2", func(t *testing.T) { testNEP5T(t, 4, 5, 2, 0, []int{3}, []int{0}) }) + t.Run("limit with page", func(t *testing.T) { testNEP5T(t, 1, 7, 3, 1, []int{3, 4}, []int{0}) }) + t.Run("limit with page 2", func(t *testing.T) { testNEP5T(t, 1, 7, 3, 2, []int{5, 6}, []int{1}) }) }) } @@ -1028,41 +1085,66 @@ func checkNep5Balances(t *testing.T, e *executor, acc interface{}) { } func checkNep5Transfers(t *testing.T, e *executor, acc interface{}) { - checkNep5TransfersAux(t, e, acc, 0, e.chain.HeaderHeight()) + checkNep5TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}, []int{0, 1, 2, 3}) } -func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, start, end uint32) { +func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) { res, ok := acc.(*result.NEP5Transfers) require.True(t, ok) rublesHash, err := util.Uint160DecodeStringLE(testContractHash) require.NoError(t, err) + + blockDeploy2, err := e.chain.GetBlock(e.chain.GetHeaderHash(7)) + require.NoError(t, err) + require.Equal(t, 1, len(blockDeploy2.Transactions)) + txDeploy2 := blockDeploy2.Transactions[0] + blockSendRubles, err := e.chain.GetBlock(e.chain.GetHeaderHash(6)) require.NoError(t, err) require.Equal(t, 1, len(blockSendRubles.Transactions)) - txSendRublesHash := blockSendRubles.Transactions[0].Hash() + txSendRubles := blockSendRubles.Transactions[0] + blockReceiveRubles, err := e.chain.GetBlock(e.chain.GetHeaderHash(5)) require.NoError(t, err) require.Equal(t, 2, len(blockReceiveRubles.Transactions)) - txReceiveRublesHash := blockReceiveRubles.Transactions[1].Hash() - blockReceiveGAS, err := e.chain.GetBlock(e.chain.GetHeaderHash(1)) - require.NoError(t, err) - require.Equal(t, 2, len(blockReceiveGAS.Transactions)) - txReceiveNEOHash := blockReceiveGAS.Transactions[0].Hash() - txReceiveGASHash := blockReceiveGAS.Transactions[1].Hash() + txInitCall := blockReceiveRubles.Transactions[0] + txReceiveRubles := blockReceiveRubles.Transactions[1] + blockSendNEO, err := e.chain.GetBlock(e.chain.GetHeaderHash(4)) require.NoError(t, err) require.Equal(t, 1, len(blockSendNEO.Transactions)) - txSendNEOHash := blockSendNEO.Transactions[0].Hash() + txSendNEO := blockSendNEO.Transactions[0] + + blockCtrInv1, err := e.chain.GetBlock(e.chain.GetHeaderHash(3)) + require.NoError(t, err) + require.Equal(t, 1, len(blockCtrInv1.Transactions)) + txCtrInv1 := blockCtrInv1.Transactions[0] + + blockCtrDeploy, err := e.chain.GetBlock(e.chain.GetHeaderHash(2)) + require.NoError(t, err) + require.Equal(t, 1, len(blockCtrDeploy.Transactions)) + txCtrDeploy := blockCtrDeploy.Transactions[0] + + blockReceiveGAS, err := e.chain.GetBlock(e.chain.GetHeaderHash(1)) + require.NoError(t, err) + require.Equal(t, 2, len(blockReceiveGAS.Transactions)) + txReceiveNEO := blockReceiveGAS.Transactions[0] + txReceiveGAS := blockReceiveGAS.Transactions[1] + + // These are laid out here explicitly for 2 purposes: + // * to be able to reference any particular event for paging + // * to check chain events consistency + // Technically these could be retrieved from application log, but that would almost + // duplicate the Server method. expected := result.NEP5Transfers{ Sent: []result.NEP5Transfer{ { - Timestamp: blockSendNEO.Timestamp, - Asset: e.chain.GoverningTokenHash(), - Address: testchain.PrivateKeyByID(1).Address(), - Amount: "1000", - Index: 4, - NotifyIndex: 0, - TxHash: txSendNEOHash, + Timestamp: blockDeploy2.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: amountToString(big.NewInt(txDeploy2.SystemFee+txDeploy2.NetworkFee), 8), + Index: 7, + TxHash: blockDeploy2.Hash(), }, { Timestamp: blockSendRubles.Timestamp, @@ -1071,7 +1153,64 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, start, en Amount: "1.23", Index: 6, NotifyIndex: 0, - TxHash: txSendRublesHash, + TxHash: txSendRubles.Hash(), + }, + { + Timestamp: blockSendRubles.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: amountToString(big.NewInt(txSendRubles.SystemFee+txSendRubles.NetworkFee), 8), + Index: 6, + TxHash: blockSendRubles.Hash(), + }, + { + Timestamp: blockReceiveRubles.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: amountToString(big.NewInt(txReceiveRubles.SystemFee+txReceiveRubles.NetworkFee), 8), + Index: 5, + TxHash: blockReceiveRubles.Hash(), + }, + { + Timestamp: blockReceiveRubles.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: amountToString(big.NewInt(txInitCall.SystemFee+txInitCall.NetworkFee), 8), + Index: 5, + TxHash: blockReceiveRubles.Hash(), + }, + { + Timestamp: blockSendNEO.Timestamp, + Asset: e.chain.GoverningTokenHash(), + Address: testchain.PrivateKeyByID(1).Address(), + Amount: "1000", + Index: 4, + NotifyIndex: 0, + TxHash: txSendNEO.Hash(), + }, + { + Timestamp: blockSendNEO.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: amountToString(big.NewInt(txSendNEO.SystemFee+txSendNEO.NetworkFee), 8), + Index: 4, + TxHash: blockSendNEO.Hash(), + }, + { + Timestamp: blockCtrInv1.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn has empty receiver + Amount: amountToString(big.NewInt(txCtrInv1.SystemFee+txCtrInv1.NetworkFee), 8), + Index: 3, + TxHash: blockCtrInv1.Hash(), + }, + { + Timestamp: blockCtrDeploy.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn has empty receiver + Amount: amountToString(big.NewInt(txCtrDeploy.SystemFee+txCtrDeploy.NetworkFee), 8), + Index: 2, + TxHash: blockCtrDeploy.Hash(), }, }, Received: []result.NEP5Transfer{ @@ -1082,7 +1221,7 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, start, en Amount: "10", Index: 5, NotifyIndex: 0, - TxHash: txReceiveRublesHash, + TxHash: txReceiveRubles.Hash(), }, { Timestamp: blockSendNEO.Timestamp, @@ -1091,7 +1230,7 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, start, en Amount: "17.99982000", Index: 4, NotifyIndex: 0, - TxHash: txSendNEOHash, + TxHash: txSendNEO.Hash(), }, { Timestamp: blockReceiveGAS.Timestamp, @@ -1100,7 +1239,7 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, start, en Amount: "1000", Index: 1, NotifyIndex: 0, - TxHash: txReceiveGASHash, + TxHash: txReceiveGAS.Hash(), }, { Timestamp: blockReceiveGAS.Timestamp, @@ -1109,49 +1248,33 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, start, en Amount: "99999000", Index: 1, NotifyIndex: 0, - TxHash: txReceiveNEOHash, + TxHash: txReceiveNEO.Hash(), }, }, Address: testchain.PrivateKeyByID(0).Address(), } - // take burned gas into account - u := testchain.PrivateKeyByID(0).GetScriptHash() - for i := 0; i <= int(e.chain.BlockHeight()); i++ { - var netFee int64 - h := e.chain.GetHeaderHash(i) - b, err := e.chain.GetBlock(h) - require.NoError(t, err) - for j := range b.Transactions { - if u.Equals(b.Transactions[j].Sender()) { - amount := b.Transactions[j].SystemFee + b.Transactions[j].NetworkFee - expected.Sent = append(expected.Sent, result.NEP5Transfer{ - Timestamp: b.Timestamp, - Asset: e.chain.UtilityTokenHash(), - Address: "", // burn has empty receiver - Amount: amountToString(big.NewInt(amount), 8), - Index: b.Index, - TxHash: b.Hash(), - }) - } - netFee += b.Transactions[j].NetworkFee - } - } require.Equal(t, expected.Address, res.Address) arr := make([]result.NEP5Transfer, 0, len(expected.Sent)) for i := range expected.Sent { - if expected.Sent[i].Index >= start && expected.Sent[i].Index <= end { - arr = append(arr, expected.Sent[i]) + for _, j := range sent { + if i == j { + arr = append(arr, expected.Sent[i]) + break + } } } - require.ElementsMatch(t, arr, res.Sent) + require.Equal(t, arr, res.Sent) arr = arr[:0] for i := range expected.Received { - if expected.Received[i].Index >= start && expected.Received[i].Index <= end { - arr = append(arr, expected.Received[i]) + for _, j := range rcvd { + if i == j { + arr = append(arr, expected.Received[i]) + break + } } } - require.ElementsMatch(t, arr, res.Received) + require.Equal(t, arr, res.Received) }