Merge pull request #675 from nspcc-dev/feature/getContractState
rpc: implement getcontractstate RPC Closes #342.
This commit is contained in:
commit
86cf309085
12 changed files with 239 additions and 10 deletions
|
@ -44,7 +44,7 @@ which would yield the response:
|
||||||
| `getblockhash` | Yes |
|
| `getblockhash` | Yes |
|
||||||
| `getblocksysfee` | No (#341) |
|
| `getblocksysfee` | No (#341) |
|
||||||
| `getconnectioncount` | Yes |
|
| `getconnectioncount` | Yes |
|
||||||
| `getcontractstate` | No (#342) |
|
| `getcontractstate` | Yes |
|
||||||
| `getnep5balances` | No (#498) |
|
| `getnep5balances` | No (#498) |
|
||||||
| `getnep5transfers` | No (#498) |
|
| `getnep5transfers` | No (#498) |
|
||||||
| `getpeers` | Yes |
|
| `getpeers` | Yes |
|
||||||
|
@ -76,4 +76,4 @@ Both methods also don't currently support arrays in function parameters.
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
* [JSON-RPC 2.0 Specification](http://www.jsonrpc.org/specification)
|
* [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)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ import (
|
||||||
"github.com/CityOfZion/neo-go/pkg/io"
|
"github.com/CityOfZion/neo-go/pkg/io"
|
||||||
"github.com/CityOfZion/neo-go/pkg/smartcontract"
|
"github.com/CityOfZion/neo-go/pkg/smartcontract"
|
||||||
"github.com/CityOfZion/neo-go/pkg/util"
|
"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/CityOfZion/neo-go/pkg/vm/opcode"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uber.org/zap/zaptest"
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -62,6 +62,12 @@ func NewInternalServerError(data string, cause error) *Error {
|
||||||
return newError(-32603, http.StatusInternalServerError, "Internal error", data, cause)
|
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.
|
// Error implements the error interface.
|
||||||
func (e Error) Error() string {
|
func (e Error) Error() string {
|
||||||
return fmt.Sprintf("%s (%d) - %s - %s", e.Message, e.Code, e.Data, e.Cause)
|
return fmt.Sprintf("%s (%d) - %s - %s", e.Message, e.Code, e.Data, e.Cause)
|
||||||
|
|
|
@ -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(
|
getversionCalled = prometheus.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Help: "Number of calls to getversion rpc endpoint",
|
Help: "Number of calls to getversion rpc endpoint",
|
||||||
|
@ -124,6 +132,7 @@ func init() {
|
||||||
getblockcountCalled,
|
getblockcountCalled,
|
||||||
getblockHashCalled,
|
getblockHashCalled,
|
||||||
getconnectioncountCalled,
|
getconnectioncountCalled,
|
||||||
|
getcontractstateCalled,
|
||||||
getversionCalled,
|
getversionCalled,
|
||||||
getpeersCalled,
|
getpeersCalled,
|
||||||
validateaddressCalled,
|
validateaddressCalled,
|
||||||
|
|
|
@ -234,13 +234,17 @@ Methods:
|
||||||
if as != nil {
|
if as != nil {
|
||||||
results = wrappers.NewAssetState(as)
|
results = wrappers.NewAssetState(as)
|
||||||
} else {
|
} else {
|
||||||
results = "Invalid assetid"
|
resultsErr = NewRPCError("Unknown asset", "", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "getaccountstate":
|
case "getaccountstate":
|
||||||
getaccountstateCalled.Inc()
|
getaccountstateCalled.Inc()
|
||||||
results, resultsErr = s.getAccountState(reqParams, false)
|
results, resultsErr = s.getAccountState(reqParams, false)
|
||||||
|
|
||||||
|
case "getcontractstate":
|
||||||
|
getcontractstateCalled.Inc()
|
||||||
|
results, resultsErr = s.getContractState(reqParams)
|
||||||
|
|
||||||
case "getrawtransaction":
|
case "getrawtransaction":
|
||||||
getrawtransactionCalled.Inc()
|
getrawtransactionCalled.Inc()
|
||||||
results, resultsErr = s.getrawtransaction(reqParams)
|
results, resultsErr = s.getrawtransaction(reqParams)
|
||||||
|
@ -288,7 +292,7 @@ func (s *Server) getrawtransaction(reqParams Params) (interface{}, error) {
|
||||||
resultsErr = errInvalidParams
|
resultsErr = errInvalidParams
|
||||||
} else if tx, height, err := s.chain.GetTransaction(txHash); err != nil {
|
} else if tx, height, err := s.chain.GetTransaction(txHash); err != nil {
|
||||||
err = errors.Wrapf(err, "Invalid transaction hash: %s", txHash)
|
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 {
|
} else if len(reqParams) >= 2 {
|
||||||
_header := s.chain.GetHeaderHash(int(height))
|
_header := s.chain.GetHeaderHash(int(height))
|
||||||
header, err := s.chain.GetHeader(_header)
|
header, err := s.chain.GetHeader(_header)
|
||||||
|
@ -349,6 +353,26 @@ func (s *Server) getTxOut(ps Params) (interface{}, error) {
|
||||||
return wrappers.NewTxOutput(&out), nil
|
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.
|
// getAccountState returns account state either in short or full (unspents included) form.
|
||||||
func (s *Server) getAccountState(reqParams Params, unspents bool) (interface{}, error) {
|
func (s *Server) getAccountState(reqParams Params, unspents bool) (interface{}, error) {
|
||||||
var resultsErr error
|
var resultsErr error
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/CityOfZion/neo-go/pkg/network"
|
"github.com/CityOfZion/neo-go/pkg/network"
|
||||||
"github.com/CityOfZion/neo-go/pkg/rpc/result"
|
"github.com/CityOfZion/neo-go/pkg/rpc/result"
|
||||||
"github.com/CityOfZion/neo-go/pkg/rpc/wrappers"
|
"github.com/CityOfZion/neo-go/pkg/rpc/wrappers"
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/util"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
)
|
)
|
||||||
|
@ -150,6 +151,29 @@ type GetUnspents struct {
|
||||||
ID int `json:"id"`
|
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) {
|
func initServerWithInMemoryChain(t *testing.T) (*core.Blockchain, http.HandlerFunc) {
|
||||||
var nBlocks uint32
|
var nBlocks uint32
|
||||||
|
|
||||||
|
@ -165,7 +189,13 @@ func initServerWithInMemoryChain(t *testing.T) (*core.Blockchain, http.HandlerFu
|
||||||
|
|
||||||
go chain.Run()
|
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)
|
require.Nil(t, err)
|
||||||
br := io.NewBinReaderFromIO(f)
|
br := io.NewBinReaderFromIO(f)
|
||||||
nBlocks = br.ReadU32LE()
|
nBlocks = br.ReadU32LE()
|
||||||
|
|
|
@ -72,6 +72,35 @@ var rpcTestCases = map[string][]rpcTestCase{
|
||||||
fail: true,
|
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": {
|
"getassetstate": {
|
||||||
{
|
{
|
||||||
name: "positive",
|
name: "positive",
|
||||||
|
@ -87,7 +116,7 @@ var rpcTestCases = map[string][]rpcTestCase{
|
||||||
{
|
{
|
||||||
name: "negative",
|
name: "negative",
|
||||||
params: `["602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de2"]`,
|
params: `["602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de2"]`,
|
||||||
result: func(e *executor) interface{} { return "Invalid assetid" },
|
fail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no params",
|
name: "no params",
|
||||||
|
|
BIN
pkg/rpc/testdata/50testblocks.acc
vendored
BIN
pkg/rpc/testdata/50testblocks.acc
vendored
Binary file not shown.
1
pkg/rpc/testdata/test_contract.avm
vendored
Executable file
1
pkg/rpc/testdata/test_contract.avm
vendored
Executable file
|
@ -0,0 +1 @@
|
||||||
|
QÅk
Hello, world!hNeo.Runtime.Logaluf
|
BIN
pkg/rpc/testdata/testblocks.acc
vendored
Normal file
BIN
pkg/rpc/testdata/testblocks.acc
vendored
Normal file
Binary file not shown.
|
@ -45,10 +45,7 @@ func NewAccountState(a *state.Account) AccountState {
|
||||||
sort.Sort(balances)
|
sort.Sort(balances)
|
||||||
|
|
||||||
// reverse scriptHash to be consistent with other client
|
// reverse scriptHash to be consistent with other client
|
||||||
scriptHash, err := util.Uint160DecodeBytesBE(a.ScriptHash.BytesLE())
|
scriptHash := a.ScriptHash.Reverse()
|
||||||
if err != nil {
|
|
||||||
scriptHash = a.ScriptHash
|
|
||||||
}
|
|
||||||
|
|
||||||
return AccountState{
|
return AccountState{
|
||||||
Version: a.Version,
|
Version: a.Version,
|
||||||
|
|
56
pkg/rpc/wrappers/contract_state.go
Normal file
56
pkg/rpc/wrappers/contract_state.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue