diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index 48027137d..99e5046cf 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -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(¬ificationEventAux{ + 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 +} diff --git a/pkg/core/state/notification_event_test.go b/pkg/core/state/notification_event_test.go index 7a24315ce..f5376e6e8 100644 --- a/pkg/core/state/notification_event_test.go +++ b/pkg/core/state/notification_event_test.go @@ -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) + } + }) +} diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index fb6146439..d0bca2474 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -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 diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index da1667e2d..5845b3065 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -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{}, } }, }, diff --git a/pkg/rpc/client/wsclient.go b/pkg/rpc/client/wsclient.go index f434ff51a..9b7a7eb73 100644 --- a/pkg/rpc/client/wsclient.go +++ b/pkg/rpc/client/wsclient.go @@ -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: diff --git a/pkg/rpc/response/result/application_log.go b/pkg/rpc/response/result/application_log.go deleted file mode 100644 index 455f1d431..000000000 --- a/pkg/rpc/response/result/application_log.go +++ /dev/null @@ -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, - } -} diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index f89bf00ac..eb6828f02 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -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 diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index a77fcb9cf..bf2ea7ed3 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -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) { diff --git a/pkg/rpc/server/subscription.go b/pkg/rpc/server/subscription.go index 3b2b728b5..4337b05a2 100644 --- a/pkg/rpc/server/subscription.go +++ b/pkg/rpc/server/subscription.go @@ -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 } diff --git a/pkg/smartcontract/trigger/trigger_type.go b/pkg/smartcontract/trigger/trigger_type.go index 2e929c346..e996ca9a9 100644 --- a/pkg/smartcontract/trigger/trigger_type.go +++ b/pkg/smartcontract/trigger/trigger_type.go @@ -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) +} diff --git a/pkg/smartcontract/trigger/trigger_type_test.go b/pkg/smartcontract/trigger/trigger_type_test.go index bf3bd9976..166c49d2f 100644 --- a/pkg/smartcontract/trigger/trigger_type_test.go +++ b/pkg/smartcontract/trigger/trigger_type_test.go @@ -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) + } +}