From 4ff3a9e9a7efaa8d75a6ed83f0ddd99e43495a28 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 4 Aug 2020 16:24:32 +0300 Subject: [PATCH] rpc: filter subscriptions' notifications by name Closes #1263 --- docs/notifications.md | 14 +++++----- pkg/rpc/client/wsclient.go | 6 ++--- pkg/rpc/client/wsclient_test.go | 42 ++++++++++++++++++++++++++--- pkg/rpc/request/param.go | 5 ++-- pkg/rpc/request/param_test.go | 13 ++++++++- pkg/rpc/server/subscription.go | 4 ++- pkg/rpc/server/subscription_test.go | 25 +++++++++++++++-- 7 files changed, 90 insertions(+), 19 deletions(-) diff --git a/docs/notifications.md b/docs/notifications.md index 9e8426ab7..ce1e6edc4 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -61,7 +61,8 @@ Recognized stream names: format for one of transaction's `Cosigners`. * `notification_from_execution` Filter: `contract` field containing string with hex-encoded Uint160 (LE - representation). + representation) and/or `name` field containing string with execution + notification name. * `transaction_executed` Filter: `state` field containing `HALT` or `FAULT` string for successful and failed executions respectively. @@ -276,10 +277,10 @@ Example: ### `notification_from_execution` notification -Contains three parameters: container hash (hex-encoded LE Uint256 in a -string), contract script hash (hex-encoded LE Uint160 in a string) and stack -item (encoded the same way as `state` field contents for notifications from -`getapplicationlog` response). +Contains three parameters: contract script hash (hex-encoded LE Uint160 +in a string), notification name and stack item (encoded the same way as +`state` field contents for notifications from `getapplicationlog` +response). Example: @@ -319,7 +320,8 @@ Example: ], "type" : "Array" }, - "contract" : "0x1b4357bff5a01bdf2a6581247cf9ed1e24629176" + "contract" : "0x1b4357bff5a01bdf2a6581247cf9ed1e24629176", + "name" : "transfer", } ] } diff --git a/pkg/rpc/client/wsclient.go b/pkg/rpc/client/wsclient.go index 4ce50b3a1..d88c4855e 100644 --- a/pkg/rpc/client/wsclient.go +++ b/pkg/rpc/client/wsclient.go @@ -269,10 +269,10 @@ func (c *WSClient) SubscribeForNewTransactions(sender *util.Uint160, cosigner *u // generated during transaction execution to this instance of client. It can be // filtered by contract's hash (that emits notifications), nil value puts no such // restrictions. -func (c *WSClient) SubscribeForExecutionNotifications(contract *util.Uint160) (string, error) { +func (c *WSClient) SubscribeForExecutionNotifications(contract *util.Uint160, name *string) (string, error) { params := request.NewRawParams("notification_from_execution") - if contract != nil { - params.Values = append(params.Values, request.NotificationFilter{Contract: *contract}) + if contract != nil || name != nil { + params.Values = append(params.Values, request.NotificationFilter{Contract: contract, Name: name}) } return c.performSubscription(params) } diff --git a/pkg/rpc/client/wsclient_test.go b/pkg/rpc/client/wsclient_test.go index 79e0af19d..97e411cd6 100644 --- a/pkg/rpc/client/wsclient_test.go +++ b/pkg/rpc/client/wsclient_test.go @@ -32,7 +32,7 @@ func TestWSClientSubscription(t *testing.T) { return wsc.SubscribeForNewTransactions(nil, nil) }, "notifications": func(wsc *WSClient) (string, error) { - return wsc.SubscribeForExecutionNotifications(nil) + return wsc.SubscribeForExecutionNotifications(nil, nil) }, "executions": func(wsc *WSClient) (string, error) { return wsc.SubscribeForTransactionExecutions(nil) @@ -240,10 +240,10 @@ func TestWSFilteredSubscriptions(t *testing.T) { require.Equal(t, util.Uint160{0, 42}, *filt.Cosigner) }, }, - {"notifications", + {"notifications contract hash", func(t *testing.T, wsc *WSClient) { contract := util.Uint160{1, 2, 3, 4, 5} - _, err := wsc.SubscribeForExecutionNotifications(&contract) + _, err := wsc.SubscribeForExecutionNotifications(&contract, nil) require.NoError(t, err) }, func(t *testing.T, p *request.Params) { @@ -252,7 +252,41 @@ func TestWSFilteredSubscriptions(t *testing.T) { require.Equal(t, request.NotificationFilterT, param.Type) filt, ok := param.Value.(request.NotificationFilter) require.Equal(t, true, ok) - require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, filt.Contract) + require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Contract) + require.Nil(t, filt.Name) + }, + }, + {"notifications name", + func(t *testing.T, wsc *WSClient) { + name := "my_pretty_notification" + _, err := wsc.SubscribeForExecutionNotifications(nil, &name) + require.NoError(t, err) + }, + func(t *testing.T, p *request.Params) { + param := p.Value(1) + require.NotNil(t, param) + require.Equal(t, request.NotificationFilterT, param.Type) + filt, ok := param.Value.(request.NotificationFilter) + require.Equal(t, true, ok) + require.Equal(t, "my_pretty_notification", *filt.Name) + require.Nil(t, filt.Contract) + }, + }, + {"notifications contract hash and name", + func(t *testing.T, wsc *WSClient) { + contract := util.Uint160{1, 2, 3, 4, 5} + name := "my_pretty_notification" + _, err := wsc.SubscribeForExecutionNotifications(&contract, &name) + require.NoError(t, err) + }, + func(t *testing.T, p *request.Params) { + param := p.Value(1) + require.NotNil(t, param) + require.Equal(t, request.NotificationFilterT, param.Type) + filt, ok := param.Value.(request.NotificationFilter) + require.Equal(t, true, ok) + require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Contract) + require.Equal(t, "my_pretty_notification", *filt.Name) }, }, {"executions", diff --git a/pkg/rpc/request/param.go b/pkg/rpc/request/param.go index 9f67b7200..a3bc975a1 100644 --- a/pkg/rpc/request/param.go +++ b/pkg/rpc/request/param.go @@ -44,9 +44,10 @@ type ( } // NotificationFilter is a wrapper structure representing filter used for // notifications generated during transaction execution. Notifications can - // only be filtered by contract hash. + // be filtered by contract hash and by name. NotificationFilter struct { - Contract util.Uint160 `json:"contract"` + Contract *util.Uint160 `json:"contract,omitempty"` + Name *string `json:"name,omitempty"` } // ExecutionFilter is a wrapper structure used for transaction execution // events. It allows to choose failing or successful transactions based diff --git a/pkg/rpc/request/param_test.go b/pkg/rpc/request/param_test.go index 1a4f44764..a831309f0 100644 --- a/pkg/rpc/request/param_test.go +++ b/pkg/rpc/request/param_test.go @@ -21,11 +21,14 @@ func TestParam_UnmarshalJSON(t *testing.T) { {"cosigner": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"}, {"sender": "f84d6a337fbc3d3a201d41da99e86b479e7a2554", "cosigner": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"}, {"contract": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"}, + {"name": "my_pretty_notification"}, + {"contract": "f84d6a337fbc3d3a201d41da99e86b479e7a2554", "name":"my_pretty_notification"}, {"state": "HALT"}, {"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569"}, [{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": "Global"}]]` contr, err := util.Uint160DecodeStringLE("f84d6a337fbc3d3a201d41da99e86b479e7a2554") require.NoError(t, err) + name := "my_pretty_notification" accountHash, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569") require.NoError(t, err) expected := Params{ @@ -86,7 +89,15 @@ func TestParam_UnmarshalJSON(t *testing.T) { }, { Type: NotificationFilterT, - Value: NotificationFilter{Contract: contr}, + Value: NotificationFilter{Contract: &contr}, + }, + { + Type: NotificationFilterT, + Value: NotificationFilter{Name: &name}, + }, + { + Type: NotificationFilterT, + Value: NotificationFilter{Contract: &contr, Name: &name}, }, { Type: ExecutionFilterT, diff --git a/pkg/rpc/server/subscription.go b/pkg/rpc/server/subscription.go index d4409e3e4..c51de4ea2 100644 --- a/pkg/rpc/server/subscription.go +++ b/pkg/rpc/server/subscription.go @@ -73,7 +73,9 @@ func (f *feed) Matches(r *response.Notification) bool { case response.NotificationEventID: filt := f.filter.(request.NotificationFilter) notification := r.Payload[0].(result.NotificationEvent) - return notification.Contract.Equals(filt.Contract) + hashOk := filt.Contract == nil || notification.Contract.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) diff --git a/pkg/rpc/server/subscription_test.go b/pkg/rpc/server/subscription_test.go index c4a81f9ca..5a2f70805 100644 --- a/pkg/rpc/server/subscription_test.go +++ b/pkg/rpc/server/subscription_test.go @@ -171,7 +171,7 @@ func TestFilteredSubscriptions(t *testing.T) { require.Equal(t, "0x"+goodSender.StringLE(), cosigner0acc) }, }, - "notification matching": { + "notification matching contract hash": { params: `["notification_from_execution", {"contract":"` + testContractHash + `"}]`, check: func(t *testing.T, resp *response.Notification) { rmap := resp.Payload[0].(map[string]interface{}) @@ -180,6 +180,26 @@ func TestFilteredSubscriptions(t *testing.T) { require.Equal(t, "0x"+testContractHash, c) }, }, + "notification matching name": { + params: `["notification_from_execution", {"name":"my_pretty_notification"}]`, + check: func(t *testing.T, resp *response.Notification) { + rmap := resp.Payload[0].(map[string]interface{}) + require.Equal(t, response.NotificationEventID, resp.Event) + n := rmap["name"].(string) + require.Equal(t, "my_pretty_notification", n) + }, + }, + "notification matching contract hash and name": { + params: `["notification_from_execution", {"contract":"` + testContractHash + `", "name":"my_pretty_notification"}]`, + check: func(t *testing.T, resp *response.Notification) { + rmap := resp.Payload[0].(map[string]interface{}) + require.Equal(t, response.NotificationEventID, resp.Event) + c := rmap["contract"].(string) + require.Equal(t, "0x"+testContractHash, c) + n := rmap["name"].(string) + require.Equal(t, "my_pretty_notification", n) + }, + }, "execution matching": { params: `["transaction_executed", {"state":"HALT"}]`, check: func(t *testing.T, resp *response.Notification) { @@ -327,7 +347,8 @@ func TestBadSubUnsub(t *testing.T) { "block invalid filter": `{"jsonrpc": "2.0", "method": "subscribe", "params": ["block_added", 1], "id": 1}`, "tx filter 1": `{"jsonrpc": "2.0", "method": "subscribe", "params": ["transaction_added", 1], "id": 1}`, "tx filter 2": `{"jsonrpc": "2.0", "method": "subscribe", "params": ["transaction_added", {"state": "HALT"}], "id": 1}`, - "notification filter": `{"jsonrpc": "2.0", "method": "subscribe", "params": ["notification_from_execution", "contract"], "id": 1}`, + "notification filter 1": `{"jsonrpc": "2.0", "method": "subscribe", "params": ["notification_from_execution", "contract"], "id": 1}`, + "notification filter 2": `{"jsonrpc": "2.0", "method": "subscribe", "params": ["notification_from_execution", "name"], "id": 1}`, "execution filter 1": `{"jsonrpc": "2.0", "method": "subscribe", "params": ["transaction_executed", "FAULT"], "id": 1}`, "execution filter 2": `{"jsonrpc": "2.0", "method": "subscribe", "params": ["transaction_executed", {"state": "STOP"}], "id": 1}`, }