Merge pull request #1419 from nspcc-dev/port-from-2.x

Port from 2.x
This commit is contained in:
Roman Khimov 2020-09-22 21:56:29 +03:00 committed by GitHub
commit 1ff1cd797e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 407 additions and 158 deletions

View file

@ -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. 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 #### Websocket server
This server accepts websocket connections on `ws://$BASE_URL/ws` address. You This server accepts websocket connections on `ws://$BASE_URL/ws` address. You

View file

@ -133,6 +133,19 @@ func NewService(cfg Config) (Service, error) {
return nil, err 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() defer srv.wallet.Close()
srv.dbft = dbft.New( srv.dbft = dbft.New(

View file

@ -784,20 +784,23 @@ func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, h util.Uint256, b *
} }
// ForEachNEP5Transfer executes f for each nep5 transfer in log. // 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) balances, err := bc.dao.GetNEP5Balances(acc)
if err != nil { if err != nil {
return nil return nil
} }
for i := uint32(0); i <= balances.NextTransferBatch; i++ { for i := int(balances.NextTransferBatch); i >= 0; i-- {
lg, err := bc.dao.GetNEP5TransferLog(acc, i) lg, err := bc.dao.GetNEP5TransferLog(acc, uint32(i))
if err != nil { if err != nil {
return nil return nil
} }
err = lg.ForEach(f) cont, err := lg.ForEach(f)
if err != nil { if err != nil {
return err return err
} }
if !cont {
break
}
} }
return nil return nil
} }

View file

@ -31,7 +31,7 @@ type Blockchainer interface {
GetContractScriptHash(id int32) (util.Uint160, error) GetContractScriptHash(id int32) (util.Uint160, error)
GetEnrollments() ([]state.Validator, error) GetEnrollments() ([]state.Validator, error)
GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) 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 GetHeaderHash(int) util.Uint256
GetHeader(hash util.Uint256) (*block.Header, error) GetHeader(hash util.Uint256) (*block.Header, error)
CurrentHeaderHash() util.Uint256 CurrentHeaderHash() util.Uint256

View file

@ -95,7 +95,7 @@ func (cd *Cached) AppendNEP5Transfer(acc util.Uint160, index uint32, tr *state.N
if err := lg.Append(tr); err != nil { if err := lg.Append(tr); err != nil {
return false, err 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 // Persist flushes all the changes made into the (supposedly) persistent

View file

@ -208,8 +208,6 @@ func (dao *Simple) putNEP5Balances(acc util.Uint160, bs *state.NEP5Balances, buf
// -- start transfer log. // -- start transfer log.
const nep5TransferBatchSize = 128
func getNEP5TransferLogKey(acc util.Uint160, index uint32) []byte { func getNEP5TransferLogKey(acc util.Uint160, index uint32) []byte {
key := make([]byte, 1+util.Uint160Size+4) key := make([]byte, 1+util.Uint160Size+4)
key[0] = byte(storage.STNEP5Transfers) 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 { if err := lg.Append(tr); err != nil {
return false, err 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. // -- end transfer log.

View file

@ -36,7 +36,7 @@ func (h *HashNode) Hash() util.Uint256 {
return h.hash 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 } func (h *HashNode) IsEmpty() bool { return !h.hashValid }
// Bytes returns serialized HashNode. // Bytes returns serialized HashNode.

View file

@ -8,6 +8,9 @@ import (
"github.com/nspcc-dev/neo-go/pkg/util" "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. // NEP5Tracker contains info about a single account in a NEP5 contract.
type NEP5Tracker struct { type NEP5Tracker struct {
// Balance is the current balance of the account. // 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. // NEP5TransferLog is a log of NEP5 token transfers for the specific command.
type NEP5TransferLog struct { type NEP5TransferLog struct {
Raw []byte Raw []byte
// size is the number of NEP5Transfers written into Raw
size int
} }
// NEP5Transfer represents a single NEP5 Transfer event. // 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. // Append appends single transfer to a log.
func (lg *NEP5TransferLog) Append(tr *NEP5Transfer) error { func (lg *NEP5TransferLog) Append(tr *NEP5Transfer) error {
w := io.NewBufBinWriter() w := io.NewBufBinWriter()
// The first entry, set up counter.
if len(lg.Raw) == 0 {
w.WriteB(1)
}
tr.EncodeBinary(w.BinWriter) tr.EncodeBinary(w.BinWriter)
if w.Err != nil { if w.Err != nil {
return w.Err return w.Err
} }
if len(lg.Raw) != 0 {
lg.Raw[0]++
}
lg.Raw = append(lg.Raw, w.Bytes()...) lg.Raw = append(lg.Raw, w.Bytes()...)
lg.size++
return nil return nil
} }
// ForEach iterates over transfer log returning on first error. // ForEach iterates over transfer log returning on first error.
func (lg *NEP5TransferLog) ForEach(f func(*NEP5Transfer) error) error { func (lg *NEP5TransferLog) ForEach(f func(*NEP5Transfer) (bool, error)) (bool, error) {
if lg == nil { if lg == nil || len(lg.Raw) == 0 {
return nil return true, nil
} }
tr := new(NEP5Transfer) transfers := make([]NEP5Transfer, lg.Size())
var bytesRead int r := io.NewBinReaderFromBuf(lg.Raw[1:])
for i := 0; i < len(lg.Raw); i += bytesRead { for i := 0; i < lg.Size(); i++ {
r := io.NewBinReaderFromBuf(lg.Raw[i:]) transfers[i].DecodeBinary(r)
bytesRead = tr.DecodeBinaryReturnCount(r) }
if r.Err != nil { if r.Err != nil {
return r.Err return false, r.Err
} else if err := f(tr); err != nil { }
return nil 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. // Size returns an amount of transfer written in log.
func (lg *NEP5TransferLog) Size() int { 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. // EncodeBinary implements io.Serializable interface.
@ -138,27 +154,18 @@ func (t *NEP5Transfer) EncodeBinary(w *io.BinWriter) {
w.WriteBytes(t.To[:]) w.WriteBytes(t.To[:])
w.WriteU32LE(t.Block) w.WriteU32LE(t.Block)
w.WriteU64LE(t.Timestamp) w.WriteU64LE(t.Timestamp)
amountBytes := bigint.ToBytes(&t.Amount) amount := bigint.ToBytes(&t.Amount)
w.WriteU64LE(uint64(len(amountBytes))) w.WriteVarBytes(amount)
w.WriteBytes(amountBytes)
} }
// DecodeBinary implements io.Serializable interface. // DecodeBinary implements io.Serializable interface.
func (t *NEP5Transfer) DecodeBinary(r *io.BinReader) { 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()) t.Asset = int32(r.ReadU32LE())
r.ReadBytes(t.Tx[:]) r.ReadBytes(t.Tx[:])
r.ReadBytes(t.From[:]) r.ReadBytes(t.From[:])
r.ReadBytes(t.To[:]) r.ReadBytes(t.To[:])
t.Block = r.ReadU32LE() t.Block = r.ReadU32LE()
t.Timestamp = r.ReadU64LE() t.Timestamp = r.ReadU64LE()
amountLen := r.ReadU64LE() amount := r.ReadVarBytes(bigint.MaxBytesLen)
amountBytes := make([]byte, amountLen) t.Amount = *bigint.FromBytes(amount)
r.ReadBytes(amountBytes)
t.Amount = *bigint.FromBytes(amountBytes)
return 4 + util.Uint160Size*2 + 8 + 4 + (8 + len(amountBytes)) + +util.Uint256Size
} }

View file

@ -8,7 +8,6 @@ import (
"github.com/nspcc-dev/neo-go/pkg/internal/random" "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/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -29,14 +28,14 @@ func TestNEP5TransferLog_Append(t *testing.T) {
require.Equal(t, len(expected), lg.Size()) require.Equal(t, len(expected), lg.Size())
i := 0 i := len(expected) - 1
err := lg.ForEach(func(tr *NEP5Transfer) error { cont, err := lg.ForEach(func(tr *NEP5Transfer) (bool, error) {
require.Equal(t, expected[i], tr) require.Equal(t, expected[i], tr)
i++ i--
return nil return true, nil
}) })
require.NoError(t, err) require.NoError(t, err)
require.True(t, cont)
} }
func TestNEP5Tracker_EncodeBinary(t *testing.T) { func TestNEP5Tracker_EncodeBinary(t *testing.T) {
@ -62,18 +61,6 @@ func TestNEP5Transfer_DecodeBinary(t *testing.T) {
testserdes.EncodeDecodeBinary(t, expected, new(NEP5Transfer)) 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 { func randomTransfer(r *rand.Rand) *NEP5Transfer {
return &NEP5Transfer{ return &NEP5Transfer{
Amount: *big.NewInt(int64(r.Uint64())), Amount: *big.NewInt(int64(r.Uint64())),

View file

@ -6,8 +6,12 @@ import (
"math/bits" "math/bits"
) )
// wordSizeBytes is a size of a big.Word (uint) in bytes.` const (
const wordSizeBytes = bits.UintSize / 8 // 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 // FromBytes converts data in little-endian format to
// an integer. // an integer.

View file

@ -95,7 +95,7 @@ func (chain testChain) GetHeader(hash util.Uint256) (*block.Header, error) {
func (chain testChain) GetNextBlockValidators() ([]*keys.PublicKey, error) { func (chain testChain) GetNextBlockValidators() ([]*keys.PublicKey, error) {
panic("TODO") 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") panic("TODO")
} }
func (chain testChain) GetNEP5Balances(util.Uint160) *state.NEP5Balances { func (chain testChain) GetNEP5Balances(util.Uint160) *state.NEP5Balances {

View file

@ -214,9 +214,31 @@ func (c *Client) GetNEP5Balances(address util.Uint160) (*result.NEP5Balances, er
return resp, nil return resp, nil
} }
// GetNEP5Transfers is a wrapper for getnep5transfers RPC. // GetNEP5Transfers is a wrapper for getnep5transfers RPC. Address parameter
func (c *Client) GetNEP5Transfers(address string) (*result.NEP5Transfers, error) { // 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) 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) resp := new(result.NEP5Transfers)
if err := c.performRequest("getnep5transfers", params, resp); err != nil { if err := c.performRequest("getnep5transfers", params, resp); err != nil {
return nil, err return nil, err

View file

@ -420,7 +420,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
{ {
name: "positive", name: "positive",
invoke: func(c *Client) (interface{}, error) { 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"}}`, 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{} { result: func(c *Client) interface{} {
@ -948,7 +948,30 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{
{ {
name: "getnep5transfers_invalid_params_error", name: "getnep5transfers_invalid_params_error",
invoke: func(c *Client) (interface{}, 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", name: "getnep5transfers_unmarshalling_error",
invoke: func(c *Client) (interface{}, error) { invoke: func(c *Client) (interface{}, error) {
return c.GetNEP5Transfers("") return c.GetNEP5Transfers("", nil, nil, nil, nil)
}, },
}, },
{ {

View file

@ -77,6 +77,9 @@ const (
// treated like subscriber, so technically it's a limit on websocket // treated like subscriber, so technically it's a limit on websocket
// connections. // connections.
maxSubscribers = 64 maxSubscribers = 64
// Maximum number of elements for get*transfers requests.
maxTransfersLimit = 1000
) )
var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){ 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 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 var start, end uint64
if p1 != nil { var limit, page int
val, err := p1.GetInt()
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 { 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 { if pLimit != nil {
val, err := p2.GetInt() l, err := pLimit.GetInt()
if err != nil { 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) 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) { 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 return nil, response.ErrInvalidParams
} }
p1, p2 := ps.Value(1), ps.Value(2) start, end, limit, page, err := getTimestampsAndLimit(ps, 1)
start, end, err := getTimestamps(p1, p2)
if err != nil { if err != nil {
return nil, response.NewInvalidParamsError(err.Error(), err) 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{ bs := &result.NEP5Transfers{
Address: address.Uint160ToString(u), Address: address.Uint160ToString(u),
@ -588,14 +615,29 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err
Sent: []result.NEP5Transfer{}, Sent: []result.NEP5Transfer{},
} }
cache := make(map[int32]decimals) cache := make(map[int32]decimals)
err = s.chain.ForEachNEP5Transfer(u, func(tr *state.NEP5Transfer) error { var resCount, frameCount int
if tr.Timestamp < start || tr.Timestamp > end { err = s.chain.ForEachNEP5Transfer(u, func(tr *state.NEP5Transfer) (bool, error) {
return nil // 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) d, err := s.getDecimals(tr.Asset, cache)
if err != nil { if err != nil {
return nil return false, err
} }
transfer := result.NEP5Transfer{ transfer := result.NEP5Transfer{
Timestamp: tr.Timestamp, Timestamp: tr.Timestamp,
Asset: d.Hash, Asset: d.Hash,
@ -608,15 +650,20 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err
transfer.Address = address.Uint160ToString(tr.From) transfer.Address = address.Uint160ToString(tr.From)
} }
bs.Received = append(bs.Received, transfer) 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) resCount++
if !tr.To.Equals(util.Uint160{}) { // Using limits, reached limit.
transfer.Address = address.Uint160ToString(tr.To) if limit != 0 && resCount >= limit {
return false, nil
} }
bs.Sent = append(bs.Sent, transfer) return true, nil
return nil
}) })
if err != nil { if err != nil {
return nil, response.NewInternalServerError("invalid NEP5 transfer log", err) return nil, response.NewInternalServerError("invalid NEP5 transfer log", err)

View file

@ -160,6 +160,36 @@ var rpcTestCases = map[string][]rpcTestCase{
params: `["` + testchain.PrivateKeyByID(0).Address() + `", "notanumber"]`, params: `["` + testchain.PrivateKeyByID(0).Address() + `", "notanumber"]`,
fail: true, 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", name: "positive",
params: `["` + testchain.PrivateKeyByID(0).Address() + `", 0]`, 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) { t.Run("getnep5transfers", func(t *testing.T) {
ps := []string{`"` + testchain.PrivateKeyByID(0).Address() + `"`} testNEP5T := func(t *testing.T, start, stop, limit, page int, sent, rcvd []int) {
h, err := e.chain.GetHeader(e.chain.GetHeaderHash(4)) ps := []string{`"` + testchain.PrivateKeyByID(0).Address() + `"`}
require.NoError(t, err) if start != 0 {
ps = append(ps, strconv.FormatUint(h.Timestamp, 10)) h, err := e.chain.GetHeader(e.chain.GetHeaderHash(start))
h, err = e.chain.GetHeader(e.chain.GetHeaderHash(5)) var ts uint64
require.NoError(t, err) if err == nil {
ps = append(ps, strconv.FormatUint(h.Timestamp, 10)) ts = h.Timestamp
} else {
p := strings.Join(ps, ", ") ts = uint64(time.Now().UnixNano() / 1_000_000)
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getnep5transfers", "params": [%s]}`, p) }
body := doRPCCall(rpc, httpSrv.URL, t) ps = append(ps, strconv.FormatUint(ts, 10))
res := checkErrGetResult(t, body, false) }
actual := new(result.NEP5Transfers) if stop != 0 {
require.NoError(t, json.Unmarshal(res, actual)) h, err := e.chain.GetHeader(e.chain.GetHeaderHash(stop))
checkNep5TransfersAux(t, e, actual, 4, 5) 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{}) { 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) res, ok := acc.(*result.NEP5Transfers)
require.True(t, ok) require.True(t, ok)
rublesHash, err := util.Uint160DecodeStringLE(testContractHash) rublesHash, err := util.Uint160DecodeStringLE(testContractHash)
require.NoError(t, err) 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)) blockSendRubles, err := e.chain.GetBlock(e.chain.GetHeaderHash(6))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(blockSendRubles.Transactions)) 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)) blockReceiveRubles, err := e.chain.GetBlock(e.chain.GetHeaderHash(5))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 2, len(blockReceiveRubles.Transactions)) require.Equal(t, 2, len(blockReceiveRubles.Transactions))
txReceiveRublesHash := blockReceiveRubles.Transactions[1].Hash() txInitCall := blockReceiveRubles.Transactions[0]
blockReceiveGAS, err := e.chain.GetBlock(e.chain.GetHeaderHash(1)) txReceiveRubles := blockReceiveRubles.Transactions[1]
require.NoError(t, err)
require.Equal(t, 2, len(blockReceiveGAS.Transactions))
txReceiveNEOHash := blockReceiveGAS.Transactions[0].Hash()
txReceiveGASHash := blockReceiveGAS.Transactions[1].Hash()
blockSendNEO, err := e.chain.GetBlock(e.chain.GetHeaderHash(4)) blockSendNEO, err := e.chain.GetBlock(e.chain.GetHeaderHash(4))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(blockSendNEO.Transactions)) 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{ expected := result.NEP5Transfers{
Sent: []result.NEP5Transfer{ Sent: []result.NEP5Transfer{
{ {
Timestamp: blockSendNEO.Timestamp, Timestamp: blockDeploy2.Timestamp,
Asset: e.chain.GoverningTokenHash(), Asset: e.chain.UtilityTokenHash(),
Address: testchain.PrivateKeyByID(1).Address(), Address: "", // burn
Amount: "1000", Amount: amountToString(big.NewInt(txDeploy2.SystemFee+txDeploy2.NetworkFee), 8),
Index: 4, Index: 7,
NotifyIndex: 0, TxHash: blockDeploy2.Hash(),
TxHash: txSendNEOHash,
}, },
{ {
Timestamp: blockSendRubles.Timestamp, Timestamp: blockSendRubles.Timestamp,
@ -1071,7 +1153,64 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, start, en
Amount: "1.23", Amount: "1.23",
Index: 6, Index: 6,
NotifyIndex: 0, 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{ Received: []result.NEP5Transfer{
@ -1082,7 +1221,7 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, start, en
Amount: "10", Amount: "10",
Index: 5, Index: 5,
NotifyIndex: 0, NotifyIndex: 0,
TxHash: txReceiveRublesHash, TxHash: txReceiveRubles.Hash(),
}, },
{ {
Timestamp: blockSendNEO.Timestamp, Timestamp: blockSendNEO.Timestamp,
@ -1091,7 +1230,7 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, start, en
Amount: "17.99982000", Amount: "17.99982000",
Index: 4, Index: 4,
NotifyIndex: 0, NotifyIndex: 0,
TxHash: txSendNEOHash, TxHash: txSendNEO.Hash(),
}, },
{ {
Timestamp: blockReceiveGAS.Timestamp, Timestamp: blockReceiveGAS.Timestamp,
@ -1100,7 +1239,7 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, start, en
Amount: "1000", Amount: "1000",
Index: 1, Index: 1,
NotifyIndex: 0, NotifyIndex: 0,
TxHash: txReceiveGASHash, TxHash: txReceiveGAS.Hash(),
}, },
{ {
Timestamp: blockReceiveGAS.Timestamp, Timestamp: blockReceiveGAS.Timestamp,
@ -1109,49 +1248,33 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, start, en
Amount: "99999000", Amount: "99999000",
Index: 1, Index: 1,
NotifyIndex: 0, NotifyIndex: 0,
TxHash: txReceiveNEOHash, TxHash: txReceiveNEO.Hash(),
}, },
}, },
Address: testchain.PrivateKeyByID(0).Address(), 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) require.Equal(t, expected.Address, res.Address)
arr := make([]result.NEP5Transfer, 0, len(expected.Sent)) arr := make([]result.NEP5Transfer, 0, len(expected.Sent))
for i := range expected.Sent { for i := range expected.Sent {
if expected.Sent[i].Index >= start && expected.Sent[i].Index <= end { for _, j := range sent {
arr = append(arr, expected.Sent[i]) 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] arr = arr[:0]
for i := range expected.Received { for i := range expected.Received {
if expected.Received[i].Index >= start && expected.Received[i].Index <= end { for _, j := range rcvd {
arr = append(arr, expected.Received[i]) if i == j {
arr = append(arr, expected.Received[i])
break
}
} }
} }
require.ElementsMatch(t, arr, res.Received) require.Equal(t, arr, res.Received)
} }