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 }