commit
1ff1cd797e
15 changed files with 407 additions and 158 deletions
22
docs/rpc.md
22
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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue