rpc: use state.AppExecResult for ApplicationLog marshalling

Closes #1371
This commit is contained in:
Anna Shaleva 2020-09-03 19:58:50 +03:00
parent 34df5d5949
commit acacac1b24
11 changed files with 276 additions and 136 deletions

View file

@ -1,7 +1,9 @@
package state
import (
"encoding/json"
"errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
@ -13,9 +15,9 @@ import (
// NotificationEvent is a tuple of scripthash that emitted the Item as a
// notification and that item itself.
type NotificationEvent struct {
ScriptHash util.Uint160
Name string
Item *stackitem.Array
ScriptHash util.Uint160 `json:"contract"`
Name string `json:"eventname"`
Item *stackitem.Array `json:"state"`
}
// AppExecResult represent the result of the script execution, gathering together
@ -79,3 +81,119 @@ func (aer *AppExecResult) DecodeBinary(r *io.BinReader) {
}
r.ReadArray(&aer.Events)
}
// notificationEventAux is an auxiliary struct for NotificationEvent JSON marshalling.
type notificationEventAux struct {
ScriptHash util.Uint160 `json:"contract"`
Name string `json:"eventname"`
Item json.RawMessage `json:"state"`
}
// MarshalJSON implements implements json.Marshaler interface.
func (ne *NotificationEvent) MarshalJSON() ([]byte, error) {
item, err := stackitem.ToJSONWithTypes(ne.Item)
if err != nil {
item = []byte(`"error: recursive reference"`)
}
return json.Marshal(&notificationEventAux{
ScriptHash: ne.ScriptHash,
Name: ne.Name,
Item: item,
})
}
// UnmarshalJSON implements json.Unmarshaler interface.
func (ne *NotificationEvent) UnmarshalJSON(data []byte) error {
aux := new(notificationEventAux)
if err := json.Unmarshal(data, aux); err != nil {
return err
}
item, err := stackitem.FromJSONWithTypes(aux.Item)
if err != nil {
return err
}
if t := item.Type(); t != stackitem.ArrayT {
return fmt.Errorf("failed to convert notification event state of type %s to array", t.String())
}
ne.Item = item.(*stackitem.Array)
ne.Name = aux.Name
ne.ScriptHash = aux.ScriptHash
return nil
}
// appExecResultAux is an auxiliary struct for JSON marshalling
type appExecResultAux struct {
TxHash util.Uint256 `json:"txid"`
Trigger string `json:"trigger"`
VMState string `json:"vmstate"`
GasConsumed int64 `json:"gasconsumed,string"`
Stack json.RawMessage `json:"stack"`
Events []NotificationEvent `json:"notifications"`
}
// MarshalJSON implements implements json.Marshaler interface.
func (aer *AppExecResult) MarshalJSON() ([]byte, error) {
var st json.RawMessage
arr := make([]json.RawMessage, len(aer.Stack))
for i := range arr {
data, err := stackitem.ToJSONWithTypes(aer.Stack[i])
if err != nil {
st = []byte(`"error: recursive reference"`)
break
}
arr[i] = data
}
var err error
if st == nil {
st, err = json.Marshal(arr)
if err != nil {
return nil, err
}
}
return json.Marshal(&appExecResultAux{
TxHash: aer.TxHash,
Trigger: aer.Trigger.String(),
VMState: aer.VMState.String(),
GasConsumed: aer.GasConsumed,
Stack: st,
Events: aer.Events,
})
}
// UnmarshalJSON implements implements json.Unmarshaler interface.
func (aer *AppExecResult) UnmarshalJSON(data []byte) error {
aux := new(appExecResultAux)
if err := json.Unmarshal(data, aux); err != nil {
return err
}
var arr []json.RawMessage
if err := json.Unmarshal(aux.Stack, &arr); err == nil {
st := make([]stackitem.Item, len(arr))
for i := range arr {
st[i], err = stackitem.FromJSONWithTypes(arr[i])
if err != nil {
break
}
}
if err == nil {
aer.Stack = st
}
}
trigger, err := trigger.FromString(aux.Trigger)
if err != nil {
return err
}
aer.Trigger = trigger
aer.TxHash = aux.TxHash
state, err := vm.StateFromString(aux.VMState)
if err != nil {
return err
}
aer.VMState = state
aer.Events = aux.Events
aer.GasConsumed = aux.GasConsumed
return nil
}

View file

@ -1,12 +1,14 @@
package state
import (
"encoding/json"
"testing"
"github.com/nspcc-dev/neo-go/pkg/internal/random"
"github.com/nspcc-dev/neo-go/pkg/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
func TestEncodeDecodeNotificationEvent(t *testing.T) {
@ -31,3 +33,92 @@ func TestEncodeDecodeAppExecResult(t *testing.T) {
testserdes.EncodeDecodeBinary(t, appExecResult, new(AppExecResult))
}
func TestMarshalUnmarshalJSONNotificationEvent(t *testing.T) {
t.Run("positive", func(t *testing.T) {
ne := &NotificationEvent{
ScriptHash: random.Uint160(),
Name: "my_ne",
Item: stackitem.NewArray([]stackitem.Item{
stackitem.NewBool(true),
}),
}
testserdes.MarshalUnmarshalJSON(t, ne, new(NotificationEvent))
})
t.Run("MarshalJSON recursive reference", func(t *testing.T) {
i := make([]stackitem.Item, 1)
recursive := stackitem.NewArray(i)
i[0] = recursive
ne := &NotificationEvent{
Item: recursive,
}
_, err := json.Marshal(ne)
require.NoError(t, err)
})
t.Run("UnmarshalJSON error", func(t *testing.T) {
errorCases := []string{
`{"contract":"0xBadHash","eventname":"my_ne","state":{"type":"Array","value":[{"type":"Boolean","value":true}]}}`,
`{"contract":"0xab2f820e2aa7cca1e081283c58a7d7943c33a2f1","eventname":"my_ne","state":{"type":"Array","value":[{"type":"BadType","value":true}]}}`,
`{"contract":"0xab2f820e2aa7cca1e081283c58a7d7943c33a2f1","eventname":"my_ne","state":{"type":"Boolean", "value":true}}`,
}
for _, errCase := range errorCases {
err := json.Unmarshal([]byte(errCase), new(NotificationEvent))
require.Error(t, err)
}
})
}
func TestMarshalUnmarshalJSONAppExecResult(t *testing.T) {
t.Run("positive", func(t *testing.T) {
appExecResult := &AppExecResult{
TxHash: random.Uint256(),
Trigger: 1,
VMState: vm.HaltState,
GasConsumed: 10,
Stack: []stackitem.Item{},
Events: []NotificationEvent{},
}
testserdes.MarshalUnmarshalJSON(t, appExecResult, new(AppExecResult))
})
t.Run("MarshalJSON recursive reference", func(t *testing.T) {
i := make([]stackitem.Item, 1)
recursive := stackitem.NewArray(i)
i[0] = recursive
errorCases := []*AppExecResult{
{
Stack: i,
},
}
for _, errCase := range errorCases {
_, err := json.Marshal(errCase)
require.NoError(t, err)
}
})
t.Run("UnmarshalJSON error", func(t *testing.T) {
nilStackCases := []string{
`{"txid":"0x17145a039fca704fcdbeb46e6b210af98a1a9e5b9768e46ffc38f71c79ac2521","trigger":"Application","vmstate":"HALT","gasconsumed":"1","stack":[{"type":"WrongType","value":"1"}],"notifications":[]}`,
}
for _, str := range nilStackCases {
actual := new(AppExecResult)
err := json.Unmarshal([]byte(str), actual)
require.NoError(t, err)
require.Nil(t, actual.Stack)
}
errorCases := []string{
`{"txid":"0xBadHash","trigger":"Application","vmstate":"HALT","gasconsumed":"1","stack":[{"type":"Integer","value":"1"}],"notifications":[]}`,
`{"txid":"0x17145a039fca704fcdbeb46e6b210af98a1a9e5b9768e46ffc38f71c79ac2521","trigger":"Application","vmstate":"BadState","gasconsumed":"1","stack":[{"type":"Integer","value":"1"}],"notifications":[]}`,
`{"txid":"0x17145a039fca704fcdbeb46e6b210af98a1a9e5b9768e46ffc38f71c79ac2521","trigger":"BadTrigger","vmstate":"HALT","gasconsumed":"1","stack":[{"type":"Integer","value":"1"}],"notifications":[]}`,
}
for _, str := range errorCases {
actual := new(AppExecResult)
err := json.Unmarshal([]byte(str), actual)
require.Error(t, err)
}
})
}

View file

@ -19,10 +19,10 @@ import (
)
// GetApplicationLog returns the contract log based on the specified txid.
func (c *Client) GetApplicationLog(hash util.Uint256) (*result.ApplicationLog, error) {
func (c *Client) GetApplicationLog(hash util.Uint256) (*state.AppExecResult, error) {
var (
params = request.NewRawParams(hash.StringLE())
resp = &result.ApplicationLog{}
resp = new(state.AppExecResult)
)
if err := c.performRequest("getapplicationlog", params, resp); err != nil {
return nil, err

View file

@ -25,7 +25,9 @@ import (
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/assert"
@ -113,13 +115,13 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
if err != nil {
panic(err)
}
return &result.ApplicationLog{
return &state.AppExecResult{
TxHash: txHash,
Trigger: "Application",
VMState: "HALT",
Trigger: trigger.Application,
VMState: vm.HaltState,
GasConsumed: 1,
Stack: []stackitem.Item{stackitem.NewBigInteger(big.NewInt(1))},
Events: []result.NotificationEvent{},
Events: []state.NotificationEvent{},
}
},
},

View file

@ -8,10 +8,10 @@ import (
"github.com/gorilla/websocket"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/rpc/request"
"github.com/nspcc-dev/neo-go/pkg/rpc/response"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/util"
)
@ -141,9 +141,9 @@ readloop:
case response.TransactionEventID:
val = &transaction.Transaction{Network: c.opts.Network}
case response.NotificationEventID:
val = new(result.NotificationEvent)
val = new(state.NotificationEvent)
case response.ExecutionEventID:
val = new(result.ApplicationLog)
val = new(state.AppExecResult)
case response.MissedEventID:
// No value.
default:

View file

@ -1,109 +0,0 @@
package result
import (
"encoding/json"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// ApplicationLog wrapper used for the representation of the
// state.AppExecResult based on the specific tx on the RPC Server.
type ApplicationLog struct {
TxHash util.Uint256
Trigger string
VMState string
GasConsumed int64
Stack []stackitem.Item
Events []NotificationEvent
}
//NotificationEvent response wrapper
type NotificationEvent struct {
Contract util.Uint160 `json:"contract"`
Name string `json:"eventname"`
Item smartcontract.Parameter `json:"state"`
}
type applicationLogAux struct {
TxHash util.Uint256 `json:"txid"`
Trigger string `json:"trigger"`
VMState string `json:"vmstate"`
GasConsumed int64 `json:"gasconsumed,string"`
Stack []json.RawMessage `json:"stack"`
Events []NotificationEvent `json:"notifications"`
}
// MarshalJSON implements json.Marshaler.
func (l ApplicationLog) MarshalJSON() ([]byte, error) {
arr := make([]json.RawMessage, len(l.Stack))
for i := range arr {
data, err := stackitem.ToJSONWithTypes(l.Stack[i])
if err != nil {
return nil, err
}
arr[i] = data
}
return json.Marshal(&applicationLogAux{
TxHash: l.TxHash,
Trigger: l.Trigger,
VMState: l.VMState,
GasConsumed: l.GasConsumed,
Stack: arr,
Events: l.Events,
})
}
// UnmarshalJSON implements json.Unmarshaler.
func (l *ApplicationLog) UnmarshalJSON(data []byte) error {
aux := new(applicationLogAux)
if err := json.Unmarshal(data, aux); err != nil {
return err
}
st := make([]stackitem.Item, len(aux.Stack))
var err error
for i := range st {
st[i], err = stackitem.FromJSONWithTypes(aux.Stack[i])
if err != nil {
return err
}
}
l.Stack = st
l.Trigger = aux.Trigger
l.TxHash = aux.TxHash
l.VMState = aux.VMState
l.Events = aux.Events
l.GasConsumed = aux.GasConsumed
return nil
}
// StateEventToResultNotification converts state.NotificationEvent to
// result.NotificationEvent.
func StateEventToResultNotification(event state.NotificationEvent) NotificationEvent {
seen := make(map[stackitem.Item]bool)
item := smartcontract.ParameterFromStackItem(event.Item, seen)
return NotificationEvent{
Contract: event.ScriptHash,
Name: event.Name,
Item: item,
}
}
// NewApplicationLog creates a new ApplicationLog wrapper.
func NewApplicationLog(appExecRes *state.AppExecResult) ApplicationLog {
events := make([]NotificationEvent, 0, len(appExecRes.Events))
for _, e := range appExecRes.Events {
events = append(events, StateEventToResultNotification(e))
}
return ApplicationLog{
TxHash: appExecRes.TxHash,
Trigger: appExecRes.Trigger.String(),
VMState: appExecRes.VMState.String(),
GasConsumed: appExecRes.GasConsumed,
Stack: appExecRes.Stack,
Events: events,
}
}

View file

@ -498,7 +498,7 @@ func (s *Server) getApplicationLog(reqParams request.Params) (interface{}, *resp
return nil, response.NewRPCError("Unknown transaction", "", err)
}
return result.NewApplicationLog(appExecResult), nil
return appExecResult, nil
}
func (s *Server) getNEP5Balances(ps request.Params) (interface{}, *response.Error) {
@ -1159,10 +1159,10 @@ chloop:
resp.Payload[0] = b
case execution := <-s.executionCh:
resp.Event = response.ExecutionEventID
resp.Payload[0] = result.NewApplicationLog(execution)
resp.Payload[0] = execution
case notification := <-s.notificationCh:
resp.Event = response.NotificationEventID
resp.Payload[0] = result.StateEventToResultNotification(*notification)
resp.Payload[0] = *notification
case tx := <-s.transactionCh:
resp.Event = response.TransactionEventID
resp.Payload[0] = tx

View file

@ -26,7 +26,9 @@ import (
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/rpc/response"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/assert"
@ -62,15 +64,15 @@ var rpcTestCases = map[string][]rpcTestCase{
{
name: "positive",
params: `["` + deploymentTxHash + `"]`,
result: func(e *executor) interface{} { return &result.ApplicationLog{} },
result: func(e *executor) interface{} { return &state.AppExecResult{} },
check: func(t *testing.T, e *executor, acc interface{}) {
res, ok := acc.(*result.ApplicationLog)
res, ok := acc.(*state.AppExecResult)
require.True(t, ok)
expectedTxHash, err := util.Uint256DecodeStringLE(deploymentTxHash)
require.NoError(t, err)
assert.Equal(t, expectedTxHash, res.TxHash)
assert.Equal(t, "Application", res.Trigger)
assert.Equal(t, "HALT", res.VMState)
assert.Equal(t, trigger.Application, res.Trigger)
assert.Equal(t, vm.HaltState, res.VMState)
},
},
{
@ -722,10 +724,10 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getapplicationlog", "params": ["%s"]}`
body := doRPCCall(fmt.Sprintf(rpc, e.chain.GetHeaderHash(1).StringLE()), httpSrv.URL, t)
data := checkErrGetResult(t, body, false)
var res result.ApplicationLog
var res state.AppExecResult
require.NoError(t, json.Unmarshal(data, &res))
require.Equal(t, "System", res.Trigger)
require.Equal(t, "HALT", res.VMState)
require.Equal(t, trigger.System, res.Trigger)
require.Equal(t, vm.HaltState, res.VMState)
})
t.Run("submit", func(t *testing.T) {

View file

@ -3,10 +3,10 @@ package server
import (
"github.com/gorilla/websocket"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/rpc/request"
"github.com/nspcc-dev/neo-go/pkg/rpc/response"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"go.uber.org/atomic"
)
@ -72,14 +72,14 @@ func (f *feed) Matches(r *response.Notification) bool {
return senderOK && signerOK
case response.NotificationEventID:
filt := f.filter.(request.NotificationFilter)
notification := r.Payload[0].(result.NotificationEvent)
hashOk := filt.Contract == nil || notification.Contract.Equals(*filt.Contract)
notification := r.Payload[0].(state.NotificationEvent)
hashOk := filt.Contract == nil || notification.ScriptHash.Equals(*filt.Contract)
nameOk := filt.Name == nil || notification.Name == *filt.Name
return hashOk && nameOk
case response.ExecutionEventID:
filt := f.filter.(request.ExecutionFilter)
applog := r.Payload[0].(result.ApplicationLog)
return applog.VMState == filt.State
applog := r.Payload[0].(*state.AppExecResult)
return applog.VMState.String() == filt.State
}
return false
}

View file

@ -1,5 +1,7 @@
package trigger
import "fmt"
//go:generate stringer -type=Type -output=trigger_type_string.go
// Type represents trigger type used in C# reference node: https://github.com/neo-project/neo/blob/c64748ecbac3baeb8045b16af0d518398a6ced24/neo/SmartContract/TriggerType.cs#L3
@ -27,3 +29,14 @@ const (
// All represents any trigger type.
All Type = System | Verification | Application
)
// FromString converts string to trigger Type
func FromString(str string) (Type, error) {
triggers := []Type{System, Verification, Application, All}
for _, t := range triggers {
if t.String() == str {
return t, nil
}
}
return 0, fmt.Errorf("unknown trigger type: %s", str)
}

View file

@ -4,6 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStringer(t *testing.T) {
@ -38,3 +39,25 @@ func TestDecodeBynary(t *testing.T) {
assert.Equal(t, o, Type(b))
}
}
func TestFromString(t *testing.T) {
testCases := map[string]Type{
"System": System,
"Application": Application,
"Verification": Verification,
"All": All,
}
for str, expected := range testCases {
actual, err := FromString(str)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
errorCases := []string{
"",
"Unknown",
}
for _, str := range errorCases {
_, err := FromString(str)
require.Error(t, err)
}
}