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  }