diff --git a/docs/docs.go b/docs/docs.go index 1a47cc38..5f267f9e 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -551,7 +551,7 @@ const docTemplate = `{ }, "/v2/finality-providers": { "get": { - "description": "Fetches finality providers with its stats with pagination support", + "description": "Fetches finality providers with their stats, sorted by active_tvl in descending order (highest TVL first). Pagination is supported via cursor-based pagination tokens.", "produces": [ "application/json" ], @@ -569,7 +569,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "List of finality providers with its stats", + "description": "List of finality providers with stats, sorted by active_tvl DESC", "schema": { "$ref": "#/definitions/handler.PublicResponse-array_v2service_FinalityProviderPublic" } diff --git a/docs/swagger.json b/docs/swagger.json index ea29ca55..97a81b0c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -543,7 +543,7 @@ }, "/v2/finality-providers": { "get": { - "description": "Fetches finality providers with its stats with pagination support", + "description": "Fetches finality providers with their stats, sorted by active_tvl in descending order (highest TVL first). Pagination is supported via cursor-based pagination tokens.", "produces": [ "application/json" ], @@ -561,7 +561,7 @@ ], "responses": { "200": { - "description": "List of finality providers with its stats", + "description": "List of finality providers with stats, sorted by active_tvl DESC", "schema": { "$ref": "#/definitions/handler.PublicResponse-array_v2service_FinalityProviderPublic" } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b3c0ffe0..ecc6ddd1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1019,7 +1019,9 @@ paths: - v2 /v2/finality-providers: get: - description: Fetches finality providers with its stats with pagination support + description: Fetches finality providers with their stats, sorted by active_tvl + in descending order (highest TVL first). Pagination is supported via cursor-based + pagination tokens. parameters: - description: Pagination key to fetch the next page of finality providers in: query @@ -1029,7 +1031,8 @@ paths: - application/json responses: "200": - description: List of finality providers with its stats + description: List of finality providers with stats, sorted by active_tvl + DESC schema: $ref: '#/definitions/handler.PublicResponse-array_v2service_FinalityProviderPublic' "400": diff --git a/internal/indexer/db/client/db_client_test.go b/internal/indexer/db/client/db_client_test.go index b22ef3e5..14ca70a0 100644 --- a/internal/indexer/db/client/db_client_test.go +++ b/internal/indexer/db/client/db_client_test.go @@ -60,6 +60,9 @@ func TestMain(m *testing.M) { log.Fatalf("failed to setup mongo client: %v", err) } + // set global mongoDB variable for resetDatabase function + mongoDB = mongoClient.Database(dbConfig.DbName) + // using config from container mongo initialize client used in tests testDB, err = setupClient(dbConfig, mongoClient) if err != nil { @@ -135,6 +138,7 @@ func resetDatabase(t *testing.T) { collections := []string{ model.FinalityProviderDetailsCollection, + model.IndexerFinalityProviderStatsCollection, model.BTCDelegationDetailsCollection, model.TimeLockCollection, model.GlobalParamsCollection, diff --git a/internal/indexer/db/client/finality_provider.go b/internal/indexer/db/client/finality_provider.go index 585405eb..e84fb767 100644 --- a/internal/indexer/db/client/finality_provider.go +++ b/internal/indexer/db/client/finality_provider.go @@ -2,10 +2,12 @@ package indexerdbclient import ( "context" + "strings" indexerdbmodel "github.com/babylonlabs-io/staking-api-service/internal/indexer/db/model" "github.com/babylonlabs-io/staking-api-service/internal/shared/db" dbmodel "github.com/babylonlabs-io/staking-api-service/internal/shared/db/model" + "github.com/babylonlabs-io/staking-api-service/pkg" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo/options" ) @@ -84,3 +86,28 @@ func (indexerdbclient *IndexerDatabase) GetFinalityProviders( indexerdbmodel.BuildIndexerFinalityProviderPaginationToken, ) } + +// GetFinalityProvidersByPks retrieves finality provider details for specific public keys +// This is used in conjunction with GetFinalityProviderStatsPaginated to fetch details +// for FPs that are already sorted by stats +func (indexerdbclient *IndexerDatabase) GetFinalityProvidersByPks( + ctx context.Context, + fpBtcPkHexes []string, +) ([]*indexerdbmodel.IndexerFinalityProviderDetails, error) { + if len(fpBtcPkHexes) == 0 { + return []*indexerdbmodel.IndexerFinalityProviderDetails{}, nil + } + + var lowercaseFpPkHexes []string + for _, fpPkHex := range fpBtcPkHexes { + lowercaseFpPkHexes = append(lowercaseFpPkHexes, strings.ToLower(fpPkHex)) + } + + client := indexerdbclient.Client.Database( + indexerdbclient.DbName, + ).Collection(indexerdbmodel.FinalityProviderDetailsCollection) + + filter := bson.M{"_id": bson.M{"$in": lowercaseFpPkHexes}} + + return pkg.FetchAll[*indexerdbmodel.IndexerFinalityProviderDetails](ctx, client, filter) +} diff --git a/internal/indexer/db/client/finality_provider_test.go b/internal/indexer/db/client/finality_provider_test.go new file mode 100644 index 00000000..71fe7ba3 --- /dev/null +++ b/internal/indexer/db/client/finality_provider_test.go @@ -0,0 +1,116 @@ +//go:build integration + +package indexerdbclient_test + +import ( + "testing" + + model "github.com/babylonlabs-io/staking-api-service/internal/indexer/db/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetFinalityProvidersByPks(t *testing.T) { + ctx := t.Context() + + fixtures := []any{ + &model.IndexerFinalityProviderDetails{ + BtcPk: "aaa111", + BabylonAddress: "bbn1address1", + Commission: "0.05", + State: model.FinalityProviderStatus_FINALITY_PROVIDER_STATUS_ACTIVE, + Description: model.Description{ + Moniker: "FP1", + }, + }, + &model.IndexerFinalityProviderDetails{ + BtcPk: "bbb222", + BabylonAddress: "bbn1address2", + Commission: "0.10", + State: model.FinalityProviderStatus_FINALITY_PROVIDER_STATUS_ACTIVE, + Description: model.Description{ + Moniker: "FP2", + }, + }, + &model.IndexerFinalityProviderDetails{ + BtcPk: "ccc333", + BabylonAddress: "bbn1address3", + Commission: "0.15", + State: model.FinalityProviderStatus_FINALITY_PROVIDER_STATUS_INACTIVE, + Description: model.Description{ + Moniker: "FP3", + }, + }, + } + + collection := testDB.Client.Database(testDB.Cfg.DbName).Collection(model.FinalityProviderDetailsCollection) + _, err := collection.InsertMany(ctx, fixtures) + require.NoError(t, err) + defer resetDatabase(t) + + t.Run("fetch multiple finality providers", func(t *testing.T) { + pks := []string{"aaa111", "bbb222"} + results, err := testDB.GetFinalityProvidersByPks(ctx, pks) + require.NoError(t, err) + + assert.Len(t, results, 2) + + // Create a map for easier lookup + resultMap := make(map[string]*model.IndexerFinalityProviderDetails) + for _, r := range results { + resultMap[r.BtcPk] = r + } + + assert.Contains(t, resultMap, "aaa111") + assert.Contains(t, resultMap, "bbb222") + assert.Equal(t, "FP1", resultMap["aaa111"].Description.Moniker) + assert.Equal(t, "FP2", resultMap["bbb222"].Description.Moniker) + }) + + t.Run("fetch with case insensitivity", func(t *testing.T) { + // Test lowercase conversion - uppercase input should still find lowercase stored keys + pks := []string{"AAA111", "BBB222"} + results, err := testDB.GetFinalityProvidersByPks(ctx, pks) + require.NoError(t, err) + + assert.Len(t, results, 2) + + resultMap := make(map[string]*model.IndexerFinalityProviderDetails) + for _, r := range results { + resultMap[r.BtcPk] = r + } + + assert.Contains(t, resultMap, "aaa111") + assert.Contains(t, resultMap, "bbb222") + }) + + t.Run("empty input returns empty slice", func(t *testing.T) { + results, err := testDB.GetFinalityProvidersByPks(ctx, []string{}) + require.NoError(t, err) + assert.Empty(t, results) + }) + + t.Run("non-existent public keys", func(t *testing.T) { + pks := []string{"nonexistent1", "nonexistent2"} + results, err := testDB.GetFinalityProvidersByPks(ctx, pks) + require.NoError(t, err) + assert.Empty(t, results) + }) + + t.Run("mixed existent and non-existent keys", func(t *testing.T) { + pks := []string{"aaa111", "nonexistent", "ccc333"} + results, err := testDB.GetFinalityProvidersByPks(ctx, pks) + require.NoError(t, err) + + assert.Len(t, results, 2) + + resultMap := make(map[string]*model.IndexerFinalityProviderDetails) + for _, r := range results { + resultMap[r.BtcPk] = r + } + + assert.Contains(t, resultMap, "aaa111") + assert.Contains(t, resultMap, "ccc333") + assert.NotContains(t, resultMap, "nonexistent") + }) +} diff --git a/internal/indexer/db/client/interface.go b/internal/indexer/db/client/interface.go index 12d425ac..3279b3a8 100644 --- a/internal/indexer/db/client/interface.go +++ b/internal/indexer/db/client/interface.go @@ -16,6 +16,7 @@ type IndexerDBClient interface { GetBtcCheckpointParams(ctx context.Context) ([]*indexertypes.BtcCheckpointParams, error) // Finality Providers GetFinalityProviders(ctx context.Context, paginationToken string) (*db.DbResultMap[*indexerdbmodel.IndexerFinalityProviderDetails], error) + GetFinalityProvidersByPks(ctx context.Context, fpBtcPkHexes []string) ([]*indexerdbmodel.IndexerFinalityProviderDetails, error) CountFinalityProvidersByStatus(ctx context.Context) (map[indexerdbmodel.FinalityProviderState]uint64, error) // Staker Delegations GetDelegation(ctx context.Context, stakingTxHashHex string) (*indexerdbmodel.IndexerDelegationDetails, error) @@ -35,6 +36,7 @@ type IndexerDBClient interface { // Stats GetOverallStats(ctx context.Context) (*indexerdbmodel.IndexerStatsDocument, error) GetFinalityProviderStats(ctx context.Context, fpPkHexes []string) ([]*indexerdbmodel.IndexerFinalityProviderStatsDocument, error) + GetFinalityProviderStatsPaginated(ctx context.Context, paginationToken string) (*db.DbResultMap[*indexerdbmodel.IndexerFinalityProviderStatsDocument], error) } type DelegationFilter struct { diff --git a/internal/indexer/db/client/setup_test.go b/internal/indexer/db/client/setup_test.go index 5b157898..931f419e 100644 --- a/internal/indexer/db/client/setup_test.go +++ b/internal/indexer/db/client/setup_test.go @@ -21,6 +21,15 @@ type index struct { var collections = map[string][]index{ model.FinalityProviderDetailsCollection: {{Indexes: map[string]int{}}}, + model.IndexerFinalityProviderStatsCollection: { + { + Indexes: map[string]int{ + "active_tvl": -1, + "_id": -1, + }, + Unique: false, + }, + }, model.BTCDelegationDetailsCollection: { { Indexes: map[string]int{ diff --git a/internal/indexer/db/client/stats.go b/internal/indexer/db/client/stats.go index 020e4f87..9413899d 100644 --- a/internal/indexer/db/client/stats.go +++ b/internal/indexer/db/client/stats.go @@ -6,10 +6,12 @@ import ( "strings" indexerdbmodel "github.com/babylonlabs-io/staking-api-service/internal/indexer/db/model" + "github.com/babylonlabs-io/staking-api-service/internal/shared/db" dbmodel "github.com/babylonlabs-io/staking-api-service/internal/shared/db/model" "github.com/babylonlabs-io/staking-api-service/pkg" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) // GetOverallStats fetches the overall stats from the indexer's stats collection @@ -55,3 +57,42 @@ func (iDB *IndexerDatabase) GetFinalityProviderStats( return pkg.FetchAll[*indexerdbmodel.IndexerFinalityProviderStatsDocument](ctx, collection, filter) } + +// GetFinalityProviderStatsPaginated retrieves finality provider stats sorted by active_tvl DESC +// This method is used for the V2 finality providers endpoint to enable sorting by TVL +func (iDB *IndexerDatabase) GetFinalityProviderStatsPaginated( + ctx context.Context, + paginationToken string, +) (*db.DbResultMap[*indexerdbmodel.IndexerFinalityProviderStatsDocument], error) { + collection := iDB.collection(dbmodel.IndexerFinalityProviderStatsCollection) + opts := options.Find().SetSort(bson.D{ + {Key: "active_tvl", Value: -1}, + {Key: "_id", Value: -1}, + }) + + filter := bson.M{} + + if paginationToken != "" { + decodedToken, err := dbmodel.DecodePaginationToken[indexerdbmodel.IndexerFinalityProviderStatsPagination](paginationToken) + if err != nil { + return nil, &db.InvalidPaginationTokenError{ + Message: "Invalid pagination token", + } + } + filter = bson.M{ + "$or": []bson.M{ + {"active_tvl": bson.M{"$lt": decodedToken.ActiveTvl}}, + {"active_tvl": decodedToken.ActiveTvl, "_id": bson.M{"$lt": strings.ToLower(decodedToken.FpBtcPkHex)}}, + }, + } + } + + return db.FindWithPagination( + ctx, + collection, + filter, + opts, + iDB.Cfg.MaxPaginationLimit, + indexerdbmodel.BuildIndexerFinalityProviderStatsPaginationToken, + ) +} diff --git a/internal/indexer/db/client/stats_test.go b/internal/indexer/db/client/stats_test.go new file mode 100644 index 00000000..bdf692ac --- /dev/null +++ b/internal/indexer/db/client/stats_test.go @@ -0,0 +1,117 @@ +//go:build integration + +package indexerdbclient_test + +import ( + "testing" + + model "github.com/babylonlabs-io/staking-api-service/internal/indexer/db/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetFinalityProviderStatsPaginated(t *testing.T) { + ctx := t.Context() + + // Create test fixtures with different TVL values + fixtures := []any{ + &model.IndexerFinalityProviderStatsDocument{ + FpBtcPkHex: "aaa111", + ActiveTvl: 1000, + ActiveDelegations: 10, + LastUpdated: 1234567890, + }, + &model.IndexerFinalityProviderStatsDocument{ + FpBtcPkHex: "bbb222", + ActiveTvl: 5000, + ActiveDelegations: 50, + LastUpdated: 1234567891, + }, + &model.IndexerFinalityProviderStatsDocument{ + FpBtcPkHex: "ccc333", + ActiveTvl: 3000, + ActiveDelegations: 30, + LastUpdated: 1234567892, + }, + &model.IndexerFinalityProviderStatsDocument{ + FpBtcPkHex: "ddd444", + ActiveTvl: 5000, // Same TVL as bbb222 to test tiebreaker + ActiveDelegations: 55, + LastUpdated: 1234567893, + }, + &model.IndexerFinalityProviderStatsDocument{ + FpBtcPkHex: "eee555", + ActiveTvl: 2000, + ActiveDelegations: 20, + LastUpdated: 1234567894, + }, + } + + // In order to test pagination, limit must be less than the number of fixtures + require.Less(t, maxPaginationLimit, len(fixtures)) + + collection := testDB.Client.Database(testDB.Cfg.DbName).Collection(model.IndexerFinalityProviderStatsCollection) + _, err := collection.InsertMany(ctx, fixtures) + require.NoError(t, err) + defer resetDatabase(t) + + t.Run("sorted by active_tvl descending with tiebreaker", func(t *testing.T) { + var allResults []*model.IndexerFinalityProviderStatsDocument + var token string + + // Paginate through all results + for { + result, err := testDB.GetFinalityProviderStatsPaginated(ctx, token) + require.NoError(t, err) + + allResults = append(allResults, result.Data...) + + token = result.PaginationToken + if token == "" { + break + } + } + + // Check that all records were fetched + assert.Equal(t, len(fixtures), len(allResults)) + + // Verify sorting: active_tvl descending, then _id descending for ties + // Expected order: bbb222(5000), ddd444(5000), ccc333(3000), eee555(2000), aaa111(1000) + // For same TVL, _id descending: ddd444 > bbb222 + assert.Equal(t, uint64(5000), allResults[0].ActiveTvl) + assert.Equal(t, "ddd444", allResults[0].FpBtcPkHex) + + assert.Equal(t, uint64(5000), allResults[1].ActiveTvl) + assert.Equal(t, "bbb222", allResults[1].FpBtcPkHex) + + assert.Equal(t, uint64(3000), allResults[2].ActiveTvl) + assert.Equal(t, "ccc333", allResults[2].FpBtcPkHex) + + assert.Equal(t, uint64(2000), allResults[3].ActiveTvl) + assert.Equal(t, "eee555", allResults[3].FpBtcPkHex) + + assert.Equal(t, uint64(1000), allResults[4].ActiveTvl) + assert.Equal(t, "aaa111", allResults[4].FpBtcPkHex) + + // Verify all TVLs are in descending order (allowing ties) + for i := 0; i < len(allResults)-1; i++ { + assert.GreaterOrEqual(t, allResults[i].ActiveTvl, allResults[i+1].ActiveTvl, + "active_tvl should be sorted in descending order") + } + }) + + t.Run("empty results with no data", func(t *testing.T) { + resetDatabase(t) + + result, err := testDB.GetFinalityProviderStatsPaginated(ctx, "") + require.NoError(t, err) + assert.Empty(t, result.Data) + assert.Empty(t, result.PaginationToken) + }) + + t.Run("invalid pagination token", func(t *testing.T) { + _, err := testDB.GetFinalityProviderStatsPaginated(ctx, "invalid-token") + require.Error(t, err) + assert.Contains(t, err.Error(), "Invalid pagination token") + }) +} diff --git a/internal/indexer/db/model/setup.go b/internal/indexer/db/model/setup.go index 964e5a72..5d58b9e1 100644 --- a/internal/indexer/db/model/setup.go +++ b/internal/indexer/db/model/setup.go @@ -1,10 +1,11 @@ package indexerdbmodel const ( - FinalityProviderDetailsCollection = "finality_provider_details" - BTCDelegationDetailsCollection = "btc_delegation_details" - TimeLockCollection = "timelock" - GlobalParamsCollection = "global_params" - LastProcessedHeightCollection = "last_processed_height" - NetworkInfoCollection = "network_info" + FinalityProviderDetailsCollection = "finality_provider_details" + IndexerFinalityProviderStatsCollection = "finality_provider_stats" + BTCDelegationDetailsCollection = "btc_delegation_details" + TimeLockCollection = "timelock" + GlobalParamsCollection = "global_params" + LastProcessedHeightCollection = "last_processed_height" + NetworkInfoCollection = "network_info" ) diff --git a/internal/indexer/db/model/stats.go b/internal/indexer/db/model/stats.go index 546cbee6..195875ad 100644 --- a/internal/indexer/db/model/stats.go +++ b/internal/indexer/db/model/stats.go @@ -1,5 +1,7 @@ package indexerdbmodel +import dbmodel "github.com/babylonlabs-io/staking-api-service/internal/shared/db/model" + // IndexerStatsDocument represents the overall stats document from the indexer type IndexerStatsDocument struct { Id string `bson:"_id"` // Always "overall_stats" @@ -15,3 +17,23 @@ type IndexerFinalityProviderStatsDocument struct { ActiveDelegations uint64 `bson:"active_delegations"` // Active delegation count for this FP LastUpdated int64 `bson:"last_updated"` // Unix timestamp } + +// IndexerFinalityProviderStatsPagination represents pagination token for FP stats +// sorted by active_tvl in descending order with fp_btc_pk_hex as tiebreaker +type IndexerFinalityProviderStatsPagination struct { + FpBtcPkHex string `json:"fp_btc_pk_hex"` + ActiveTvl uint64 `json:"active_tvl"` +} + +// BuildIndexerFinalityProviderStatsPaginationToken creates pagination token from stats document +func BuildIndexerFinalityProviderStatsPaginationToken(d *IndexerFinalityProviderStatsDocument) (string, error) { + page := IndexerFinalityProviderStatsPagination{ + FpBtcPkHex: d.FpBtcPkHex, + ActiveTvl: d.ActiveTvl, + } + token, err := dbmodel.GetPaginationToken(page) + if err != nil { + return "", err + } + return token, nil +} diff --git a/internal/v2/api/handlers/finality_provider.go b/internal/v2/api/handlers/finality_provider.go index 415c2cb6..bea11ae0 100644 --- a/internal/v2/api/handlers/finality_provider.go +++ b/internal/v2/api/handlers/finality_provider.go @@ -7,14 +7,14 @@ import ( "github.com/babylonlabs-io/staking-api-service/internal/shared/types" ) -// GetFinalityProviders gets a list of finality providers with its stats +// GetFinalityProviders gets a list of finality providers with their stats, sorted by active TVL // // @Summary List Finality Providers -// @Description Fetches finality providers with its stats with pagination support +// @Description Fetches finality providers with their stats, sorted by active_tvl in descending order (highest TVL first). Pagination is supported via cursor-based pagination tokens. // @Produce json // @Tags v2 // @Param pagination_key query string false "Pagination key to fetch the next page of finality providers" -// @Success 200 {object} handler.PublicResponse[[]v2service.FinalityProviderPublic] "List of finality providers with its stats" +// @Success 200 {object} handler.PublicResponse[[]v2service.FinalityProviderPublic] "List of finality providers with stats, sorted by active_tvl DESC" // @Failure 400 {object} types.Error "Invalid pagination token" // @Failure 404 {object} types.Error "No finality providers found" // @Failure 500 {object} types.Error "Internal server error occurred" diff --git a/internal/v2/service/finality_provider.go b/internal/v2/service/finality_provider.go index 67367a51..30713497 100644 --- a/internal/v2/service/finality_provider.go +++ b/internal/v2/service/finality_provider.go @@ -44,28 +44,29 @@ func mapToFinalityProviderStatsPublic( } } -// GetFinalityProvidersWithStats retrieves finality providers and their associated statistics with pagination +// GetFinalityProvidersWithStats retrieves finality providers sorted by active TVL with pagination +// Implementation follows stats-first approach: query stats sorted by active_tvl, then fetch details func (s *V2Service) GetFinalityProvidersWithStats( ctx context.Context, paginationToken string, ) ([]*FinalityProviderPublic, string, *types.Error) { - finalityProvidersResult, err := s.dbClients.IndexerDBClient.GetFinalityProviders(ctx, paginationToken) + fpStatsResult, err := s.dbClients.IndexerDBClient.GetFinalityProviderStatsPaginated(ctx, paginationToken) if err != nil { if db.IsInvalidPaginationTokenError(err) { - log.Ctx(ctx).Warn().Err(err).Msg("Invalid pagination token when fetching finality providers") + log.Ctx(ctx).Warn().Err(err).Msg("Invalid pagination token when fetching finality provider stats") return nil, "", types.NewError(http.StatusBadRequest, types.BadRequest, err) } return nil, "", types.NewErrorWithMsg( http.StatusInternalServerError, types.InternalServiceError, - "failed to get finality providers", + "failed to get finality provider stats", ) } - finalityProviders := finalityProvidersResult.Data + fpStats := fpStatsResult.Data - if len(finalityProviders) == 0 { - log.Ctx(ctx).Warn().Msg("No finality providers found") + if len(fpStats) == 0 { + log.Ctx(ctx).Warn().Msg("No finality provider stats found") return nil, "", types.NewErrorWithMsg( http.StatusNotFound, types.NotFound, @@ -73,58 +74,56 @@ func (s *V2Service) GetFinalityProvidersWithStats( ) } - fpPkHexes := make([]string, 0, len(finalityProviders)) - for _, fp := range finalityProviders { - fpPkHexes = append(fpPkHexes, fp.BtcPk) + fpPkHexes := make([]string, 0, len(fpStats)) + for _, stat := range fpStats { + fpPkHexes = append(fpPkHexes, stat.FpBtcPkHex) } - indexerProviderStats, err := s.dbClients.IndexerDBClient.GetFinalityProviderStats(ctx, fpPkHexes) + finalityProviders, err := s.dbClients.IndexerDBClient.GetFinalityProvidersByPks(ctx, fpPkHexes) if err != nil { return nil, "", types.NewErrorWithMsg( http.StatusInternalServerError, types.InternalServiceError, - "failed to get finality provider stats", + "failed to get finality provider details", ) } logoMap := s.fetchLogos(ctx, finalityProviders) - statsLookup := make(map[string]*v2dbmodel.V2FinalityProviderStatsDocument) - for _, stats := range indexerProviderStats { - // Convert indexer stats (uint64) to V2 format (int64) - statsLookup[stats.FpBtcPkHex] = &v2dbmodel.V2FinalityProviderStatsDocument{ - FinalityProviderPkHex: stats.FpBtcPkHex, - ActiveTvl: int64(stats.ActiveTvl), - ActiveDelegations: int64(stats.ActiveDelegations), - } + detailsLookup := make(map[string]*indexerdbmodel.IndexerFinalityProviderDetails) + for _, fp := range finalityProviders { + detailsLookup[fp.BtcPk] = fp } - finalityProvidersPublic := make([]*FinalityProviderPublic, 0, len(finalityProviders)) + finalityProvidersPublic := make([]*FinalityProviderPublic, 0, len(fpStats)) - for _, provider := range finalityProviders { - providerStats, hasStats := statsLookup[provider.BtcPk] - if !hasStats { - providerStats = &v2dbmodel.V2FinalityProviderStatsDocument{ - ActiveTvl: 0, - ActiveDelegations: 0, - } + for _, stat := range fpStats { + fpDetails, hasDetails := detailsLookup[stat.FpBtcPkHex] + if !hasDetails { log.Ctx(ctx).Debug(). - Str("finality_provider_pk_hex", provider.BtcPk). - Msg("Initializing finality provider with default stats") + Str("finality_provider_pk_hex", stat.FpBtcPkHex). + Msg("Finality provider has stats but no details, skipping") + continue + } + + v2Stats := &v2dbmodel.V2FinalityProviderStatsDocument{ + FinalityProviderPkHex: stat.FpBtcPkHex, + ActiveTvl: int64(stat.ActiveTvl), + ActiveDelegations: int64(stat.ActiveDelegations), } var logoURL string if logoMap != nil { - logoURL = logoMap[provider.BtcPk] + logoURL = logoMap[fpDetails.BtcPk] } finalityProvidersPublic = append( finalityProvidersPublic, - mapToFinalityProviderStatsPublic(*provider, providerStats, logoURL), + mapToFinalityProviderStatsPublic(*fpDetails, v2Stats, logoURL), ) } - return finalityProvidersPublic, finalityProvidersResult.PaginationToken, nil + return finalityProvidersPublic, fpStatsResult.PaginationToken, nil } func (s *V2Service) fetchLogos(ctx context.Context, fps []*indexerdbmodel.IndexerFinalityProviderDetails) map[string]string { diff --git a/tests/mocks/mock_indexer_db_client.go b/tests/mocks/mock_indexer_db_client.go index fcf211a3..94d69d2a 100644 --- a/tests/mocks/mock_indexer_db_client.go +++ b/tests/mocks/mock_indexer_db_client.go @@ -295,6 +295,36 @@ func (_m *IndexerDBClient) GetFinalityProviderStats(ctx context.Context, fpPkHex return r0, r1 } +// GetFinalityProviderStatsPaginated provides a mock function with given fields: ctx, paginationToken +func (_m *IndexerDBClient) GetFinalityProviderStatsPaginated(ctx context.Context, paginationToken string) (*db.DbResultMap[*indexerdbmodel.IndexerFinalityProviderStatsDocument], error) { + ret := _m.Called(ctx, paginationToken) + + if len(ret) == 0 { + panic("no return value specified for GetFinalityProviderStatsPaginated") + } + + var r0 *db.DbResultMap[*indexerdbmodel.IndexerFinalityProviderStatsDocument] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*db.DbResultMap[*indexerdbmodel.IndexerFinalityProviderStatsDocument], error)); ok { + return rf(ctx, paginationToken) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *db.DbResultMap[*indexerdbmodel.IndexerFinalityProviderStatsDocument]); ok { + r0 = rf(ctx, paginationToken) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*db.DbResultMap[*indexerdbmodel.IndexerFinalityProviderStatsDocument]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, paginationToken) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetFinalityProviders provides a mock function with given fields: ctx, paginationToken func (_m *IndexerDBClient) GetFinalityProviders(ctx context.Context, paginationToken string) (*db.DbResultMap[*indexerdbmodel.IndexerFinalityProviderDetails], error) { ret := _m.Called(ctx, paginationToken) @@ -325,6 +355,36 @@ func (_m *IndexerDBClient) GetFinalityProviders(ctx context.Context, paginationT return r0, r1 } +// GetFinalityProvidersByPks provides a mock function with given fields: ctx, fpBtcPkHexes +func (_m *IndexerDBClient) GetFinalityProvidersByPks(ctx context.Context, fpBtcPkHexes []string) ([]*indexerdbmodel.IndexerFinalityProviderDetails, error) { + ret := _m.Called(ctx, fpBtcPkHexes) + + if len(ret) == 0 { + panic("no return value specified for GetFinalityProvidersByPks") + } + + var r0 []*indexerdbmodel.IndexerFinalityProviderDetails + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string) ([]*indexerdbmodel.IndexerFinalityProviderDetails, error)); ok { + return rf(ctx, fpBtcPkHexes) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) []*indexerdbmodel.IndexerFinalityProviderDetails); ok { + r0 = rf(ctx, fpBtcPkHexes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*indexerdbmodel.IndexerFinalityProviderDetails) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok { + r1 = rf(ctx, fpBtcPkHexes) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetLastProcessedBbnHeight provides a mock function with given fields: ctx func (_m *IndexerDBClient) GetLastProcessedBbnHeight(ctx context.Context) (uint64, error) { ret := _m.Called(ctx)