diff --git a/pkg/rpc/client/doc.go b/pkg/rpc/client/doc.go index 5fb0bc10b..5ad1eac12 100644 --- a/pkg/rpc/client/doc.go +++ b/pkg/rpc/client/doc.go @@ -29,6 +29,9 @@ Supported methods getconnectioncount getcontractstate getnativecontracts + getnep11balances + getnep11properties + getnep11transfers getnep17balances getnep17transfers getpeers diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index b0a02824e..da10e46d5 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -1,6 +1,8 @@ package client import ( + "encoding/base64" + "encoding/hex" "errors" "fmt" @@ -267,6 +269,16 @@ func (c *Client) GetNativeContracts() ([]state.NativeContract, error) { return resp, nil } +// GetNEP11Balances is a wrapper for getnep11balances RPC. +func (c *Client) GetNEP11Balances(address util.Uint160) (*result.NEP11Balances, error) { + params := request.NewRawParams(address.StringLE()) + resp := new(result.NEP11Balances) + if err := c.performRequest("getnep11balances", params, resp); err != nil { + return nil, err + } + return resp, nil +} + // GetNEP17Balances is a wrapper for getnep17balances RPC. func (c *Client) GetNEP17Balances(address util.Uint160) (*result.NEP17Balances, error) { params := request.NewRawParams(address.StringLE()) @@ -277,12 +289,53 @@ func (c *Client) GetNEP17Balances(address util.Uint160) (*result.NEP17Balances, return resp, nil } -// GetNEP17Transfers is a wrapper for getnep17transfers 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) GetNEP17Transfers(address string, start, stop *uint64, limit, page *int) (*result.NEP17Transfers, error) { +// GetNEP11Properties is a wrapper for getnep11properties RPC. We recommend using +// NEP11Properties method instead of this to receive and work with proper VM types, +// this method is provided mostly for the sake of completeness. For well-known +// attributes like "description", "image", "name" and "tokenURI" it returns strings, +// while for all other ones []byte (which can be nil). +func (c *Client) GetNEP11Properties(asset util.Uint160, token []byte) (map[string]interface{}, error) { + params := request.NewRawParams(asset.StringLE(), hex.EncodeToString(token)) + resp := make(map[string]interface{}) + if err := c.performRequest("getnep11properties", params, &resp); err != nil { + return nil, err + } + for k, v := range resp { + if v == nil { + continue + } + str, ok := v.(string) + if !ok { + return nil, errors.New("value is not a string") + } + if result.KnownNEP11Properties[k] { + continue + } + val, err := base64.StdEncoding.DecodeString(str) + if err != nil { + return nil, err + } + resp[k] = val + } + return resp, nil +} + +// GetNEP11Transfers is a wrapper for getnep11transfers RPC. Address parameter +// is mandatory, while all the others are optional. Limit and page parameters are +// only supported by NeoGo servers and can only be specified with start and stop. +func (c *Client) GetNEP11Transfers(address string, start, stop *uint64, limit, page *int) (*result.NEP11Transfers, error) { + params, err := packTransfersParams(address, start, stop, limit, page) + if err != nil { + return nil, err + } + resp := new(result.NEP11Transfers) + if err := c.performRequest("getnep11transfers", *params, resp); err != nil { + return nil, err + } + return resp, nil +} + +func packTransfersParams(address string, start, stop *uint64, limit, page *int) (*request.RawParams, error) { params := request.NewRawParams(address) if start != nil { params.Values = append(params.Values, *start) @@ -302,8 +355,21 @@ func (c *Client) GetNEP17Transfers(address string, start, stop *uint64, limit, p } else if stop != nil || limit != nil || page != nil { return nil, errors.New("bad parameters") } + return ¶ms, nil +} + +// GetNEP17Transfers is a wrapper for getnep17transfers 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) GetNEP17Transfers(address string, start, stop *uint64, limit, page *int) (*result.NEP17Transfers, error) { + params, err := packTransfersParams(address, start, stop, limit, page) + if err != nil { + return nil, err + } resp := new(result.NEP17Transfers) - if err := c.performRequest("getnep17transfers", params, resp); err != nil { + if err := c.performRequest("getnep17transfers", *params, resp); err != nil { return nil, err } return resp, nil diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index c14ddc130..a7a58a47d 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -532,6 +532,36 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ }, }, }, + "getnep11balances": { + { + name: "positive", + invoke: func(c *Client) (interface{}, error) { + hash, err := util.Uint160DecodeStringLE("1aada0032aba1ef6d1f07bbd8bec1d85f5380fb3") + if err != nil { + panic(err) + } + return c.GetNEP11Balances(hash) + }, + serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"balance":[{"assethash":"a48b6e1291ba24211ad11bb90ae2a10bf1fcd5a8","tokens":[{"tokenid":"abcdef","amount":"1","lastupdatedblock":251604}]}],"address":"NcEkNmgWmf7HQVQvzhxpengpnt4DXjmZLe"}}`, + result: func(c *Client) interface{} { + hash, err := util.Uint160DecodeStringLE("a48b6e1291ba24211ad11bb90ae2a10bf1fcd5a8") + if err != nil { + panic(err) + } + return &result.NEP11Balances{ + Balances: []result.NEP11AssetBalance{{ + Asset: hash, + Tokens: []result.NEP11TokenBalance{{ + ID: "abcdef", + Amount: "1", + LastUpdated: 251604, + }}, + }}, + Address: "NcEkNmgWmf7HQVQvzhxpengpnt4DXjmZLe", + } + }, + }, + }, "getnep17balances": { { name: "positive", @@ -559,6 +589,61 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ }, }, }, + "getnep11properties": { + { + name: "positive", + invoke: func(c *Client) (interface{}, error) { + hash, err := util.Uint160DecodeStringLE("1aada0032aba1ef6d1f07bbd8bec1d85f5380fb3") + if err != nil { + panic(err) + } + return c.GetNEP11Properties(hash, []byte("abcdef")) + }, // NcEkNmgWmf7HQVQvzhxpengpnt4DXjmZLe + serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"name":"sometoken","field1":"c29tZXRoaW5n","field2":null}}`, + result: func(c *Client) interface{} { + return map[string]interface{}{ + "name": "sometoken", + "field1": []byte("something"), + "field2": nil, + } + }, + }, + }, + "getnep11transfers": { + { + name: "positive", + invoke: func(c *Client) (interface{}, error) { + return c.GetNEP11Transfers("NcEkNmgWmf7HQVQvzhxpengpnt4DXjmZLe", nil, nil, nil, nil) + }, + serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"sent":[],"received":[{"timestamp":1555651816,"assethash":"600c4f5200db36177e3e8a09e9f18e2fc7d12a0f","transferaddress":"NfgHwwTi3wHAS8aFAN243C5vGbkYDpqLHP","amount":"1","tokenid":"abcdef","blockindex":436036,"transfernotifyindex":0,"txhash":"df7683ece554ecfb85cf41492c5f143215dd43ef9ec61181a28f922da06aba58"}],"address":"NcEkNmgWmf7HQVQvzhxpengpnt4DXjmZLe"}}`, + result: func(c *Client) interface{} { + assetHash, err := util.Uint160DecodeStringLE("600c4f5200db36177e3e8a09e9f18e2fc7d12a0f") + if err != nil { + panic(err) + } + txHash, err := util.Uint256DecodeStringLE("df7683ece554ecfb85cf41492c5f143215dd43ef9ec61181a28f922da06aba58") + if err != nil { + panic(err) + } + return &result.NEP11Transfers{ + Sent: []result.NEP11Transfer{}, + Received: []result.NEP11Transfer{ + { + Timestamp: 1555651816, + Asset: assetHash, + Address: "NfgHwwTi3wHAS8aFAN243C5vGbkYDpqLHP", + Amount: "1", + ID: "abcdef", + Index: 436036, + NotifyIndex: 0, + TxHash: txHash, + }, + }, + Address: "NcEkNmgWmf7HQVQvzhxpengpnt4DXjmZLe", + } + }, + }, + }, "getnep17transfers": { { name: "positive", @@ -1052,6 +1137,22 @@ type rpcClientErrorCase struct { } var rpcClientErrorCases = map[string][]rpcClientErrorCase{ + `{"jsonrpc":"2.0","id":1,"result":{"name":"name","bad":42}}`: { + { + name: "getnep11properties_unmarshalling_error", + invoke: func(c *Client) (interface{}, error) { + return c.GetNEP11Properties(util.Uint160{}, []byte{}) + }, + }, + }, + `{"jsonrpc":"2.0","id":1,"result":{"name":100500,"good":"c29tZXRoaW5n"}}`: { + { + name: "getnep11properties_unmarshalling_error", + invoke: func(c *Client) (interface{}, error) { + return c.GetNEP11Properties(util.Uint160{}, []byte{}) + }, + }, + }, `{"jsonrpc":"2.0","id":1,"result":"not-a-hex-string"}`: { { name: "getblock_not_a_hex_response", @@ -1229,12 +1330,30 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{ return c.GetContractStateByHash(util.Uint160{}) }, }, + { + name: "getnep11balances_invalid_params_error", + invoke: func(c *Client) (interface{}, error) { + return c.GetNEP11Balances(util.Uint160{}) + }, + }, { name: "getnep17balances_invalid_params_error", invoke: func(c *Client) (interface{}, error) { return c.GetNEP17Balances(util.Uint160{}) }, }, + { + name: "getnep11properties_invalid_params_error", + invoke: func(c *Client) (interface{}, error) { + return c.GetNEP11Properties(util.Uint160{}, []byte{}) + }, + }, + { + name: "getnep11transfers_invalid_params_error", + invoke: func(c *Client) (interface{}, error) { + return c.GetNEP11Transfers("", nil, nil, nil, nil) + }, + }, { name: "getnep17transfers_invalid_params_error", invoke: func(c *Client) (interface{}, error) { @@ -1416,12 +1535,24 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{ return c.GetContractStateByHash(util.Uint160{}) }, }, + { + name: "getnep11balances_unmarshalling_error", + invoke: func(c *Client) (interface{}, error) { + return c.GetNEP11Balances(util.Uint160{}) + }, + }, { name: "getnep17balances_unmarshalling_error", invoke: func(c *Client) (interface{}, error) { return c.GetNEP17Balances(util.Uint160{}) }, }, + { + name: "getnep11transfers_unmarshalling_error", + invoke: func(c *Client) (interface{}, error) { + return c.GetNEP11Transfers("", nil, nil, nil, nil) + }, + }, { name: "getnep17transfers_unmarshalling_error", invoke: func(c *Client) (interface{}, error) { diff --git a/pkg/rpc/response/result/tokens.go b/pkg/rpc/response/result/tokens.go index cb2ca0c82..1afc9fcdb 100644 --- a/pkg/rpc/response/result/tokens.go +++ b/pkg/rpc/response/result/tokens.go @@ -72,3 +72,11 @@ type NEP17Transfer struct { NotifyIndex uint32 `json:"transfernotifyindex"` TxHash util.Uint256 `json:"txhash"` } + +// KnownNEP11Properties contains a list of well-known NEP-11 token property names. +var KnownNEP11Properties = map[string]bool{ + "description": true, + "image": true, + "name": true, + "tokenURI": true, +} diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 32965c688..70ec058a5 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -159,13 +159,6 @@ var invalidBlockHeightError = func(index int, height int) *response.Error { // doesn't set any Error function. var upgrader = websocket.Upgrader{} -var knownNEP11Properties = map[string]bool{ - "description": true, - "image": true, - "name": true, - "tokenURI": true, -} - // New creates a new Server struct. func New(chain blockchainer.Blockchainer, conf rpc.Config, coreServer *network.Server, orc *oracle.Oracle, log *zap.Logger) Server { @@ -783,12 +776,12 @@ func (s *Server) getNEP11Properties(ps request.Params) (interface{}, *response.E continue } var val interface{} - if knownNEP11Properties[string(key)] || kv.Value.Type() != stackitem.AnyT { + if result.KnownNEP11Properties[string(key)] || kv.Value.Type() != stackitem.AnyT { v, err := kv.Value.TryBytes() if err != nil { continue } - if knownNEP11Properties[string(key)] { + if result.KnownNEP11Properties[string(key)] { val = string(v) } else { val = v