package services import ( "context" "fmt" "math/big" "github.com/ethereum/go-ethereum/common/math" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" "github.com/libp2p/go-libp2p/core/peer" "github.com/woop-chain/woop/block" "github.com/woop-chain/woop/eth/rpc" "github.com/woop-chain/woop/wiki" nodeconfig "github.com/woop-chain/woop/internal/configs/node" "github.com/woop-chain/woop/rosetta/common" commonRPC "github.com/woop-chain/woop/rpc/common" "github.com/woop-chain/woop/shard" ) // NetworkAPI implements the server.NetworkAPIServicer interface. type NetworkAPI struct { wiki *wiki.Woop } // NewNetworkAPI creates a new instance of a NetworkAPI. func NewNetworkAPI(wiki *wiki.Woop) server.NetworkAPIServicer { return &NetworkAPI{ wiki: wiki, } } // NetworkList implements the /network/list endpoint // TODO (dm): Update Node API to support multiple shards... func (s *NetworkAPI) NetworkList( ctx context.Context, request *types.MetadataRequest, ) (*types.NetworkListResponse, *types.Error) { network, err := common.GetNetwork(s.wiki.ShardID) if err != nil { return nil, common.NewError(common.CatchAllError, map[string]interface{}{ "message": err.Error(), }) } return &types.NetworkListResponse{ NetworkIdentifiers: []*types.NetworkIdentifier{ network, }, }, nil } // NetworkStatus implements the /network/status endpoint func (s *NetworkAPI) NetworkStatus( ctx context.Context, request *types.NetworkRequest, ) (*types.NetworkStatusResponse, *types.Error) { if err := assertValidNetworkIdentifier(request.NetworkIdentifier, s.wiki.ShardID); err != nil { return nil, err } // Fetch relevant headers, syncing status, & peers currBlock := s.wiki.CurrentBlock() var currentHeader *block.Header var err error if currBlock.Number().Cmp(big.NewInt(0)) == 1 && !s.wiki.IsStakingEpoch(currBlock.Epoch()) { // all blocks in the era before staking epoch requires the next block to get the block reward transactions blkNum := new(big.Int).Sub(currBlock.Number(), big.NewInt(1)) currentHeader, err = s.wiki.HeaderByNumber(ctx, rpc.BlockNumber(blkNum.Uint64())) } else { currentHeader, err = s.wiki.HeaderByNumber(ctx, rpc.LatestBlockNumber) } if err != nil { return nil, common.NewError(common.CatchAllError, map[string]interface{}{ "message": fmt.Sprintf("unable to get current header: %v", err.Error()), }) } genesisHeader, err := s.wiki.HeaderByNumber(ctx, rpc.BlockNumber(0)) if err != nil { return nil, common.NewError(common.CatchAllError, map[string]interface{}{ "message": fmt.Sprintf("unable to get genesis header: %v", err.Error()), }) } peers, rosettaError := getPeersFromNodePeerInfo(s.wiki.GetPeerInfo()) if rosettaError != nil { return nil, rosettaError } isSyncing, targetHeight, _ := s.wiki.NodeAPI.SyncStatus(s.wiki.BlockChain.ShardID()) syncStatus := common.SyncingFinish if targetHeight == 0 { syncStatus = common.SyncingUnknown } else if isSyncing { syncStatus = common.SyncingNewBlock } stage := syncStatus.String() currentBlockIdentifier := &types.BlockIdentifier{ Index: currentHeader.Number().Int64(), Hash: currentHeader.Hash().String(), } // Only applicable to non-archival nodes var oldestBlockIdentifier *types.BlockIdentifier if !nodeconfig.GetShardConfig(s.wiki.ShardID).GetArchival() { maxGarbCollectedBlockNum := s.wiki.BlockChain.GetMaxGarbageCollectedBlockNumber() if maxGarbCollectedBlockNum == -1 || maxGarbCollectedBlockNum >= currentHeader.Number().Int64() { oldestBlockIdentifier = currentBlockIdentifier } else { oldestBlockHeader, err := s.wiki.HeaderByNumber(ctx, rpc.BlockNumber(maxGarbCollectedBlockNum+1)) if err != nil { return nil, common.NewError(common.CatchAllError, map[string]interface{}{ "message": fmt.Sprintf("unable to get oldest block header: %v", err.Error()), }) } oldestBlockIdentifier = &types.BlockIdentifier{ Index: oldestBlockHeader.Number().Int64(), Hash: oldestBlockHeader.Hash().String(), } } } targetInt := int64(targetHeight) if targetHeight == math.MaxUint64 { targetInt = 0 } currentIndex := currentHeader.Number().Int64() ss := &types.SyncStatus{ CurrentIndex: ¤tIndex, TargetIndex: &targetInt, Stage: &stage, } return &types.NetworkStatusResponse{ CurrentBlockIdentifier: currentBlockIdentifier, OldestBlockIdentifier: oldestBlockIdentifier, CurrentBlockTimestamp: currentHeader.Time().Int64() * 1e3, // Timestamp must be in ms. GenesisBlockIdentifier: &types.BlockIdentifier{ Index: genesisHeader.Number().Int64(), Hash: genesisHeader.Hash().String(), }, Peers: peers, SyncStatus: ss, }, nil } // NetworkOptions implements the /network/options endpoint func (s *NetworkAPI) NetworkOptions( ctx context.Context, request *types.NetworkRequest, ) (*types.NetworkOptionsResponse, *types.Error) { if err := assertValidNetworkIdentifier(request.NetworkIdentifier, s.wiki.ShardID); err != nil { return nil, err } // Fetch allows based on current network option var allow *types.Allow isArchival := nodeconfig.GetShardConfig(s.wiki.ShardID).GetArchival() if s.wiki.ShardID == shard.BeaconChainShardID { allow = getBeaconAllow(isArchival) } else { allow = getAllow(isArchival) } return &types.NetworkOptionsResponse{ Version: &types.Version{ RosettaVersion: common.RosettaVersion, NodeVersion: nodeconfig.GetVersion(), }, Allow: allow, }, nil } func getBeaconAllow(isArchival bool) *types.Allow { return &types.Allow{ OperationStatuses: append(getOperationStatuses(), getBeaconOperationStatuses()...), OperationTypes: append(common.PlainOperationTypes, common.StakingOperationTypes...), Errors: append(getErrors(), getBeaconErrors()...), HistoricalBalanceLookup: isArchival, } } func getAllow(isArchival bool) *types.Allow { return &types.Allow{ OperationStatuses: getOperationStatuses(), OperationTypes: common.PlainOperationTypes, Errors: getErrors(), HistoricalBalanceLookup: isArchival, } } func getBeaconOperationStatuses() []*types.OperationStatus { return []*types.OperationStatus{} } func getOperationStatuses() []*types.OperationStatus { return []*types.OperationStatus{ common.SuccessOperationStatus, common.FailureOperationStatus, common.ContractFailureOperationStatus, } } func getBeaconErrors() []*types.Error { return []*types.Error{ &common.StakingTransactionSubmissionError, } } func getErrors() []*types.Error { return []*types.Error{ &common.CatchAllError, &common.SanityCheckError, &common.InvalidNetworkError, &common.TransactionSubmissionError, &common.BlockNotFoundError, &common.TransactionNotFoundError, &common.ReceiptNotFoundError, &common.UnsupportedCurveTypeError, &common.InvalidTransactionConstructionError, } } // getPeersFromNodePeerInfo formats all the unique peers from the NodePeerInfo and // notes each topic for each peer in the metadata. func getPeersFromNodePeerInfo(allPeerInfo commonRPC.NodePeerInfo) ([]*types.Peer, *types.Error) { seenPeerIndex := map[peer.ID]int{} peers := []*types.Peer{} for _, peerInfo := range allPeerInfo.P { for _, pID := range peerInfo.Peers { i, ok := seenPeerIndex[pID] if !ok { newPeer := &types.Peer{ PeerID: pID.String(), Metadata: map[string]interface{}{ "topics": []string{peerInfo.Topic}, }, } peers = append(peers, newPeer) seenPeerIndex[pID] = len(peers) - 1 } else { topics, ok := peers[i].Metadata["topics"].([]string) if !ok { return nil, common.NewError(common.SanityCheckError, map[string]interface{}{ "message": "could not cast peer metadata to slice of string", }) } for _, topic := range topics { if peerInfo.Topic == topic { continue } } peers[i].Metadata["topics"] = append(topics, peerInfo.Topic) } } } return peers, nil } func assertValidNetworkIdentifier(netID *types.NetworkIdentifier, shardID uint32) *types.Error { currNetID, err := common.GetNetwork(shardID) if err != nil { return common.NewError(common.SanityCheckError, map[string]interface{}{ "message": fmt.Sprintf("Error while asserting valid network ID: %v", err.Error()), }) } if netID == nil || types.Hash(currNetID) != types.Hash(netID) { return &common.InvalidNetworkError } return nil }