diff --git a/docs/rpc.md b/docs/rpc.md index 117e957fa..9b84e5719 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -44,7 +44,7 @@ which would yield the response: | `getblockhash` | Yes | | `getblocksysfee` | No (#341) | | `getconnectioncount` | Yes | -| `getcontractstate` | No (#342) | +| `getcontractstate` | Yes | | `getnep5balances` | No (#498) | | `getnep5transfers` | No (#498) | | `getpeers` | Yes | @@ -76,4 +76,4 @@ Both methods also don't currently support arrays in function parameters. ## Reference * [JSON-RPC 2.0 Specification](http://www.jsonrpc.org/specification) -* [NEO JSON-RPC 2.0 docs](https://docs.neo.org/en-us/node/cli/apigen.html) +* [NEO JSON-RPC 2.0 docs](https://docs.neo.org/docs/en-us/reference/rpc/latest-version/api.html) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 355184706..07e6ffe45 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "os" "testing" "time" @@ -17,6 +18,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm/emit" "github.com/CityOfZion/neo-go/pkg/vm/opcode" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" @@ -163,3 +165,78 @@ func newDumbBlock() *block.Block { }, } } + +// This function generates "../rpc/testdata/testblocks.acc" file which contains data +// for RPC unit tests. +// To generate new "../rpc/testdata/testblocks.acc", follow the steps: +// 1. Rename the function +// 2. Add specific test-case into "neo-go/pkg/core/blockchain_test.go" +// 3. Run tests with `$ make test` +func _(t *testing.T) { + bc := newTestChain(t) + n := 50 + blocks := makeBlocks(n) + + for i := 0; i < len(blocks); i++ { + if err := bc.AddBlock(blocks[i]); err != nil { + t.Fatal(err) + } + } + + tx1 := newMinerTX() + + avm, err := ioutil.ReadFile("../rpc/testdata/test_contract.avm") + if err != nil { + t.Fatal(err) + } + + var props smartcontract.PropertyState + script := io.NewBufBinWriter() + emit.Bytes(script.BinWriter, []byte("Da contract dat hallos u")) + emit.Bytes(script.BinWriter, []byte("joe@example.com")) + emit.Bytes(script.BinWriter, []byte("Random Guy")) + emit.Bytes(script.BinWriter, []byte("0.99")) + emit.Bytes(script.BinWriter, []byte("Helloer")) + props |= smartcontract.HasStorage + emit.Int(script.BinWriter, int64(props)) + emit.Int(script.BinWriter, int64(5)) + params := make([]byte, 1) + params[0] = byte(7) + emit.Bytes(script.BinWriter, params) + emit.Bytes(script.BinWriter, avm) + emit.Syscall(script.BinWriter, "Neo.Contract.Create") + txScript := script.Bytes() + + tx2 := transaction.NewInvocationTX(txScript, util.Fixed8FromFloat(100)) + + block := newBlock(uint32(n+1), tx1, tx2) + if err := bc.AddBlock(block); err != nil { + t.Fatal(err) + } + + outStream, err := os.Create("../rpc/testdata/testblocks.acc") + if err != nil { + t.Fatal(err) + } + defer outStream.Close() + + writer := io.NewBinWriterFromIO(outStream) + + count := bc.BlockHeight() + 1 + writer.WriteU32LE(count - 1) + + for i := 1; i < int(count); i++ { + bh := bc.GetHeaderHash(i) + b, err := bc.GetBlock(bh) + if err != nil { + t.Fatal(err) + } + buf := io.NewBufBinWriter() + b.EncodeBinary(buf.BinWriter) + bytes := buf.Bytes() + writer.WriteBytes(bytes) + if writer.Err != nil { + t.Fatal(err) + } + } +} diff --git a/pkg/rpc/errors.go b/pkg/rpc/errors.go index f7c3441d1..966f5f678 100644 --- a/pkg/rpc/errors.go +++ b/pkg/rpc/errors.go @@ -62,6 +62,12 @@ func NewInternalServerError(data string, cause error) *Error { return newError(-32603, http.StatusInternalServerError, "Internal error", data, cause) } +// NewRPCError creates a new error with +// code -100 +func NewRPCError(message string, data string, cause error) *Error { + return newError(-100, http.StatusUnprocessableEntity, message, data, cause) +} + // Error implements the error interface. func (e Error) Error() string { return fmt.Sprintf("%s (%d) - %s - %s", e.Message, e.Code, e.Data, e.Cause) diff --git a/pkg/rpc/prometheus.go b/pkg/rpc/prometheus.go index 686d09853..dd17124c4 100644 --- a/pkg/rpc/prometheus.go +++ b/pkg/rpc/prometheus.go @@ -44,6 +44,14 @@ var ( }, ) + getcontractstateCalled = prometheus.NewCounter( + prometheus.CounterOpts{ + Help: "Number of calls to getcontractstate rpc endpoint", + Name: "getcontractstate_called", + Namespace: "neogo", + }, + ) + getversionCalled = prometheus.NewCounter( prometheus.CounterOpts{ Help: "Number of calls to getversion rpc endpoint", @@ -124,6 +132,7 @@ func init() { getblockcountCalled, getblockHashCalled, getconnectioncountCalled, + getcontractstateCalled, getversionCalled, getpeersCalled, validateaddressCalled, diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index 3805da144..cc0d5217f 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -234,13 +234,17 @@ Methods: if as != nil { results = wrappers.NewAssetState(as) } else { - results = "Invalid assetid" + resultsErr = NewRPCError("Unknown asset", "", nil) } case "getaccountstate": getaccountstateCalled.Inc() results, resultsErr = s.getAccountState(reqParams, false) + case "getcontractstate": + getcontractstateCalled.Inc() + results, resultsErr = s.getContractState(reqParams) + case "getrawtransaction": getrawtransactionCalled.Inc() results, resultsErr = s.getrawtransaction(reqParams) @@ -288,7 +292,7 @@ func (s *Server) getrawtransaction(reqParams Params) (interface{}, error) { resultsErr = errInvalidParams } else if tx, height, err := s.chain.GetTransaction(txHash); err != nil { err = errors.Wrapf(err, "Invalid transaction hash: %s", txHash) - return nil, NewInvalidParamsError(err.Error(), err) + return nil, NewRPCError("Unknown transaction", err.Error(), err) } else if len(reqParams) >= 2 { _header := s.chain.GetHeaderHash(int(height)) header, err := s.chain.GetHeader(_header) @@ -349,6 +353,26 @@ func (s *Server) getTxOut(ps Params) (interface{}, error) { return wrappers.NewTxOutput(&out), nil } +// getContractState returns contract state (contract information, according to the contract script hash). +func (s *Server) getContractState(reqParams Params) (interface{}, error) { + var results interface{} + + param, ok := reqParams.ValueWithType(0, stringT) + if !ok { + return nil, errInvalidParams + } else if scriptHash, err := param.GetUint160FromHex(); err != nil { + return nil, errInvalidParams + } else { + cs := s.chain.GetContractState(scriptHash) + if cs != nil { + results = wrappers.NewContractState(cs) + } else { + return nil, NewRPCError("Unknown contract", "", nil) + } + } + return results, nil +} + // getAccountState returns account state either in short or full (unspents included) form. func (s *Server) getAccountState(reqParams Params, unspents bool) (interface{}, error) { var resultsErr error diff --git a/pkg/rpc/server_helper_test.go b/pkg/rpc/server_helper_test.go index 369562930..5451d4e1b 100644 --- a/pkg/rpc/server_helper_test.go +++ b/pkg/rpc/server_helper_test.go @@ -13,6 +13,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/network" "github.com/CityOfZion/neo-go/pkg/rpc/result" "github.com/CityOfZion/neo-go/pkg/rpc/wrappers" + "github.com/CityOfZion/neo-go/pkg/util" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) @@ -150,6 +151,29 @@ type GetUnspents struct { ID int `json:"id"` } +// GetContractStateResponse struct for testing. +type GetContractStateResponce struct { + Jsonrpc string `json:"jsonrpc"` + Result struct { + Version byte `json:"version"` + ScriptHash util.Uint160 `json:"hash"` + Script []byte `json:"script"` + ParamList interface{} `json:"parameters"` + ReturnType interface{} `json:"returntype"` + Name string `json:"name"` + CodeVersion string `json:"code_version"` + Author string `json:"author"` + Email string `json:"email"` + Description string `json:"description"` + Properties struct { + HasStorage bool `json:"storage"` + HasDynamicInvoke bool `json:"dynamic_invoke"` + IsPayable bool `json:"is_payable"` + } `json:"properties"` + } `json:"result"` + ID int `json:"id"` +} + func initServerWithInMemoryChain(t *testing.T) (*core.Blockchain, http.HandlerFunc) { var nBlocks uint32 @@ -165,7 +189,13 @@ func initServerWithInMemoryChain(t *testing.T) (*core.Blockchain, http.HandlerFu go chain.Run() - f, err := os.Open("testdata/50testblocks.acc") + // File "./testdata/testblocks.acc" was generated by function core._ + // ("neo-go/pkg/core/helper_test.go"). + // To generate new "./testdata/testblocks.acc", follow the steps: + // 1. Rename the function + // 2. Add specific test-case into "neo-go/pkg/core/blockchain_test.go" + // 3. Run tests with `$ make test` + f, err := os.Open("testdata/testblocks.acc") require.Nil(t, err) br := io.NewBinReaderFromIO(f) nBlocks = br.ReadU32LE() diff --git a/pkg/rpc/server_test.go b/pkg/rpc/server_test.go index 0c03ffb4d..a0c83480a 100644 --- a/pkg/rpc/server_test.go +++ b/pkg/rpc/server_test.go @@ -72,6 +72,35 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "getcontractstate": { + { + name: "positive", + params: `["6d1eeca891ee93de2b7a77eb91c26f3b3c04d6cf"]`, + result: func(e *executor) interface{} { return &GetContractStateResponce{} }, + check: func(t *testing.T, e *executor, result interface{}) { + res, ok := result.(*GetContractStateResponce) + require.True(t, ok) + assert.Equal(t, byte(0), res.Result.Version) + assert.Equal(t, util.Uint160{0x6d, 0x1e, 0xec, 0xa8, 0x91, 0xee, 0x93, 0xde, 0x2b, 0x7a, 0x77, 0xeb, 0x91, 0xc2, 0x6f, 0x3b, 0x3c, 0x4, 0xd6, 0xcf}, res.Result.ScriptHash) + assert.Equal(t, "0.99", res.Result.CodeVersion) + }, + }, + { + name: "negative", + params: `["6d1eeca891ee93de2b7a77eb91c26f3b3c04d6c3"]`, + fail: true, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "invalid hash", + params: `["notahex"]`, + fail: true, + }, + }, "getassetstate": { { name: "positive", @@ -87,7 +116,7 @@ var rpcTestCases = map[string][]rpcTestCase{ { name: "negative", params: `["602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de2"]`, - result: func(e *executor) interface{} { return "Invalid assetid" }, + fail: true, }, { name: "no params", diff --git a/pkg/rpc/testdata/50testblocks.acc b/pkg/rpc/testdata/50testblocks.acc deleted file mode 100644 index 91825648d..000000000 Binary files a/pkg/rpc/testdata/50testblocks.acc and /dev/null differ diff --git a/pkg/rpc/testdata/test_contract.avm b/pkg/rpc/testdata/test_contract.avm new file mode 100755 index 000000000..10193d3de --- /dev/null +++ b/pkg/rpc/testdata/test_contract.avm @@ -0,0 +1 @@ +QÅk Hello, world!hNeo.Runtime.Logaluf \ No newline at end of file diff --git a/pkg/rpc/testdata/testblocks.acc b/pkg/rpc/testdata/testblocks.acc new file mode 100644 index 000000000..3f63e2bdc Binary files /dev/null and b/pkg/rpc/testdata/testblocks.acc differ diff --git a/pkg/rpc/wrappers/account_state.go b/pkg/rpc/wrappers/account_state.go index ddc960337..b56981e38 100644 --- a/pkg/rpc/wrappers/account_state.go +++ b/pkg/rpc/wrappers/account_state.go @@ -45,10 +45,7 @@ func NewAccountState(a *state.Account) AccountState { sort.Sort(balances) // reverse scriptHash to be consistent with other client - scriptHash, err := util.Uint160DecodeBytesBE(a.ScriptHash.BytesLE()) - if err != nil { - scriptHash = a.ScriptHash - } + scriptHash := a.ScriptHash.Reverse() return AccountState{ Version: a.Version, diff --git a/pkg/rpc/wrappers/contract_state.go b/pkg/rpc/wrappers/contract_state.go new file mode 100644 index 000000000..ca1b11716 --- /dev/null +++ b/pkg/rpc/wrappers/contract_state.go @@ -0,0 +1,56 @@ +package wrappers + +import ( + "github.com/CityOfZion/neo-go/pkg/core/state" + "github.com/CityOfZion/neo-go/pkg/smartcontract" + "github.com/CityOfZion/neo-go/pkg/util" +) + +// ContractState wrapper used for the representation of +// state.Contract on the RPC Server. +type ContractState struct { + Version byte `json:"version"` + ScriptHash util.Uint160 `json:"hash"` + Script []byte `json:"script"` + ParamList []smartcontract.ParamType `json:"parameters"` + ReturnType smartcontract.ParamType `json:"returntype"` + Name string `json:"name"` + CodeVersion string `json:"code_version"` + Author string `json:"author"` + Email string `json:"email"` + Description string `json:"description"` + Properties Properties `json:"properties"` +} + +// Properties response wrapper. +type Properties struct { + HasStorage bool `json:"storage"` + HasDynamicInvoke bool `json:"dynamic_invoke"` + IsPayable bool `json:"is_payable"` +} + +// NewContractState creates a new Contract wrapper. +func NewContractState(c *state.Contract) ContractState { + // reverse scriptHash to be consistent with other client + scriptHash := c.ScriptHash().Reverse() + + properties := Properties{ + HasStorage: c.HasStorage(), + HasDynamicInvoke: c.HasDynamicInvoke(), + IsPayable: c.IsPayable(), + } + + return ContractState{ + Version: 0, + ScriptHash: scriptHash, + Script: c.Script, + ParamList: c.ParamList, + ReturnType: c.ReturnType, + Properties: properties, + Name: c.Name, + CodeVersion: c.CodeVersion, + Author: c.Author, + Email: c.Email, + Description: c.Description, + } +}