github.com/fibonacci-chain/fbc@v0.0.0-20231124064014-c7636198c1e9/app/rpc/websockets/pubsub_api.go (about)

     1  package websockets
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  
     7  	"github.com/fibonacci-chain/fbc/libs/tendermint/libs/log"
     8  	coretypes "github.com/fibonacci-chain/fbc/libs/tendermint/rpc/core/types"
     9  	tmtypes "github.com/fibonacci-chain/fbc/libs/tendermint/types"
    10  	"github.com/fibonacci-chain/fbc/x/evm/watcher"
    11  
    12  	"github.com/ethereum/go-ethereum/common"
    13  	"github.com/ethereum/go-ethereum/common/hexutil"
    14  	"github.com/ethereum/go-ethereum/eth/filters"
    15  	"github.com/ethereum/go-ethereum/rpc"
    16  	"github.com/fibonacci-chain/fbc/libs/cosmos-sdk/client/context"
    17  
    18  	rpcfilters "github.com/fibonacci-chain/fbc/app/rpc/namespaces/eth/filters"
    19  	rpctypes "github.com/fibonacci-chain/fbc/app/rpc/types"
    20  	evmtypes "github.com/fibonacci-chain/fbc/x/evm/types"
    21  )
    22  
    23  // PubSubAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec
    24  type PubSubAPI struct {
    25  	clientCtx context.CLIContext
    26  	events    *rpcfilters.EventSystem
    27  	filtersMu *sync.RWMutex
    28  	filters   map[rpc.ID]*wsSubscription
    29  	logger    log.Logger
    30  }
    31  
    32  // NewAPI creates an instance of the ethereum PubSub API.
    33  func NewAPI(clientCtx context.CLIContext, log log.Logger) *PubSubAPI {
    34  	return &PubSubAPI{
    35  		clientCtx: clientCtx,
    36  		events:    rpcfilters.NewEventSystem(clientCtx.Client),
    37  		filtersMu: new(sync.RWMutex),
    38  		filters:   make(map[rpc.ID]*wsSubscription),
    39  		logger:    log.With("module", "websocket-client"),
    40  	}
    41  }
    42  
    43  func (api *PubSubAPI) subscribe(conn *wsConn, params []interface{}) (rpc.ID, error) {
    44  	method, ok := params[0].(string)
    45  	if !ok {
    46  		return "0", fmt.Errorf("invalid parameters")
    47  	}
    48  
    49  	switch method {
    50  	case "newHeads":
    51  		// TODO: handle extra params
    52  		return api.subscribeNewHeads(conn)
    53  	case "logs":
    54  		var p interface{}
    55  		if len(params) > 1 {
    56  			p = params[1]
    57  		}
    58  
    59  		return api.subscribeLogs(conn, p)
    60  	case "newPendingTransactions":
    61  		var isDetail, ok bool
    62  		if len(params) > 1 {
    63  			isDetail, ok = params[1].(bool)
    64  			if !ok {
    65  				return "0", fmt.Errorf("invalid parameters")
    66  			}
    67  		}
    68  		return api.subscribePendingTransactions(conn, isDetail)
    69  	case "syncing":
    70  		return api.subscribeSyncing(conn)
    71  	case "blockTime":
    72  		return api.subscribeLatestBlockTime(conn)
    73  
    74  	default:
    75  		return "0", fmt.Errorf("unsupported method %s", method)
    76  	}
    77  }
    78  
    79  func (api *PubSubAPI) unsubscribe(id rpc.ID) bool {
    80  	api.filtersMu.Lock()
    81  	defer api.filtersMu.Unlock()
    82  
    83  	if api.filters[id] == nil {
    84  		api.logger.Debug("client doesn't exist in filters", "ID", id)
    85  		return false
    86  	}
    87  	if api.filters[id].sub != nil {
    88  		api.filters[id].sub.Unsubscribe(api.events)
    89  	}
    90  	close(api.filters[id].unsubscribed)
    91  	delete(api.filters, id)
    92  	api.logger.Debug("close client channel & delete client from filters", "ID", id)
    93  	return true
    94  }
    95  
    96  func (api *PubSubAPI) subscribeNewHeads(conn *wsConn) (rpc.ID, error) {
    97  	sub, _, err := api.events.SubscribeNewHeads()
    98  	if err != nil {
    99  		return "", fmt.Errorf("error creating block filter: %s", err.Error())
   100  	}
   101  
   102  	unsubscribed := make(chan struct{})
   103  	api.filtersMu.Lock()
   104  	api.filters[sub.ID()] = &wsSubscription{
   105  		sub:          sub,
   106  		conn:         conn,
   107  		unsubscribed: unsubscribed,
   108  	}
   109  	api.filtersMu.Unlock()
   110  
   111  	go func(headersCh <-chan coretypes.ResultEvent, errCh <-chan error) {
   112  		for {
   113  			select {
   114  			case event := <-headersCh:
   115  				data, ok := event.Data.(tmtypes.EventDataNewBlockHeader)
   116  				if !ok {
   117  					api.logger.Error(fmt.Sprintf("invalid data type %T, expected EventDataTx", event.Data), "ID", sub.ID())
   118  					continue
   119  				}
   120  				headerWithBlockHash, err := rpctypes.EthHeaderWithBlockHashFromTendermint(&data.Header)
   121  				if err != nil {
   122  					api.logger.Error("failed to get header with block hash", "error", err)
   123  					continue
   124  				}
   125  
   126  				api.filtersMu.RLock()
   127  				if f, found := api.filters[sub.ID()]; found {
   128  					// write to ws conn
   129  					res := &SubscriptionNotification{
   130  						Jsonrpc: "2.0",
   131  						Method:  "eth_subscription",
   132  						Params: &SubscriptionResult{
   133  							Subscription: sub.ID(),
   134  							Result:       headerWithBlockHash,
   135  						},
   136  					}
   137  
   138  					err = f.conn.WriteJSON(res)
   139  					if err != nil {
   140  						api.logger.Error("failed to write header", "ID", sub.ID(), "blockNumber", headerWithBlockHash.Number, "error", err)
   141  					} else {
   142  						api.logger.Debug("successfully write header", "ID", sub.ID(), "blockNumber", headerWithBlockHash.Number)
   143  					}
   144  				}
   145  				api.filtersMu.RUnlock()
   146  
   147  				if err != nil {
   148  					api.unsubscribe(sub.ID())
   149  				}
   150  			case err := <-errCh:
   151  				if err != nil {
   152  					api.unsubscribe(sub.ID())
   153  					api.logger.Error("websocket recv error, close the conn", "ID", sub.ID(), "error", err)
   154  				}
   155  				return
   156  			case <-unsubscribed:
   157  				api.logger.Debug("NewHeads channel is closed", "ID", sub.ID())
   158  				return
   159  			}
   160  		}
   161  	}(sub.Event(), sub.Err())
   162  
   163  	return sub.ID(), nil
   164  }
   165  
   166  func (api *PubSubAPI) subscribeLogs(conn *wsConn, extra interface{}) (rpc.ID, error) {
   167  	crit := filters.FilterCriteria{}
   168  	bytx := false // batch logs push by tx
   169  
   170  	if extra != nil {
   171  		params, ok := extra.(map[string]interface{})
   172  		if !ok {
   173  			return "", fmt.Errorf("invalid criteria")
   174  		}
   175  
   176  		if params["address"] != nil {
   177  			address, ok := params["address"].(string)
   178  			addresses, sok := params["address"].([]interface{})
   179  			if !ok && !sok {
   180  				return "", fmt.Errorf("invalid address; must be address or array of addresses")
   181  			}
   182  
   183  			if ok {
   184  				if !common.IsHexAddress(address) {
   185  					return "", fmt.Errorf("invalid address")
   186  				}
   187  				crit.Addresses = []common.Address{common.HexToAddress(address)}
   188  			} else if sok {
   189  				crit.Addresses = []common.Address{}
   190  				for _, addr := range addresses {
   191  					address, ok := addr.(string)
   192  					if !ok || !common.IsHexAddress(address) {
   193  						return "", fmt.Errorf("invalid address")
   194  					}
   195  
   196  					crit.Addresses = append(crit.Addresses, common.HexToAddress(address))
   197  				}
   198  			}
   199  		}
   200  
   201  		if params["topics"] != nil {
   202  			topics, ok := params["topics"].([]interface{})
   203  			if !ok {
   204  				return "", fmt.Errorf("invalid topics")
   205  			}
   206  
   207  			topicFilterLists, err := resolveTopicList(topics)
   208  			if err != nil {
   209  				return "", fmt.Errorf("invalid topics")
   210  			}
   211  			crit.Topics = topicFilterLists
   212  		}
   213  
   214  		if params["bytx"] != nil {
   215  			b, ok := params["bytx"].(bool)
   216  			if !ok {
   217  				return "", fmt.Errorf("invalid batch; must be true or false")
   218  			}
   219  			bytx = b
   220  		}
   221  	}
   222  
   223  	sub, _, err := api.events.SubscribeLogsBatch(crit)
   224  	if err != nil {
   225  		return rpc.ID(""), err
   226  	}
   227  
   228  	unsubscribed := make(chan struct{})
   229  	api.filtersMu.Lock()
   230  	api.filters[sub.ID()] = &wsSubscription{
   231  		sub:          sub,
   232  		conn:         conn,
   233  		unsubscribed: unsubscribed,
   234  	}
   235  	api.filtersMu.Unlock()
   236  
   237  	go func(ch <-chan coretypes.ResultEvent, errCh <-chan error) {
   238  		quit := false
   239  		for {
   240  			select {
   241  			case event := <-ch:
   242  				go func(event coretypes.ResultEvent) {
   243  					//batch receive txResult
   244  					txs, ok := event.Data.(tmtypes.EventDataTxs)
   245  					if !ok {
   246  						api.logger.Error(fmt.Sprintf("invalid event data %T, expected EventDataTxs", event.Data))
   247  						return
   248  					}
   249  
   250  					for _, txResult := range txs.Results {
   251  						if quit {
   252  							return
   253  						}
   254  
   255  						//check evm type event
   256  						if !evmtypes.IsEvmEvent(txResult) {
   257  							continue
   258  						}
   259  
   260  						//decode txResult data
   261  						var resultData evmtypes.ResultData
   262  						resultData, err = evmtypes.DecodeResultData(txResult.Data)
   263  						if err != nil {
   264  							api.logger.Error("failed to decode result data", "error", err)
   265  							return
   266  						}
   267  
   268  						//filter logs
   269  						logs := rpcfilters.FilterLogs(resultData.Logs, crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics)
   270  						if len(logs) == 0 {
   271  							continue
   272  						}
   273  
   274  						//write log to client by each tx
   275  						api.filtersMu.RLock()
   276  						if f, found := api.filters[sub.ID()]; found {
   277  							// write to ws conn
   278  							res := &SubscriptionNotification{
   279  								Jsonrpc: "2.0",
   280  								Method:  "eth_subscription",
   281  								Params: &SubscriptionResult{
   282  									Subscription: sub.ID(),
   283  								},
   284  							}
   285  							if bytx {
   286  								res.Params.Result = logs
   287  								err = f.conn.WriteJSON(res)
   288  								if err != nil {
   289  									api.logger.Error("failed to batch write logs", "ID", sub.ID(), "height", logs[0].BlockNumber, "txHash", logs[0].TxHash, "error", err)
   290  								}
   291  								api.logger.Info("successfully batch write logs ", "ID", sub.ID(), "height", logs[0].BlockNumber, "txHash", logs[0].TxHash)
   292  							} else {
   293  								for _, singleLog := range logs {
   294  									res.Params.Result = singleLog
   295  									err = f.conn.WriteJSON(res)
   296  									if err != nil {
   297  										api.logger.Error("failed to write log", "ID", sub.ID(), "height", singleLog.BlockNumber, "txHash", singleLog.TxHash, "error", err)
   298  										break
   299  									}
   300  									api.logger.Info("successfully write log", "ID", sub.ID(), "height", singleLog.BlockNumber, "txHash", singleLog.TxHash)
   301  								}
   302  							}
   303  						}
   304  						api.filtersMu.RUnlock()
   305  
   306  						if err != nil {
   307  							//unsubscribe and quit current routine
   308  							api.unsubscribe(sub.ID())
   309  							return
   310  						}
   311  					}
   312  				}(event)
   313  			case err := <-errCh:
   314  				quit = true
   315  				if err != nil {
   316  					api.unsubscribe(sub.ID())
   317  					api.logger.Error("websocket recv error, close the conn", "ID", sub.ID(), "error", err)
   318  				}
   319  				return
   320  			case <-unsubscribed:
   321  				quit = true
   322  				api.logger.Debug("Logs channel is closed", "ID", sub.ID())
   323  				return
   324  			}
   325  		}
   326  	}(sub.Event(), sub.Err())
   327  
   328  	return sub.ID(), nil
   329  }
   330  
   331  func resolveTopicList(params []interface{}) ([][]common.Hash, error) {
   332  	topicFilterLists := make([][]common.Hash, len(params))
   333  	for i, param := range params { // eg: ["0xddf252......f523b3ef", null, ["0x000000......32fea9e4", "0x000000......ab14dc5d"]]
   334  		if param == nil {
   335  			// 1.1 if the topic is null
   336  			topicFilterLists[i] = nil
   337  		} else {
   338  			// 2.1 judge if the param is the type of string or not
   339  			topicStr, ok := param.(string)
   340  			// 2.1 judge if the param is the type of string slice or not
   341  			topicSlices, sok := param.([]interface{})
   342  			if !ok && !sok {
   343  				// if both judgement are false, return invalid topics
   344  				return topicFilterLists, fmt.Errorf("invalid topics")
   345  			}
   346  
   347  			if ok {
   348  				// 2.2 This is string
   349  				// 2.3 judge the topic is a valid hex hash or not
   350  				if !IsHexHash(topicStr) {
   351  					return topicFilterLists, fmt.Errorf("invalid topics")
   352  				}
   353  				// 2.4 add this topic to topic-hash-lists
   354  				topicHash := common.HexToHash(topicStr)
   355  				topicFilterLists[i] = []common.Hash{topicHash}
   356  			} else if sok {
   357  				// 2.2 This is slice of string
   358  				topicHashes := make([]common.Hash, len(topicSlices))
   359  				for n, topicStr := range topicSlices {
   360  					//2.3 judge every topic
   361  					topicHash, ok := topicStr.(string)
   362  					if !ok || !IsHexHash(topicHash) {
   363  						return topicFilterLists, fmt.Errorf("invalid topics")
   364  					}
   365  					topicHashes[n] = common.HexToHash(topicHash)
   366  				}
   367  				// 2.4 add this topic slice to topic-hash-lists
   368  				topicFilterLists[i] = topicHashes
   369  			}
   370  		}
   371  	}
   372  	return topicFilterLists, nil
   373  }
   374  
   375  func IsHexHash(s string) bool {
   376  	if has0xPrefix(s) {
   377  		s = s[2:]
   378  	}
   379  	return len(s) == 2*common.HashLength && isHex(s)
   380  }
   381  
   382  // has0xPrefix validates str begins with '0x' or '0X'.
   383  func has0xPrefix(str string) bool {
   384  	return len(str) >= 2 && str[0] == '0' && (str[1] == 'x' || str[1] == 'X')
   385  }
   386  
   387  // isHexCharacter returns bool of c being a valid hexadecimal.
   388  func isHexCharacter(c byte) bool {
   389  	return ('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F')
   390  }
   391  
   392  // isHex validates whether each byte is valid hexadecimal string.
   393  func isHex(str string) bool {
   394  	if len(str)%2 != 0 {
   395  		return false
   396  	}
   397  	for _, c := range []byte(str) {
   398  		if !isHexCharacter(c) {
   399  			return false
   400  		}
   401  	}
   402  	return true
   403  }
   404  
   405  func (api *PubSubAPI) subscribePendingTransactions(conn *wsConn, isDetail bool) (rpc.ID, error) {
   406  	sub, _, err := api.events.SubscribePendingTxs()
   407  	if err != nil {
   408  		return "", fmt.Errorf("error creating block filter: %s", err.Error())
   409  	}
   410  
   411  	unsubscribed := make(chan struct{})
   412  	api.filtersMu.Lock()
   413  	api.filters[sub.ID()] = &wsSubscription{
   414  		sub:          sub,
   415  		conn:         conn,
   416  		unsubscribed: unsubscribed,
   417  	}
   418  	api.filtersMu.Unlock()
   419  
   420  	go func(txsCh <-chan coretypes.ResultEvent, errCh <-chan error) {
   421  		for {
   422  			select {
   423  			case ev := <-txsCh:
   424  				data, ok := ev.Data.(tmtypes.EventDataTx)
   425  				if !ok {
   426  					api.logger.Error(fmt.Sprintf("invalid data type %T, expected EventDataTx", ev.Data), "ID", sub.ID())
   427  					continue
   428  				}
   429  				txHash := common.BytesToHash(data.Tx.Hash(data.Height))
   430  				var res interface{} = txHash
   431  				if isDetail {
   432  					ethTx, err := rpctypes.RawTxToEthTx(api.clientCtx, data.Tx, data.Height)
   433  					if err != nil {
   434  						api.logger.Error("failed to decode raw tx to eth tx", "hash", txHash.String(), "error", err)
   435  						continue
   436  					}
   437  
   438  					tx, err := watcher.NewTransaction(ethTx, txHash, common.Hash{}, uint64(data.Height), uint64(data.Index))
   439  					if err != nil {
   440  						api.logger.Error("failed to new transaction", "hash", txHash.String(), "error", err)
   441  						continue
   442  					}
   443  					res = tx
   444  				}
   445  				api.filtersMu.RLock()
   446  				if f, found := api.filters[sub.ID()]; found {
   447  					// write to ws conn
   448  					res := &SubscriptionNotification{
   449  						Jsonrpc: "2.0",
   450  						Method:  "eth_subscription",
   451  						Params: &SubscriptionResult{
   452  							Subscription: sub.ID(),
   453  							Result:       res,
   454  						},
   455  					}
   456  
   457  					err = f.conn.WriteJSON(res)
   458  					if err != nil {
   459  						api.logger.Error("failed to write pending tx", "ID", sub.ID(), "error", err)
   460  					} else {
   461  						api.logger.Info("successfully write pending tx", "ID", sub.ID(), "txHash", txHash)
   462  					}
   463  				}
   464  				api.filtersMu.RUnlock()
   465  
   466  				if err != nil {
   467  					api.unsubscribe(sub.ID())
   468  				}
   469  			case err := <-errCh:
   470  				if err != nil {
   471  					api.unsubscribe(sub.ID())
   472  					api.logger.Error("websocket recv error, close the conn", "ID", sub.ID(), "error", err)
   473  				}
   474  				return
   475  			case <-unsubscribed:
   476  				api.logger.Debug("PendingTransactions channel is closed", "ID", sub.ID())
   477  				return
   478  			}
   479  		}
   480  	}(sub.Event(), sub.Err())
   481  
   482  	return sub.ID(), nil
   483  }
   484  
   485  func (api *PubSubAPI) subscribeSyncing(conn *wsConn) (rpc.ID, error) {
   486  	sub, _, err := api.events.SubscribeNewHeads()
   487  	if err != nil {
   488  		return "", fmt.Errorf("error creating block filter: %s", err.Error())
   489  	}
   490  
   491  	unsubscribed := make(chan struct{})
   492  	api.filtersMu.Lock()
   493  	api.filters[sub.ID()] = &wsSubscription{
   494  		sub:          sub,
   495  		conn:         conn,
   496  		unsubscribed: unsubscribed,
   497  	}
   498  	api.filtersMu.Unlock()
   499  
   500  	status, err := api.clientCtx.Client.Status()
   501  	if err != nil {
   502  		return "", fmt.Errorf("error get sync status: %s", err.Error())
   503  	}
   504  	startingBlock := hexutil.Uint64(status.SyncInfo.EarliestBlockHeight)
   505  	highestBlock := hexutil.Uint64(0)
   506  
   507  	var result interface{}
   508  
   509  	go func(headersCh <-chan coretypes.ResultEvent, errCh <-chan error) {
   510  		for {
   511  			select {
   512  			case <-headersCh:
   513  
   514  				newStatus, err := api.clientCtx.Client.Status()
   515  				if err != nil {
   516  					api.logger.Error(fmt.Sprintf("error get sync status: %s", err.Error()))
   517  					continue
   518  				}
   519  
   520  				if !newStatus.SyncInfo.CatchingUp {
   521  					result = false
   522  				} else {
   523  					result = map[string]interface{}{
   524  						"startingBlock": startingBlock,
   525  						"currentBlock":  hexutil.Uint64(newStatus.SyncInfo.LatestBlockHeight),
   526  						"highestBlock":  highestBlock,
   527  					}
   528  				}
   529  
   530  				api.filtersMu.RLock()
   531  				if f, found := api.filters[sub.ID()]; found {
   532  					// write to ws conn
   533  					res := &SubscriptionNotification{
   534  						Jsonrpc: "2.0",
   535  						Method:  "eth_subscription",
   536  						Params: &SubscriptionResult{
   537  							Subscription: sub.ID(),
   538  							Result:       result,
   539  						},
   540  					}
   541  
   542  					err = f.conn.WriteJSON(res)
   543  					if err != nil {
   544  						api.logger.Error("failed to write syncing status", "ID", sub.ID(), "error", err)
   545  					} else {
   546  						api.logger.Debug("successfully write syncing status", "ID", sub.ID())
   547  					}
   548  				}
   549  				api.filtersMu.RUnlock()
   550  
   551  				if err != nil {
   552  					api.unsubscribe(sub.ID())
   553  				}
   554  
   555  			case err := <-errCh:
   556  				if err != nil {
   557  					api.unsubscribe(sub.ID())
   558  					api.logger.Error("websocket recv error, close the conn", "ID", sub.ID(), "error", err)
   559  				}
   560  				return
   561  			case <-unsubscribed:
   562  				api.logger.Debug("Syncing channel is closed", "ID", sub.ID())
   563  				return
   564  			}
   565  		}
   566  	}(sub.Event(), sub.Err())
   567  
   568  	return sub.ID(), nil
   569  }
   570  
   571  func (api *PubSubAPI) subscribeLatestBlockTime(conn *wsConn) (rpc.ID, error) {
   572  	sub, _, err := api.events.SubscribeBlockTime()
   573  	if err != nil {
   574  		return "", fmt.Errorf("error creating block filter: %s", err.Error())
   575  	}
   576  
   577  	unsubscribed := make(chan struct{})
   578  	api.filtersMu.Lock()
   579  	api.filters[sub.ID()] = &wsSubscription{
   580  		sub:          sub,
   581  		conn:         conn,
   582  		unsubscribed: unsubscribed,
   583  	}
   584  	api.filtersMu.Unlock()
   585  
   586  	go func(txsCh <-chan coretypes.ResultEvent, errCh <-chan error) {
   587  		for {
   588  			select {
   589  			case ev := <-txsCh:
   590  				result, ok := ev.Data.(tmtypes.EventDataBlockTime)
   591  				if !ok {
   592  					api.logger.Error(fmt.Sprintf("invalid data type %T, expected EventDataTx", ev.Data), "ID", sub.ID())
   593  					continue
   594  				}
   595  
   596  				api.filtersMu.RLock()
   597  				if f, found := api.filters[sub.ID()]; found {
   598  					// write to ws conn
   599  					res := &SubscriptionNotification{
   600  						Jsonrpc: "2.0",
   601  						Method:  "eth_subscription",
   602  						Params: &SubscriptionResult{
   603  							Subscription: sub.ID(),
   604  							Result:       result,
   605  						},
   606  					}
   607  
   608  					err = f.conn.WriteJSON(res)
   609  					if err != nil {
   610  						api.logger.Error("failed to write latest blocktime", "ID", sub.ID(), "error", err)
   611  					} else {
   612  						api.logger.Debug("successfully write latest blocktime", "ID", sub.ID(), "data", result)
   613  					}
   614  				}
   615  				api.filtersMu.RUnlock()
   616  
   617  				if err != nil {
   618  					api.unsubscribe(sub.ID())
   619  				}
   620  			case err := <-errCh:
   621  				if err != nil {
   622  					api.unsubscribe(sub.ID())
   623  					api.logger.Error("websocket recv error, close the conn", "ID", sub.ID(), "error", err)
   624  				}
   625  				return
   626  			case <-unsubscribed:
   627  				api.logger.Debug("BlockTime channel is closed", "ID", sub.ID())
   628  				return
   629  			}
   630  		}
   631  	}(sub.Event(), sub.Err())
   632  
   633  	return sub.ID(), nil
   634  }