github.com/evdatsion/aphelion-dpos-bft@v0.32.1/rpc/core/events.go (about) 1 package core 2 3 import ( 4 "context" 5 "fmt" 6 7 "github.com/pkg/errors" 8 9 tmpubsub "github.com/evdatsion/aphelion-dpos-bft/libs/pubsub" 10 tmquery "github.com/evdatsion/aphelion-dpos-bft/libs/pubsub/query" 11 ctypes "github.com/evdatsion/aphelion-dpos-bft/rpc/core/types" 12 rpctypes "github.com/evdatsion/aphelion-dpos-bft/rpc/lib/types" 13 ) 14 15 // Subscribe for events via WebSocket. 16 // 17 // To tell which events you want, you need to provide a query. query is a 18 // string, which has a form: "condition AND condition ..." (no OR at the 19 // moment). condition has a form: "key operation operand". key is a string with 20 // a restricted set of possible symbols ( \t\n\r\\()"'=>< are not allowed). 21 // operation can be "=", "<", "<=", ">", ">=", "CONTAINS". operand can be a 22 // string (escaped with single quotes), number, date or time. 23 // 24 // Examples: 25 // tm.event = 'NewBlock' # new blocks 26 // tm.event = 'CompleteProposal' # node got a complete proposal 27 // tm.event = 'Tx' AND tx.hash = 'XYZ' # single transaction 28 // tm.event = 'Tx' AND tx.height = 5 # all txs of the fifth block 29 // tx.height = 5 # all txs of the fifth block 30 // 31 // Tendermint provides a few predefined keys: tm.event, tx.hash and tx.height. 32 // Note for transactions, you can define additional keys by providing events with 33 // DeliverTx response. 34 // 35 // import ( 36 // abci "github.com/evdatsion/aphelion-dpos-bft/abci/types" 37 // "github.com/evdatsion/aphelion-dpos-bft/libs/pubsub/query" 38 // ) 39 // 40 // abci.ResponseDeliverTx{ 41 // Events: []abci.Event{ 42 // { 43 // Type: "rewards.withdraw", 44 // Attributes: cmn.KVPairs{ 45 // cmn.KVPair{Key: []byte("address"), Value: []byte("AddrA")}, 46 // cmn.KVPair{Key: []byte("source"), Value: []byte("SrcX")}, 47 // cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, 48 // cmn.KVPair{Key: []byte("balance"), Value: []byte("...")}, 49 // }, 50 // }, 51 // { 52 // Type: "rewards.withdraw", 53 // Attributes: cmn.KVPairs{ 54 // cmn.KVPair{Key: []byte("address"), Value: []byte("AddrB")}, 55 // cmn.KVPair{Key: []byte("source"), Value: []byte("SrcY")}, 56 // cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, 57 // cmn.KVPair{Key: []byte("balance"), Value: []byte("...")}, 58 // }, 59 // }, 60 // { 61 // Type: "transfer", 62 // Attributes: cmn.KVPairs{ 63 // cmn.KVPair{Key: []byte("sender"), Value: []byte("AddrC")}, 64 // cmn.KVPair{Key: []byte("recipient"), Value: []byte("AddrD")}, 65 // cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, 66 // }, 67 // }, 68 // }, 69 // } 70 // 71 // All events are indexed by a composite key of the form {eventType}.{evenAttrKey}. 72 // In the above examples, the following keys would be indexed: 73 // - rewards.withdraw.address 74 // - rewards.withdraw.source 75 // - rewards.withdraw.amount 76 // - rewards.withdraw.balance 77 // - transfer.sender 78 // - transfer.recipient 79 // - transfer.amount 80 // 81 // Multiple event types with duplicate keys are allowed and are meant to 82 // categorize unique and distinct events. In the above example, all events 83 // indexed under the key `rewards.withdraw.address` will have the following 84 // values stored and queryable: 85 // 86 // - AddrA 87 // - AddrB 88 // 89 // To create a query for txs where address AddrA withdrew rewards: 90 // query.MustParse("tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA'") 91 // 92 // To create a query for txs where address AddrA withdrew rewards from source Y: 93 // query.MustParse("tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA' AND rewards.withdraw.source = 'Y'") 94 // 95 // To create a query for txs where AddrA transferred funds: 96 // query.MustParse("tm.event = 'Tx' AND transfer.sender = 'AddrA'") 97 // 98 // The following queries would return no results: 99 // query.MustParse("tm.event = 'Tx' AND transfer.sender = 'AddrZ'") 100 // query.MustParse("tm.event = 'Tx' AND rewards.withdraw.address = 'AddrZ'") 101 // query.MustParse("tm.event = 'Tx' AND rewards.withdraw.source = 'W'") 102 // 103 // See list of all possible events here 104 // https://godoc.org/github.com/evdatsion/aphelion-dpos-bft/types#pkg-constants 105 // 106 // For complete query syntax, check out 107 // https://godoc.org/github.com/evdatsion/aphelion-dpos-bft/libs/pubsub/query. 108 // 109 // ```go 110 // import "github.com/evdatsion/aphelion-dpos-bft/types" 111 // 112 // client := client.NewHTTP("tcp://0.0.0.0:26657", "/websocket") 113 // err := client.Start() 114 // if err != nil { 115 // // handle error 116 // } 117 // defer client.Stop() 118 // ctx, cancel := context.WithTimeout(context.Background(), 1 * time.Second) 119 // defer cancel() 120 // query := "tm.event = 'Tx' AND tx.height = 3" 121 // txs, err := client.Subscribe(ctx, "test-client", query) 122 // if err != nil { 123 // // handle error 124 // } 125 // 126 // go func() { 127 // for e := range txs { 128 // fmt.Println("got ", e.Data.(types.EventDataTx)) 129 // } 130 // }() 131 // ``` 132 // 133 // > The above command returns JSON structured like this: 134 // 135 // ```json 136 // { 137 // "error": "", 138 // "result": {}, 139 // "id": "", 140 // "jsonrpc": "2.0" 141 // } 142 // ``` 143 // 144 // ### Query Parameters 145 // 146 // | Parameter | Type | Default | Required | Description | 147 // |-----------+--------+---------+----------+-------------| 148 // | query | string | "" | true | Query | 149 // 150 // <aside class="notice">WebSocket only</aside> 151 func Subscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, error) { 152 addr := ctx.RemoteAddr() 153 154 if eventBus.NumClients() >= config.MaxSubscriptionClients { 155 return nil, fmt.Errorf("max_subscription_clients %d reached", config.MaxSubscriptionClients) 156 } else if eventBus.NumClientSubscriptions(addr) >= config.MaxSubscriptionsPerClient { 157 return nil, fmt.Errorf("max_subscriptions_per_client %d reached", config.MaxSubscriptionsPerClient) 158 } 159 160 logger.Info("Subscribe to query", "remote", addr, "query", query) 161 162 q, err := tmquery.New(query) 163 if err != nil { 164 return nil, errors.Wrap(err, "failed to parse query") 165 } 166 167 subCtx, cancel := context.WithTimeout(ctx.Context(), SubscribeTimeout) 168 defer cancel() 169 170 sub, err := eventBus.Subscribe(subCtx, addr, q) 171 if err != nil { 172 return nil, err 173 } 174 175 go func() { 176 for { 177 select { 178 case msg := <-sub.Out(): 179 resultEvent := &ctypes.ResultEvent{Query: query, Data: msg.Data(), Events: msg.Events()} 180 ctx.WSConn.TryWriteRPCResponse( 181 rpctypes.NewRPCSuccessResponse( 182 ctx.WSConn.Codec(), 183 rpctypes.JSONRPCStringID(fmt.Sprintf("%v#event", ctx.JSONReq.ID)), 184 resultEvent, 185 )) 186 case <-sub.Cancelled(): 187 if sub.Err() != tmpubsub.ErrUnsubscribed { 188 var reason string 189 if sub.Err() == nil { 190 reason = "Tendermint exited" 191 } else { 192 reason = sub.Err().Error() 193 } 194 ctx.WSConn.TryWriteRPCResponse( 195 rpctypes.RPCServerError(rpctypes.JSONRPCStringID( 196 fmt.Sprintf("%v#event", ctx.JSONReq.ID)), 197 fmt.Errorf("subscription was cancelled (reason: %s)", reason), 198 )) 199 } 200 return 201 } 202 } 203 }() 204 205 return &ctypes.ResultSubscribe{}, nil 206 } 207 208 // Unsubscribe from events via WebSocket. 209 // 210 // ```go 211 // client := client.NewHTTP("tcp://0.0.0.0:26657", "/websocket") 212 // err := client.Start() 213 // if err != nil { 214 // // handle error 215 // } 216 // defer client.Stop() 217 // query := "tm.event = 'Tx' AND tx.height = 3" 218 // err = client.Unsubscribe(context.Background(), "test-client", query) 219 // if err != nil { 220 // // handle error 221 // } 222 // ``` 223 // 224 // > The above command returns JSON structured like this: 225 // 226 // ```json 227 // { 228 // "error": "", 229 // "result": {}, 230 // "id": "", 231 // "jsonrpc": "2.0" 232 // } 233 // ``` 234 // 235 // ### Query Parameters 236 // 237 // | Parameter | Type | Default | Required | Description | 238 // |-----------+--------+---------+----------+-------------| 239 // | query | string | "" | true | Query | 240 // 241 // <aside class="notice">WebSocket only</aside> 242 func Unsubscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultUnsubscribe, error) { 243 addr := ctx.RemoteAddr() 244 logger.Info("Unsubscribe from query", "remote", addr, "query", query) 245 q, err := tmquery.New(query) 246 if err != nil { 247 return nil, errors.Wrap(err, "failed to parse query") 248 } 249 err = eventBus.Unsubscribe(context.Background(), addr, q) 250 if err != nil { 251 return nil, err 252 } 253 return &ctypes.ResultUnsubscribe{}, nil 254 } 255 256 // Unsubscribe from all events via WebSocket. 257 // 258 // ```go 259 // client := client.NewHTTP("tcp://0.0.0.0:26657", "/websocket") 260 // err := client.Start() 261 // if err != nil { 262 // // handle error 263 // } 264 // defer client.Stop() 265 // err = client.UnsubscribeAll(context.Background(), "test-client") 266 // if err != nil { 267 // // handle error 268 // } 269 // ``` 270 // 271 // > The above command returns JSON structured like this: 272 // 273 // ```json 274 // { 275 // "error": "", 276 // "result": {}, 277 // "id": "", 278 // "jsonrpc": "2.0" 279 // } 280 // ``` 281 // 282 // <aside class="notice">WebSocket only</aside> 283 func UnsubscribeAll(ctx *rpctypes.Context) (*ctypes.ResultUnsubscribe, error) { 284 addr := ctx.RemoteAddr() 285 logger.Info("Unsubscribe from all", "remote", addr) 286 err := eventBus.UnsubscribeAll(context.Background(), addr) 287 if err != nil { 288 return nil, err 289 } 290 return &ctypes.ResultUnsubscribe{}, nil 291 }