diff --git a/docs/rpc.md b/docs/rpc.md index 910464833..a32ef1cce 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -48,7 +48,7 @@ which would yield the response: | `getconnectioncount` | Yes | | `getcontractstate` | Yes | | `getnep5balances` | Yes | -| `getnep5transfers` | No (#498) | +| `getnep5transfers` | Yes | | `getpeers` | Yes | | `getrawmempool` | Yes | | `getrawtransaction` | Yes | diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index d1b6a4433..6c6118284 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -810,12 +810,21 @@ func (bc *Blockchain) processNEP5Transfer(cache *cachedDao, tx *transaction.Tran } transfer.Amount = amount - if err := cache.AppendNEP5Transfer(fromAddr, transfer); err != nil { + if err := cache.AppendNEP5Transfer(toAddr, transfer); err != nil { return } } } +// GetNEP5TransferLog returns NEP5 transfer log for the acc. +func (bc *Blockchain) GetNEP5TransferLog(acc util.Uint160) *state.NEP5TransferLog { + lg, err := bc.dao.GetNEP5TransferLog(acc) + if err != nil { + return nil + } + return lg +} + // LastBatch returns last persisted storage batch. func (bc *Blockchain) LastBatch() *storage.MemBatch { return bc.lastBatch diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index 968ab66a0..0c2aa0065 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -34,6 +34,7 @@ type Blockchainer interface { GetAssetState(util.Uint256) *state.Asset GetAccountState(util.Uint160) *state.Account GetAppExecResult(util.Uint256) (*state.AppExecResult, error) + GetNEP5TransferLog(util.Uint160) *state.NEP5TransferLog GetValidators(txes ...*transaction.Transaction) ([]*keys.PublicKey, error) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem diff --git a/pkg/core/dao.go b/pkg/core/dao.go index 482eadbee..d64b1426c 100644 --- a/pkg/core/dao.go +++ b/pkg/core/dao.go @@ -142,6 +142,9 @@ func (dao *dao) GetNEP5TransferLog(acc util.Uint160) (*state.NEP5TransferLog, er key := storage.AppendPrefix(storage.STNEP5Transfers, acc.BytesBE()) value, err := dao.store.Get(key) if err != nil { + if err == storage.ErrKeyNotFound { + return new(state.NEP5TransferLog), nil + } return nil, err } return &state.NEP5TransferLog{Raw: value}, nil diff --git a/pkg/core/state/nep5.go b/pkg/core/state/nep5.go index e2143d13d..589a3d89c 100644 --- a/pkg/core/state/nep5.go +++ b/pkg/core/state/nep5.go @@ -52,6 +52,24 @@ func (lg *NEP5TransferLog) Append(tr *NEP5Transfer) error { 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 + } + tr := new(NEP5Transfer) + for i := 0; i < len(lg.Raw); i += NEP5TransferSize { + r := io.NewBinReaderFromBuf(lg.Raw[i : i+NEP5TransferSize]) + tr.DecodeBinary(r) + if r.Err != nil { + return r.Err + } else if err := f(tr); err != nil { + return nil + } + } + return nil +} + // EncodeBinary implements io.Serializable interface. func (t *NEP5Tracker) EncodeBinary(w *io.BinWriter) { w.WriteU64LE(uint64(t.Balance)) diff --git a/pkg/core/state/nep5_test.go b/pkg/core/state/nep5_test.go index e97a37c8f..03cf14e57 100644 --- a/pkg/core/state/nep5_test.go +++ b/pkg/core/state/nep5_test.go @@ -1,14 +1,40 @@ package state import ( + gio "io" "math/rand" "testing" + "time" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/stretchr/testify/require" ) +func TestNEP5TransferLog_Append(t *testing.T) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + expected := []*NEP5Transfer{ + randomTransfer(t, r), + randomTransfer(t, r), + randomTransfer(t, r), + randomTransfer(t, r), + } + + lg := new(NEP5TransferLog) + for _, tr := range expected { + require.NoError(t, lg.Append(tr)) + } + + i := 0 + err := lg.ForEach(func(tr *NEP5Transfer) error { + require.Equal(t, expected[i], tr) + i++ + return nil + }) + require.NoError(t, err) + +} + func TestNEP5Tracker_EncodeBinary(t *testing.T) { expected := &NEP5Tracker{ Balance: int64(rand.Uint64()), @@ -32,6 +58,25 @@ func TestNEP5Transfer_DecodeBinary(t *testing.T) { testEncodeDecode(t, expected, new(NEP5Transfer)) } +func randomTransfer(t *testing.T, r *rand.Rand) *NEP5Transfer { + tr := &NEP5Transfer{ + Amount: int64(r.Uint64()), + Block: r.Uint32(), + } + + var err error + _, err = gio.ReadFull(r, tr.Asset[:]) + require.NoError(t, err) + _, err = gio.ReadFull(r, tr.From[:]) + require.NoError(t, err) + _, err = gio.ReadFull(r, tr.To[:]) + require.NoError(t, err) + _, err = gio.ReadFull(r, tr.Tx[:]) + require.NoError(t, err) + + return tr +} + func testEncodeDecode(t *testing.T, expected, actual io.Serializable) { w := io.NewBufBinWriter() expected.EncodeBinary(w.BinWriter) diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 21161dc0c..ff2edbc4b 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -92,6 +92,9 @@ func (chain testChain) GetAssetState(util.Uint256) *state.Asset { func (chain testChain) GetAccountState(util.Uint160) *state.Account { panic("TODO") } +func (chain testChain) GetNEP5TransferLog(util.Uint160) *state.NEP5TransferLog { + panic("TODO") +} func (chain testChain) GetValidators(...*transaction.Transaction) ([]*keys.PublicKey, error) { panic("TODO") } diff --git a/pkg/rpc/client/doc.go b/pkg/rpc/client/doc.go index 03e85379f..888ef0a54 100644 --- a/pkg/rpc/client/doc.go +++ b/pkg/rpc/client/doc.go @@ -22,6 +22,7 @@ Supported methods getblock getclaimable getnep5balances + getnep5transfers getrawtransaction getunspents invoke @@ -44,7 +45,6 @@ Unsupported methods getconnectioncount getcontractstate getmetricblocktimestamp - getnep5transfers getnewaddress getpeers getrawmempool diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 6d9b3f67f..d30148ee4 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -103,6 +103,16 @@ 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) { + params := request.NewRawParams(address) + resp := new(result.NEP5Transfers) + if err := c.performRequest("getnep5transfers", params, resp); err != nil { + return nil, err + } + return resp, nil +} + // GetRawTransaction returns a transaction by hash. func (c *Client) GetRawTransaction(hash util.Uint256) (*transaction.Transaction, error) { var ( diff --git a/pkg/rpc/response/result/nep5.go b/pkg/rpc/response/result/nep5.go index 8a70b13b7..29e0bc9e9 100644 --- a/pkg/rpc/response/result/nep5.go +++ b/pkg/rpc/response/result/nep5.go @@ -14,3 +14,21 @@ type NEP5Balance struct { Amount string `json:"amount"` LastUpdated uint32 `json:"last_updated_block"` } + +// NEP5Transfers is a result for the getnep5transfers RPC. +type NEP5Transfers struct { + Sent []NEP5Transfer `json:"sent"` + Received []NEP5Transfer `json:"received"` + Address string `json:"address"` +} + +// NEP5Transfer represents single NEP5 transfer event. +type NEP5Transfer struct { + Timestamp uint32 `json:"timestamp"` + Asset util.Uint160 `json:"asset_hash"` + Address string `json:"transfer_address,omitempty"` + Amount string `json:"amount"` + Index uint32 `json:"block_index"` + NotifyIndex uint32 `json:"transfer_notify_index"` + TxHash util.Uint256 `json:"tx_hash"` +} diff --git a/pkg/rpc/server/prometheus.go b/pkg/rpc/server/prometheus.go index 0907b0bc5..bc18a0966 100644 --- a/pkg/rpc/server/prometheus.go +++ b/pkg/rpc/server/prometheus.go @@ -83,6 +83,14 @@ var ( }, ) + getnep5transfersCalled = prometheus.NewCounter( + prometheus.CounterOpts{ + Help: "Number of calls to getnep5transfers rpc endpoint", + Name: "getnep5transfers_called", + Namespace: "neogo", + }, + ) + getversionCalled = prometheus.NewCounter( prometheus.CounterOpts{ Help: "Number of calls to getversion rpc endpoint", diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 37b7e4561..9d13d33f0 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -202,6 +202,10 @@ Methods: getnep5balancesCalled.Inc() results, resultsErr = s.getNEP5Balances(reqParams) + case "getnep5transfers": + getnep5transfersCalled.Inc() + results, resultsErr = s.getNEP5Transfers(reqParams) + case "getversion": getversionCalled.Inc() results = result.Version{ @@ -410,6 +414,47 @@ func (s *Server) getNEP5Balances(ps request.Params) (interface{}, error) { return bs, nil } +func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, error) { + p, ok := ps.ValueWithType(0, request.StringT) + if !ok { + return nil, response.ErrInvalidParams + } + u, err := p.GetUint160FromAddress() + if err != nil { + return nil, response.ErrInvalidParams + } + + bs := &result.NEP5Transfers{Address: address.Uint160ToString(u)} + lg := s.chain.GetNEP5TransferLog(u) + err = lg.ForEach(func(tr *state.NEP5Transfer) error { + transfer := result.NEP5Transfer{ + Timestamp: tr.Timestamp, + Asset: tr.Asset, + Index: tr.Block, + TxHash: tr.Tx, + } + if tr.Amount > 0 { // token was received + transfer.Amount = strconv.FormatInt(tr.Amount, 10) + if !tr.From.Equals(util.Uint160{}) { + transfer.Address = address.Uint160ToString(tr.From) + } + bs.Received = append(bs.Received, transfer) + return nil + } + + transfer.Amount = strconv.FormatInt(-tr.Amount, 10) + if !tr.From.Equals(util.Uint160{}) { + transfer.Address = address.Uint160ToString(tr.To) + } + bs.Sent = append(bs.Sent, transfer) + return nil + }) + if err != nil { + return nil, response.NewInternalServerError("invalid NEP5 transfer log", err) + } + return bs, nil +} + func (s *Server) getStorage(ps request.Params) (interface{}, error) { param, ok := ps.Value(0) if !ok { diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index a2a77381c..c26c98332 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -14,6 +14,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/internal/random" "github.com/nspcc-dev/neo-go/pkg/rpc/response" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" @@ -170,6 +171,41 @@ var rpcTestCases = map[string][]rpcTestCase{ }, }, }, + "getnep5transfers": { + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "invalid address", + params: `["notahex"]`, + fail: true, + }, + { + name: "positive", + params: `["AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs"]`, + result: func(e *executor) interface{} { return &result.NEP5Transfers{} }, + check: func(t *testing.T, e *executor, acc interface{}) { + res, ok := acc.(*result.NEP5Transfers) + require.True(t, ok) + require.Equal(t, "AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs", res.Address) + + assetHash, err := util.Uint160DecodeStringLE("d864728bdbc88da799bc43862ae6aaa62adc3a87") + require.NoError(t, err) + + require.Equal(t, 1, len(res.Received)) + require.Equal(t, "1000", res.Received[0].Amount) + require.Equal(t, assetHash, res.Received[0].Asset) + require.Equal(t, address.Uint160ToString(assetHash), res.Received[0].Address) + + require.Equal(t, 1, len(res.Sent)) + require.Equal(t, "123", res.Sent[0].Amount) + require.Equal(t, assetHash, res.Sent[0].Asset) + require.Equal(t, "AWLYWXB8C9Lt1nHdDZJnC5cpYJjgRDLk17", res.Sent[0].Address) + }, + }, + }, "getstorage": { { name: "positive",