diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index b0876a4795..6f2ee64832 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -543,7 +543,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ } return c.GetNEP11Balances(hash) }, - serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"balance":[{"assethash":"a48b6e1291ba24211ad11bb90ae2a10bf1fcd5a8","tokens":[{"tokenid":"abcdef","amount":"1","lastupdatedblock":251604}]}],"address":"NcEkNmgWmf7HQVQvzhxpengpnt4DXjmZLe"}}`, + serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"balance":[{"assethash":"a48b6e1291ba24211ad11bb90ae2a10bf1fcd5a8","symbol":"SOME","decimals":"42","name":"Contract","tokens":[{"tokenid":"abcdef","amount":"1","lastupdatedblock":251604}]}],"address":"NcEkNmgWmf7HQVQvzhxpengpnt4DXjmZLe"}}`, result: func(c *Client) interface{} { hash, err := util.Uint160DecodeStringLE("a48b6e1291ba24211ad11bb90ae2a10bf1fcd5a8") if err != nil { @@ -551,7 +551,10 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ } return &result.NEP11Balances{ Balances: []result.NEP11AssetBalance{{ - Asset: hash, + Asset: hash, + Decimals: 42, + Name: "Contract", + Symbol: "SOME", Tokens: []result.NEP11TokenBalance{{ ID: "abcdef", Amount: "1", @@ -573,7 +576,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ } return c.GetNEP17Balances(hash) }, - serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"balance":[{"assethash":"a48b6e1291ba24211ad11bb90ae2a10bf1fcd5a8","amount":"50000000000","lastupdatedblock":251604}],"address":"AY6eqWjsUFCzsVELG7yG72XDukKvC34p2w"}}`, + serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"balance":[{"assethash":"a48b6e1291ba24211ad11bb90ae2a10bf1fcd5a8","symbol":"N17","decimals":"8","name":"Token","amount":"50000000000","lastupdatedblock":251604}],"address":"AY6eqWjsUFCzsVELG7yG72XDukKvC34p2w"}}`, result: func(c *Client) interface{} { hash, err := util.Uint160DecodeStringLE("a48b6e1291ba24211ad11bb90ae2a10bf1fcd5a8") if err != nil { @@ -582,6 +585,9 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ return &result.NEP17Balances{ Balances: []result.NEP17Balance{{ Asset: hash, + Decimals: 8, + Name: "Token", + Symbol: "N17", Amount: "50000000000", LastUpdated: 251604, }}, diff --git a/pkg/rpc/response/result/tokens.go b/pkg/rpc/response/result/tokens.go index 1afc9fcdb1..33d7c8ef5f 100644 --- a/pkg/rpc/response/result/tokens.go +++ b/pkg/rpc/response/result/tokens.go @@ -12,8 +12,11 @@ type NEP11Balances struct { // NEP11Balance is a structure holding balance of a NEP-11 asset. type NEP11AssetBalance struct { - Asset util.Uint160 `json:"assethash"` - Tokens []NEP11TokenBalance `json:"tokens"` + Asset util.Uint160 `json:"assethash"` + Decimals int `json:"decimals,string"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Tokens []NEP11TokenBalance `json:"tokens"` } // NEP11TokenBalance represents balance of a single NFT. @@ -33,7 +36,10 @@ type NEP17Balances struct { type NEP17Balance struct { Asset util.Uint160 `json:"assethash"` Amount string `json:"amount"` + Decimals int `json:"decimals,string"` LastUpdated uint32 `json:"lastupdatedblock"` + Name string `json:"name"` + Symbol string `json:"symbol"` } // NEP11Transfers is a result for the getnep11transfers RPC. diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 8436369d8a..e3564a50a1 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -677,17 +677,28 @@ func (s *Server) getApplicationLog(reqParams request.Params) (interface{}, *resp return result.NewApplicationLog(hash, appExecResults, trig), nil } -func (s *Server) getNEP11Tokens(h util.Uint160, acc util.Uint160, bw *io.BufBinWriter) ([]stackitem.Item, error) { - item, finalize, err := s.invokeReadOnly(bw, h, "tokensOf", acc) +func (s *Server) getNEP11Tokens(h util.Uint160, acc util.Uint160, bw *io.BufBinWriter) ([]stackitem.Item, string, int, error) { + items, finalize, err := s.invokeReadOnlyMulti(bw, h, []string{"tokensOf", "symbol", "decimals"}, [][]interface{}{{acc}, nil, nil}) if err != nil { - return nil, err + return nil, "", 0, err } defer finalize() - if (item.Type() == stackitem.InteropT) && iterator.IsIterator(item) { - vals, _ := iterator.Values(item, s.config.MaxNEP11Tokens) - return vals, nil + if (items[0].Type() != stackitem.InteropT) || !iterator.IsIterator(items[0]) { + return nil, "", 0, fmt.Errorf("invalid `tokensOf` result type %s", items[0].String()) + } + vals, _ := iterator.Values(items[0], s.config.MaxNEP11Tokens) + sym, err := stackitem.ToString(items[1]) + if err != nil { + return nil, "", 0, fmt.Errorf("`symbol` return value error: %w", err) } - return nil, fmt.Errorf("invalid `tokensOf` result type %s", item.String()) + dec, err := items[2].TryInteger() + if err != nil { + return nil, "", 0, fmt.Errorf("`decimals` return value error: %w", err) + } + if !dec.IsInt64() || dec.Sign() == -1 || dec.Int64() > math.MaxInt32 { + return nil, "", 0, errors.New("`decimals` returned a bad integer") + } + return vals, sym, int(dec.Int64()), nil } func (s *Server) getNEP11Balances(ps request.Params) (interface{}, *response.Error) { @@ -709,7 +720,7 @@ func (s *Server) getNEP11Balances(ps request.Params) (interface{}, *response.Err bw := io.NewBufBinWriter() contract_loop: for _, h := range s.chain.GetNEP11Contracts() { - toks, err := s.getNEP11Tokens(h, u, bw) + toks, sym, dec, err := s.getNEP11Tokens(h, u, bw) if err != nil { continue } @@ -730,8 +741,11 @@ contract_loop: lub = stateSyncPoint } bs.Balances = append(bs.Balances, result.NEP11AssetBalance{ - Asset: h, - Tokens: make([]result.NEP11TokenBalance, 0, len(toks)), + Asset: h, + Decimals: dec, + Name: cs.Manifest.Name, + Symbol: sym, + Tokens: make([]result.NEP11TokenBalance, 0, len(toks)), }) curAsset := &bs.Balances[len(bs.Balances)-1] for i := range toks { @@ -741,7 +755,7 @@ contract_loop: } var amount = "1" if isDivisible { - balance, err := s.getTokenBalance(h, u, id, bw) + balance, err := s.getNEP11DTokenBalance(h, u, id, bw) if err != nil { continue } @@ -829,7 +843,7 @@ func (s *Server) getNEP17Balances(ps request.Params) (interface{}, *response.Err stateSyncPoint := lastUpdated[math.MinInt32] bw := io.NewBufBinWriter() for _, h := range s.chain.GetNEP17Contracts() { - balance, err := s.getTokenBalance(h, u, nil, bw) + balance, sym, dec, err := s.getNEP17TokenBalance(h, u, bw) if err != nil { continue } @@ -851,21 +865,37 @@ func (s *Server) getNEP17Balances(ps request.Params) (interface{}, *response.Err bs.Balances = append(bs.Balances, result.NEP17Balance{ Asset: h, Amount: balance.String(), + Decimals: dec, LastUpdated: lub, + Name: cs.Manifest.Name, + Symbol: sym, }) } return bs, nil } func (s *Server) invokeReadOnly(bw *io.BufBinWriter, h util.Uint160, method string, params ...interface{}) (stackitem.Item, func(), error) { + r, f, err := s.invokeReadOnlyMulti(bw, h, []string{method}, [][]interface{}{params}) + if err != nil { + return nil, nil, err + } + return r[0], f, nil +} + +func (s *Server) invokeReadOnlyMulti(bw *io.BufBinWriter, h util.Uint160, methods []string, params [][]interface{}) ([]stackitem.Item, func(), error) { if bw == nil { bw = io.NewBufBinWriter() } else { bw.Reset() } - emit.AppCall(bw.BinWriter, h, method, callflag.ReadStates|callflag.AllowCall, params...) - if bw.Err != nil { - return nil, nil, fmt.Errorf("failed to create `%s` invocation script: %w", method, bw.Err) + if len(methods) != len(params) { + return nil, nil, fmt.Errorf("asymmetric parameters") + } + for i := range methods { + emit.AppCall(bw.BinWriter, h, methods[i], callflag.ReadStates|callflag.AllowCall, params[i]...) + if bw.Err != nil { + return nil, nil, fmt.Errorf("failed to create `%s` invocation script: %w", methods[i], bw.Err) + } } script := bw.Bytes() tx := &transaction.Transaction{Script: script} @@ -879,26 +909,42 @@ func (s *Server) invokeReadOnly(bw *io.BufBinWriter, h util.Uint160, method stri err = ic.VM.Run() if err != nil { ic.Finalize() - return nil, nil, fmt.Errorf("failed to run `%s` for %s: %w", method, h.StringLE(), err) + return nil, nil, fmt.Errorf("failed to run %d methods of %s: %w", len(methods), h.StringLE(), err) } - if ic.VM.Estack().Len() != 1 { + estack := ic.VM.Estack() + if estack.Len() != len(methods) { ic.Finalize() - return nil, nil, fmt.Errorf("invalid `%s` return values count: expected 1, got %d", method, ic.VM.Estack().Len()) + return nil, nil, fmt.Errorf("invalid return values count: expected %d, got %d", len(methods), estack.Len()) } - return ic.VM.Estack().Pop().Item(), ic.Finalize, nil + return estack.ToArray(), ic.Finalize, nil } -func (s *Server) getTokenBalance(h util.Uint160, acc util.Uint160, id []byte, bw *io.BufBinWriter) (*big.Int, error) { - var ( - item stackitem.Item - finalize func() - err error - ) - if id == nil { // NEP-17 and NEP-11 generic. - item, finalize, err = s.invokeReadOnly(bw, h, "balanceOf", acc) - } else { // NEP-11 divisible. - item, finalize, err = s.invokeReadOnly(bw, h, "balanceOf", acc, id) +func (s *Server) getNEP17TokenBalance(h util.Uint160, acc util.Uint160, bw *io.BufBinWriter) (*big.Int, string, int, error) { + items, finalize, err := s.invokeReadOnlyMulti(bw, h, []string{"balanceOf", "symbol", "decimals"}, [][]interface{}{{acc}, nil, nil}) + if err != nil { + return nil, "", 0, err } + finalize() + res, err := items[0].TryInteger() + if err != nil { + return nil, "", 0, fmt.Errorf("unexpected `balanceOf` result type: %w", err) + } + sym, err := stackitem.ToString(items[1]) + if err != nil { + return nil, "", 0, fmt.Errorf("`symbol` return value error: %w", err) + } + dec, err := items[2].TryInteger() + if err != nil { + return nil, "", 0, fmt.Errorf("`decimals` return value error: %w", err) + } + if !dec.IsInt64() || dec.Sign() == -1 || dec.Int64() > math.MaxInt32 { + return nil, "", 0, errors.New("`decimals` returned a bad integer") + } + return res, sym, int(dec.Int64()), nil +} + +func (s *Server) getNEP11DTokenBalance(h util.Uint160, acc util.Uint160, id []byte, bw *io.BufBinWriter) (*big.Int, error) { + item, finalize, err := s.invokeReadOnly(bw, h, "balanceOf", acc, id) if err != nil { return nil, err } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 27bf5985bf..7b659de93a 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -2428,7 +2428,9 @@ func checkNep11Balances(t *testing.T, e *executor, acc interface{}) { expected := result.NEP11Balances{ Balances: []result.NEP11AssetBalance{ { - Asset: nnsHash, + Asset: nnsHash, + Name: "NameService", + Symbol: "NNS", Tokens: []result.NEP11TokenBalance{ { ID: nnsToken1ID, @@ -2438,7 +2440,10 @@ func checkNep11Balances(t *testing.T, e *executor, acc interface{}) { }, }, { - Asset: nfsoHash, + Asset: nfsoHash, + Decimals: 2, + Name: "NeoFS Object NFT", + Symbol: "NFSO", Tokens: []result.NEP11TokenBalance{ { ID: nfsoToken1ID, @@ -2464,17 +2469,25 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { { Asset: rubles, Amount: "877", + Decimals: 2, LastUpdated: 6, + Name: "Rubl", + Symbol: "RUB", }, { Asset: e.chain.GoverningTokenHash(), Amount: "99998000", LastUpdated: 4, + Name: "NeoToken", + Symbol: "NEO", }, { Asset: e.chain.UtilityTokenHash(), Amount: "47102199200", + Decimals: 8, LastUpdated: 19, + Name: "GasToken", + Symbol: "GAS", }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), }