From acacac1b24d9dc91bd2036af717de9e6f90fe484 Mon Sep 17 00:00:00 2001
From: Anna Shaleva <shaleva.ann@nspcc.ru>
Date: Thu, 3 Sep 2020 19:58:50 +0300
Subject: [PATCH] rpc: use state.AppExecResult for ApplicationLog marshalling

Closes #1371
---
 pkg/core/state/notification_event.go          | 124 +++++++++++++++++-
 pkg/core/state/notification_event_test.go     |  91 +++++++++++++
 pkg/rpc/client/rpc.go                         |   4 +-
 pkg/rpc/client/rpc_test.go                    |  10 +-
 pkg/rpc/client/wsclient.go                    |   6 +-
 pkg/rpc/response/result/application_log.go    | 109 ---------------
 pkg/rpc/server/server.go                      |   6 +-
 pkg/rpc/server/server_test.go                 |  16 ++-
 pkg/rpc/server/subscription.go                |  10 +-
 pkg/smartcontract/trigger/trigger_type.go     |  13 ++
 .../trigger/trigger_type_test.go              |  23 ++++
 11 files changed, 276 insertions(+), 136 deletions(-)
 delete mode 100644 pkg/rpc/response/result/application_log.go

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(&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
+}
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)
+	}
+}