Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand All @@ -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"
}
Expand Down
4 changes: 2 additions & 2 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand All @@ -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"
}
Expand Down
7 changes: 5 additions & 2 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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":
Expand Down
4 changes: 4 additions & 0 deletions internal/indexer/db/client/db_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -135,6 +138,7 @@ func resetDatabase(t *testing.T) {

collections := []string{
model.FinalityProviderDetailsCollection,
model.IndexerFinalityProviderStatsCollection,
model.BTCDelegationDetailsCollection,
model.TimeLockCollection,
model.GlobalParamsCollection,
Expand Down
27 changes: 27 additions & 0 deletions internal/indexer/db/client/finality_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}
116 changes: 116 additions & 0 deletions internal/indexer/db/client/finality_provider_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
2 changes: 2 additions & 0 deletions internal/indexer/db/client/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions internal/indexer/db/client/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
41 changes: 41 additions & 0 deletions internal/indexer/db/client/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
Loading
Loading