From 15a939b1dae72f5ee3b3c8e7aa6c76b89e8d7f30 Mon Sep 17 00:00:00 2001
From: Anna Shaleva <shaleva.ann@nspcc.ru>
Date: Fri, 25 Sep 2020 12:40:57 +0300
Subject: [PATCH] rpc: allow to `getcontractstate` by address, id or name

close #1423
---
 docs/rpc.md                           |  5 +++
 pkg/core/blockchain.go                |  9 ++++++
 pkg/core/blockchainer/blockchainer.go |  1 +
 pkg/core/native/contract.go           | 12 ++++++++
 pkg/network/helper_test.go            |  3 ++
 pkg/rpc/server/server.go              | 43 ++++++++++++++++++++++++--
 pkg/rpc/server/server_test.go         | 44 +++++++++++++++++++++++++--
 7 files changed, 112 insertions(+), 5 deletions(-)

diff --git a/docs/rpc.md b/docs/rpc.md
index a16d81cee..045dc83db 100644
--- a/docs/rpc.md
+++ b/docs/rpc.md
@@ -77,6 +77,11 @@ Both methods also don't currently support arrays in function parameters.
 It's possible to call this method for any address with neo-go, unlike with C#
 node where it only works for addresses from opened wallet.
 
+##### `getcontractstate`
+
+It's possible to get non-native contract state by its ID, unlike with C# node where
+it only works for native contracts.
+
 ### Unsupported methods
 
 Methods listed down below are not going to be supported for various reasons
diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go
index dad9fa944..84d2de535 100644
--- a/pkg/core/blockchain.go
+++ b/pkg/core/blockchain.go
@@ -1073,6 +1073,15 @@ func (bc *Blockchain) GetContractScriptHash(id int32) (util.Uint160, error) {
 	return bc.dao.GetContractScriptHash(id)
 }
 
+// GetNativeContractScriptHash returns native contract script hash by its name.
+func (bc *Blockchain) GetNativeContractScriptHash(name string) (util.Uint160, error) {
+	c := bc.contracts.ByName(name)
+	if c != nil {
+		return c.Metadata().Hash, nil
+	}
+	return util.Uint160{}, errors.New("Unknown native contract")
+}
+
 // GetConfig returns the config stored in the blockchain.
 func (bc *Blockchain) GetConfig() config.ProtocolConfiguration {
 	return bc.config
diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go
index b02ff1316..04e72b569 100644
--- a/pkg/core/blockchainer/blockchainer.go
+++ b/pkg/core/blockchainer/blockchainer.go
@@ -39,6 +39,7 @@ type Blockchainer interface {
 	HasBlock(util.Uint256) bool
 	HasTransaction(util.Uint256) bool
 	GetAppExecResult(util.Uint256) (*state.AppExecResult, error)
+	GetNativeContractScriptHash(string) (util.Uint160, error)
 	GetNextBlockValidators() ([]*keys.PublicKey, error)
 	GetNEP5Balances(util.Uint160) *state.NEP5Balances
 	GetValidators() ([]*keys.PublicKey, error)
diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go
index a250bfed5..a32dbc827 100644
--- a/pkg/core/native/contract.go
+++ b/pkg/core/native/contract.go
@@ -2,6 +2,7 @@ package native
 
 import (
 	"errors"
+	"strings"
 
 	"github.com/nspcc-dev/neo-go/pkg/core/interop"
 	"github.com/nspcc-dev/neo-go/pkg/io"
@@ -35,6 +36,17 @@ func (cs *Contracts) ByHash(h util.Uint160) interop.Contract {
 	return nil
 }
 
+// ByName returns native contract with the specified name.
+func (cs *Contracts) ByName(name string) interop.Contract {
+	name = strings.ToLower(name)
+	for _, ctr := range cs.Contracts {
+		if strings.ToLower(ctr.Metadata().Name) == name {
+			return ctr
+		}
+	}
+	return nil
+}
+
 // NewContracts returns new set of native contracts with new GAS, NEO and Policy
 // contracts.
 func NewContracts() *Contracts {
diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go
index 5f74e24b7..b8a619006 100644
--- a/pkg/network/helper_test.go
+++ b/pkg/network/helper_test.go
@@ -89,6 +89,9 @@ func (chain testChain) GetContractState(hash util.Uint160) *state.Contract {
 func (chain testChain) GetContractScriptHash(id int32) (util.Uint160, error) {
 	panic("TODO")
 }
+func (chain testChain) GetNativeContractScriptHash(name string) (util.Uint160, error) {
+	panic("TODO")
+}
 func (chain testChain) GetHeaderHash(int) util.Uint256 {
 	return util.Uint256{}
 }
diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go
index 305838f15..d820d8332 100644
--- a/pkg/rpc/server/server.go
+++ b/pkg/rpc/server/server.go
@@ -765,6 +765,42 @@ func (s *Server) contractIDFromParam(param *request.Param) (int32, *response.Err
 	return result, nil
 }
 
+// getContractScriptHashFromParam returns the contract script hash by hex contract hash, address, id or native contract name.
+func (s *Server) contractScriptHashFromParam(param *request.Param) (util.Uint160, *response.Error) {
+	var result util.Uint160
+	if param == nil {
+		return result, response.ErrInvalidParams
+	}
+	switch param.Type {
+	case request.StringT:
+		var err error
+		result, err = param.GetUint160FromAddressOrHex()
+		if err == nil {
+			return result, nil
+		}
+		name, err := param.GetString()
+		if err != nil {
+			return result, response.ErrInvalidParams
+		}
+		result, err = s.chain.GetNativeContractScriptHash(name)
+		if err != nil {
+			return result, response.NewRPCError("Unknown contract: querying by name is supported for native contracts only", "", nil)
+		}
+	case request.NumberT:
+		id, err := param.GetInt()
+		if err != nil {
+			return result, response.ErrInvalidParams
+		}
+		result, err = s.chain.GetContractScriptHash(int32(id))
+		if err != nil {
+			return result, response.NewRPCError("Unknown contract", "", err)
+		}
+	default:
+		return result, response.ErrInvalidParams
+	}
+	return result, nil
+}
+
 func (s *Server) getStorage(ps request.Params) (interface{}, *response.Error) {
 	id, rErr := s.contractIDFromParam(ps.Value(0))
 	if rErr == response.ErrUnknown {
@@ -828,11 +864,12 @@ func (s *Server) getTransactionHeight(ps request.Params) (interface{}, *response
 	return height, nil
 }
 
-// getContractState returns contract state (contract information, according to the contract script hash).
+// getContractState returns contract state (contract information, according to the contract script hash,
+// contract id or native contract name).
 func (s *Server) getContractState(reqParams request.Params) (interface{}, *response.Error) {
-	scriptHash, err := reqParams.ValueWithType(0, request.StringT).GetUint160FromHex()
+	scriptHash, err := s.contractScriptHashFromParam(reqParams.Value(0))
 	if err != nil {
-		return nil, response.ErrInvalidParams
+		return nil, err
 	}
 	cs := s.chain.GetContractState(scriptHash)
 	if cs == nil {
diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go
index 9d2c9a0ad..66f5a02ba 100644
--- a/pkg/rpc/server/server_test.go
+++ b/pkg/rpc/server/server_test.go
@@ -96,7 +96,7 @@ var rpcTestCases = map[string][]rpcTestCase{
 	},
 	"getcontractstate": {
 		{
-			name:   "positive",
+			name:   "positive, by hash",
 			params: fmt.Sprintf(`["%s"]`, testContractHash),
 			result: func(e *executor) interface{} { return &state.Contract{} },
 			check: func(t *testing.T, e *executor, cs interface{}) {
@@ -106,10 +106,50 @@ var rpcTestCases = map[string][]rpcTestCase{
 			},
 		},
 		{
-			name:   "negative",
+			name:   "positive, by id",
+			params: `[0]`,
+			result: func(e *executor) interface{} { return &state.Contract{} },
+			check: func(t *testing.T, e *executor, cs interface{}) {
+				res, ok := cs.(*state.Contract)
+				require.True(t, ok)
+				assert.Equal(t, int32(0), res.ID)
+			},
+		},
+		{
+			name:   "positive, native by id",
+			params: `[-3]`,
+			result: func(e *executor) interface{} { return &state.Contract{} },
+			check: func(t *testing.T, e *executor, cs interface{}) {
+				res, ok := cs.(*state.Contract)
+				require.True(t, ok)
+				assert.Equal(t, int32(-3), res.ID)
+			},
+		},
+		{
+			name:   "positive, native by name",
+			params: `["Policy"]`,
+			result: func(e *executor) interface{} { return &state.Contract{} },
+			check: func(t *testing.T, e *executor, cs interface{}) {
+				res, ok := cs.(*state.Contract)
+				require.True(t, ok)
+				assert.Equal(t, int32(-3), res.ID)
+			},
+		},
+		{
+			name:   "negative, bad hash",
 			params: `["6d1eeca891ee93de2b7a77eb91c26f3b3c04d6c3"]`,
 			fail:   true,
 		},
+		{
+			name:   "negative, bad ID",
+			params: `[-8]`,
+			fail:   true,
+		},
+		{
+			name:   "negative, bad native name",
+			params: `["unknown_native"]`,
+			fail:   true,
+		},
 		{
 			name:   "no params",
 			params: `[]`,