diff --git a/internal/v2/db/client/interface.go b/internal/v2/db/client/interface.go index a7cd082..f8ad932 100644 --- a/internal/v2/db/client/interface.go +++ b/internal/v2/db/client/interface.go @@ -11,6 +11,7 @@ type V2DBClient interface { dbclient.DBClient GetOverallStats(ctx context.Context) (*v2dbmodel.V2OverallStatsDocument, error) GetStakerStats(ctx context.Context, stakerPKHex string) (*v2dbmodel.V2StakerStatsDocument, error) + GetFinalityProviderStats(ctx context.Context) ([]*v2dbmodel.V2FinalityProviderStatsDocument, error) GetOrCreateStatsLock( ctx context.Context, stakingTxHashHex string, state string, ) (*v2dbmodel.V2StatsLockDocument, error) diff --git a/internal/v2/db/client/stats.go b/internal/v2/db/client/stats.go index c557431..ffd1893 100644 --- a/internal/v2/db/client/stats.go +++ b/internal/v2/db/client/stats.go @@ -408,3 +408,20 @@ func (v2dbclient *V2Database) updateFinalityProviderStats( _, txErr := session.WithTransaction(ctx, transactionWork) return txErr } + +func (v2dbclient *V2Database) GetFinalityProviderStats( + ctx context.Context, +) ([]*v2dbmodel.V2FinalityProviderStatsDocument, error) { + client := v2dbclient.Client.Database(v2dbclient.DbName).Collection(dbmodel.V2FinalityProviderStatsCollection) + cursor, err := client.Find(ctx, bson.M{}) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + + var results []*v2dbmodel.V2FinalityProviderStatsDocument + if err := cursor.All(ctx, &results); err != nil { + return nil, err + } + return results, nil +} diff --git a/internal/v2/db/model/stats.go b/internal/v2/db/model/stats.go index 03db1d8..327aa44 100644 --- a/internal/v2/db/model/stats.go +++ b/internal/v2/db/model/stats.go @@ -1,7 +1,5 @@ package v2dbmodel -import dbmodel "github.com/babylonlabs-io/staking-api-service/internal/shared/db/model" - // StatsLockDocument represents the document in the stats lock collection // It's used as a lock to prevent concurrent stats calculation for the same staking tx hash // As well as to prevent the same staking tx hash + txType to be processed multiple times @@ -41,21 +39,10 @@ type V2StakerStatsDocument struct { TotalDelegations int64 `bson:"total_delegations"` } -// StakerStatsByStakerPagination is used to paginate the top stakers by active tvl -// ActiveTvl is used as the sorting key, whereas StakerPkHex is used as the secondary sorting key -type V2StakerStatsByStakerPagination struct { - StakerPkHex string `json:"staker_pk_hex"` - ActiveTvl int64 `json:"active_tvl"` -} - -func BuildV2StakerStatsByStakerPaginationToken(d *V2StakerStatsDocument) (string, error) { - page := V2StakerStatsByStakerPagination{ - StakerPkHex: d.StakerPkHex, - ActiveTvl: d.ActiveTvl, - } - token, err := dbmodel.GetPaginationToken(page) - if err != nil { - return "", err - } - return token, nil +type V2FinalityProviderStatsDocument struct { + FinalityProviderPkHex string `bson:"_id"` + ActiveTvl int64 `bson:"active_tvl"` + TotalTvl int64 `bson:"total_tvl"` + ActiveDelegations int64 `bson:"active_delegations"` + TotalDelegations int64 `bson:"total_delegations"` } diff --git a/internal/v2/service/finality_provider.go b/internal/v2/service/finality_provider.go index 7d9dbc1..4e4b997 100644 --- a/internal/v2/service/finality_provider.go +++ b/internal/v2/service/finality_provider.go @@ -7,6 +7,7 @@ import ( indexerdbmodel "github.com/babylonlabs-io/staking-api-service/internal/indexer/db/model" "github.com/babylonlabs-io/staking-api-service/internal/shared/db" "github.com/babylonlabs-io/staking-api-service/internal/shared/types" + v2dbmodel "github.com/babylonlabs-io/staking-api-service/internal/v2/db/model" "github.com/rs/zerolog/log" ) @@ -23,39 +24,72 @@ type FinalityProvidersStatsPublic struct { FinalityProviders []FinalityProviderStatsPublic `json:"finality_providers"` } -func mapToFinalityProviderStatsPublic(provider indexerdbmodel.IndexerFinalityProviderDetails) *FinalityProviderStatsPublic { +func mapToFinalityProviderStatsPublic( + provider indexerdbmodel.IndexerFinalityProviderDetails, + fpStats *v2dbmodel.V2FinalityProviderStatsDocument, +) *FinalityProviderStatsPublic { return &FinalityProviderStatsPublic{ BtcPk: provider.BtcPk, State: types.FinalityProviderQueryingState(provider.State), Description: types.FinalityProviderDescription(provider.Description), Commission: provider.Commission, - ActiveTvl: 0, - ActiveDelegations: 0, + ActiveTvl: fpStats.ActiveTvl, + ActiveDelegations: fpStats.ActiveDelegations, } } -// GetFinalityProviders gets a list of finality providers with stats +// GetFinalityProvidersWithStats retrieves all finality providers and their associated statistics func (s *V2Service) GetFinalityProvidersWithStats( ctx context.Context, ) ([]*FinalityProviderStatsPublic, *types.Error) { - fps, err := s.DbClients.IndexerDBClient.GetFinalityProviders(ctx) + finalityProviders, err := s.DbClients.IndexerDBClient.GetFinalityProviders(ctx) if err != nil { if db.IsNotFoundError(err) { - log.Ctx(ctx).Warn().Err(err).Msg("Finality providers not found") + log.Ctx(ctx).Warn().Err(err).Msg("No finality providers found") return nil, types.NewErrorWithMsg( - http.StatusNotFound, types.NotFound, "finality providers not found, please retry", + http.StatusNotFound, + types.NotFound, + "finality providers not found, please retry", ) } return nil, types.NewErrorWithMsg( - http.StatusInternalServerError, types.InternalServiceError, "failed to get finality providers", + http.StatusInternalServerError, + types.InternalServiceError, + "failed to get finality providers", ) } - // TODO: Call the FP stats service to get the stats for compose the response - providersPublic := make([]*FinalityProviderStatsPublic, 0, len(fps)) + providerStats, err := s.DbClients.V2DBClient.GetFinalityProviderStats(ctx) + if err != nil { + return nil, types.NewErrorWithMsg( + http.StatusInternalServerError, + types.InternalServiceError, + "failed to get finality provider stats", + ) + } - for _, provider := range fps { - providersPublic = append(providersPublic, mapToFinalityProviderStatsPublic(*provider)) + statsLookup := make(map[string]*v2dbmodel.V2FinalityProviderStatsDocument) + for _, stats := range providerStats { + statsLookup[stats.FinalityProviderPkHex] = stats + } + + finalityProvidersWithStats := make([]*FinalityProviderStatsPublic, 0, len(finalityProviders)) + + for _, provider := range finalityProviders { + providerStats, hasStats := statsLookup[provider.BtcPk] + if !hasStats { + providerStats = &v2dbmodel.V2FinalityProviderStatsDocument{ + ActiveTvl: 0, + ActiveDelegations: 0, + } + log.Ctx(ctx).Debug(). + Str("finality_provider_pk_hex", provider.BtcPk). + Msg("Initializing finality provider with default stats") + } + finalityProvidersWithStats = append( + finalityProvidersWithStats, + mapToFinalityProviderStatsPublic(*provider, providerStats), + ) } - return providersPublic, nil + return finalityProvidersWithStats, nil } diff --git a/tests/mocks/mock_v2_db_client.go b/tests/mocks/mock_v2_db_client.go index 07125c4..2b9cb0d 100644 --- a/tests/mocks/mock_v2_db_client.go +++ b/tests/mocks/mock_v2_db_client.go @@ -124,6 +124,36 @@ func (_m *V2DBClient) FindUnprocessableMessages(ctx context.Context) ([]dbmodel. return r0, r1 } +// GetFinalityProviderStats provides a mock function with given fields: ctx +func (_m *V2DBClient) GetFinalityProviderStats(ctx context.Context) ([]*v2dbmodel.V2FinalityProviderStatsDocument, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetFinalityProviderStats") + } + + var r0 []*v2dbmodel.V2FinalityProviderStatsDocument + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]*v2dbmodel.V2FinalityProviderStatsDocument, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []*v2dbmodel.V2FinalityProviderStatsDocument); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*v2dbmodel.V2FinalityProviderStatsDocument) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetOrCreateStatsLock provides a mock function with given fields: ctx, stakingTxHashHex, state func (_m *V2DBClient) GetOrCreateStatsLock(ctx context.Context, stakingTxHashHex string, state string) (*v2dbmodel.V2StatsLockDocument, error) { ret := _m.Called(ctx, stakingTxHashHex, state)