Skip to content

Commit

Permalink
Merge pull request #42 from babylonlabs-io/able-return-single-FP
Browse files Browse the repository at this point in the history
feat: return single FP stats
  • Loading branch information
jrwbabylonlab authored Aug 29, 2024
2 parents 700c3a8 + fa1e19f commit dc2b2ff
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 17 deletions.
6 changes: 6 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ const docTemplate = `{
],
"summary": "Get Active Finality Providers",
"parameters": [
{
"type": "string",
"description": "Public key of the finality provider to fetch",
"name": "fp_btc_pk",
"in": "query"
},
{
"type": "string",
"description": "Pagination key to fetch the next page of finality providers",
Expand Down
6 changes: 6 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@
],
"summary": "Get Active Finality Providers",
"parameters": [
{
"type": "string",
"description": "Public key of the finality provider to fetch",
"name": "fp_btc_pk",
"in": "query"
},
{
"type": "string",
"description": "Pagination key to fetch the next page of finality providers",
Expand Down
4 changes: 4 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,10 @@ paths:
description: Fetches details of all active finality providers sorted by their
active total value locked (ActiveTvl) in descending order.
parameters:
- description: Public key of the finality provider to fetch
in: query
name: fp_btc_pk
type: string
- description: Pagination key to fetch the next page of finality providers
in: query
name: pagination_key
Expand Down
19 changes: 19 additions & 0 deletions internal/api/handlers/finality_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,36 @@ package handlers
import (
"net/http"

"github.com/babylonlabs-io/staking-api-service/internal/services"
"github.com/babylonlabs-io/staking-api-service/internal/types"
)

// GetFinalityProviders gets active finality providers sorted by ActiveTvl.
// @Summary Get Active Finality Providers
// @Description Fetches details of all active finality providers sorted by their active total value locked (ActiveTvl) in descending order.
// @Produce json
// @Param fp_btc_pk query string false "Public key of the finality provider to fetch"
// @Param pagination_key query string false "Pagination key to fetch the next page of finality providers"
// @Success 200 {object} PublicResponse[[]services.FpDetailsPublic] "A list of finality providers sorted by ActiveTvl in descending order"
// @Router /v1/finality-providers [get]
func (h *Handler) GetFinalityProviders(request *http.Request) (*Result, *types.Error) {
fpPk, err := parsePublicKeyQuery(request, "fp_btc_pk", true)
if err != nil {
return nil, err
}
if fpPk != "" {
var result []*services.FpDetailsPublic
fp, err := h.services.GetFinalityProvider(request.Context(), fpPk)
if err != nil {
return nil, err
}
if fp != nil {
result = append(result, fp)
}

return NewResult(result), nil
}

paginationKey, err := parsePaginationQuery(request)
if err != nil {
return nil, err
Expand Down
5 changes: 4 additions & 1 deletion internal/api/handlers/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,12 @@ func parsePaginationQuery(r *http.Request) (string, *types.Error) {
return pageKey, nil
}

func parsePublicKeyQuery(r *http.Request, queryName string) (string, *types.Error) {
func parsePublicKeyQuery(r *http.Request, queryName string, isOptional bool) (string, *types.Error) {
pkHex := r.URL.Query().Get(queryName)
if pkHex == "" {
if isOptional {
return "", nil
}
return "", types.NewErrorWithMsg(
http.StatusBadRequest, types.BadRequest, queryName+" is required",
)
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/staker.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
// @Failure 400 {object} types.Error "Error: Bad Request"
// @Router /v1/staker/delegations [get]
func (h *Handler) GetStakerDelegations(request *http.Request) (*Result, *types.Error) {
stakerBtcPk, err := parsePublicKeyQuery(request, "staker_btc_pk")
stakerBtcPk, err := parsePublicKeyQuery(request, "staker_btc_pk", false)
if err != nil {
return nil, err
}
Expand Down
69 changes: 69 additions & 0 deletions internal/services/finality_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,75 @@ func (s *Services) GetFinalityProvidersFromGlobalParams() []*FpParamsPublic {
return fpDetails
}

func (s *Services) GetFinalityProvider(
ctx context.Context, fpPkHex string,
) (*FpDetailsPublic, *types.Error) {
fpStatsByPks, err :=
s.DbClient.FindFinalityProviderStatsByFinalityProviderPkHex(
ctx, []string{fpPkHex},
)
if err != nil {
log.Ctx(ctx).Error().Err(err).
Msg("Error while fetching finality provider from DB")
return nil, types.NewInternalServiceError(err)
}

fpParams := s.GetFinalityProvidersFromGlobalParams()
// Found nothing in the DB, we will try get the FP from global params
if len(fpStatsByPks) == 0 {
for _, fp := range fpParams {
if fp.BtcPk == fpPkHex {
return &FpDetailsPublic{
Description: fp.Description,
Commission: fp.Commission,
BtcPk: fp.BtcPk,
ActiveTvl: 0,
TotalTvl: 0,
ActiveDelegations: 0,
TotalDelegations: 0,
}, nil
}
}
// Return nil as nothing was found
return nil, nil
}
if len(fpStatsByPks) > 1 {
log.Ctx(ctx).Error().
Str("fpPkHex", fpPkHex).
Int("numFinalityProviders", len(fpStatsByPks)).
Msg("Found more than one finality provider with the same pk")
return nil, types.NewErrorWithMsg(
http.StatusInternalServerError, types.InternalServiceError,
"Found more than one finality provider with the same pk",
)
}
fpStat := fpStatsByPks[0]

var fpParamsPublic *FpParamsPublic
for _, fp := range fpParams {
if fp.BtcPk == fpStat.FinalityProviderPkHex {
fpParamsPublic = fp
break
}
}
if fpParamsPublic == nil {
fpParamsPublic = &FpParamsPublic{
Description: emptyFpDescriptionPublic,
Commission: "",
BtcPk: fpStat.FinalityProviderPkHex,
}
}
return &FpDetailsPublic{
Description: fpParamsPublic.Description,
Commission: fpParamsPublic.Commission,
BtcPk: fpStat.FinalityProviderPkHex,
ActiveTvl: fpStat.ActiveTvl,
TotalTvl: fpStat.TotalTvl,
ActiveDelegations: fpStat.ActiveDelegations,
TotalDelegations: fpStat.TotalDelegations,
}, nil
}

func (s *Services) GetFinalityProviders(ctx context.Context, page string) ([]*FpDetailsPublic, string, *types.Error) {
fpParams := s.GetFinalityProvidersFromGlobalParams()
if len(fpParams) == 0 {
Expand Down
92 changes: 77 additions & 15 deletions tests/integration_test/finality_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,8 @@ const (
func shouldGetFinalityProvidersSuccessfully(t *testing.T, testServer *TestServer) {
url := testServer.Server.URL + finalityProvidersPath
defer testServer.Close()
// Make a GET request to the finality providers endpoint
resp, err := http.Get(url)
assert.NoError(t, err, "making GET request to finality providers endpoint should not fail")
defer resp.Body.Close()

// Check that the status code is HTTP 200 OK
assert.Equal(t, http.StatusOK, resp.StatusCode, "expected HTTP 200 OK status")

// Read the response body
bodyBytes, err := io.ReadAll(resp.Body)
assert.NoError(t, err, "reading response body should not fail")

var responseBody handlers.PublicResponse[[]services.FpDetailsPublic]
err = json.Unmarshal(bodyBytes, &responseBody)
assert.NoError(t, err, "unmarshalling response body should not fail")

responseBody := fetchSuccessfulResponse[[]services.FpDetailsPublic](t, url)
result := responseBody.Data
assert.Equal(t, "Babylon Foundation 2", result[2].Description.Moniker)
assert.Equal(t, "0.060000000000000000", result[1].Commission)
Expand Down Expand Up @@ -98,6 +84,18 @@ func TestGetFinalityProviderReturn4xxErrorIfPageTokenInvalid(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}

func TestGetFinalityProviderReturn4xxErrorIfPkInvalid(t *testing.T) {
testServer := setupTestServer(t, nil)
url := testServer.Server.URL + finalityProvidersPath + "?fp_btc_pk=invalid"
defer testServer.Close()
// Make a GET request to the finality providers endpoint
resp, err := http.Get(url)
assert.NoError(t, err)
defer resp.Body.Close()

assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}

func FuzzGetFinalityProviderShouldReturnAllRegisteredFps(f *testing.F) {
attachRandomSeedsToFuzzer(f, 100)
f.Fuzz(func(t *testing.T, seed int64) {
Expand Down Expand Up @@ -331,6 +329,70 @@ func FuzzShouldNotReturnDefaultFpFromParamsWhenPageTokenIsPresent(f *testing.F)
})
}

func FuzzGetFinalityProvider(f *testing.F) {
attachRandomSeedsToFuzzer(f, 3)
f.Fuzz(func(t *testing.T, seed int64) {
r := rand.New(rand.NewSource(seed))
fpParams, registeredFpsStats, notRegisteredFpsStats := setUpFinalityProvidersStatsDataSet(t, r, nil)
// Manually force a single value for the finality provider to be used in db mocking
fpStats := []*model.FinalityProviderStatsDocument{registeredFpsStats[0]}

mockDB := new(testmock.DBClient)
mockDB.On("FindFinalityProviderStatsByFinalityProviderPkHex",
mock.Anything, mock.Anything,
).Return(fpStats, nil)

testServer := setupTestServer(t, &TestServerDependency{MockDbClient: mockDB, MockedFinalityProviders: fpParams})
url := testServer.Server.URL + finalityProvidersPath + "?fp_btc_pk=" + fpParams[0].BtcPk
// Make a GET request to the finality providers endpoint
respBody := fetchSuccessfulResponse[[]services.FpDetailsPublic](t, url)
result := respBody.Data
assert.Equal(t, 1, len(result))
assert.Equal(t, fpParams[0].Description.Moniker, result[0].Description.Moniker)
assert.Equal(t, fpParams[0].Commission, result[0].Commission)
assert.Equal(t, fpParams[0].BtcPk, result[0].BtcPk)
assert.Equal(t, registeredFpsStats[0].ActiveTvl, result[0].ActiveTvl)
assert.Equal(t, registeredFpsStats[0].TotalTvl, result[0].TotalTvl)
assert.Equal(t, registeredFpsStats[0].ActiveDelegations, result[0].ActiveDelegations)
assert.Equal(t, registeredFpsStats[0].TotalDelegations, result[0].TotalDelegations)
testServer.Close()

// Test the API with a non-existent finality provider from notRegisteredFpsStats
fpStats = []*model.FinalityProviderStatsDocument{notRegisteredFpsStats[0]}
mockDB = new(testmock.DBClient)
mockDB.On("FindFinalityProviderStatsByFinalityProviderPkHex",
mock.Anything, mock.Anything,
).Return(fpStats, nil)
testServer = setupTestServer(t, &TestServerDependency{
MockDbClient: mockDB, MockedFinalityProviders: fpParams,
})
notRegisteredFp := notRegisteredFpsStats[0]
url = testServer.Server.URL +
finalityProvidersPath +
"?fp_btc_pk=" + notRegisteredFp.FinalityProviderPkHex
respBody = fetchSuccessfulResponse[[]services.FpDetailsPublic](t, url)
result = respBody.Data
assert.Equal(t, 1, len(result))
assert.Equal(t, "", result[0].Description.Moniker)
assert.Equal(t, "", result[0].Commission)
assert.Equal(t, notRegisteredFp.FinalityProviderPkHex, result[0].BtcPk)
assert.Equal(t, notRegisteredFp.ActiveTvl, result[0].ActiveTvl)
testServer.Close()

// Test the API with a non-existent finality provider PK
randomPk, err := testutils.RandomPk()
testServer = setupTestServer(t, &TestServerDependency{
MockedFinalityProviders: fpParams,
})
defer testServer.Close()
assert.NoError(t, err, "generating random public key should not fail")
url = testServer.Server.URL + finalityProvidersPath + "?fp_btc_pk=" + randomPk
respBody = fetchSuccessfulResponse[[]services.FpDetailsPublic](t, url)
result = respBody.Data
assert.Equal(t, 0, len(result))
})
}

func generateFinalityProviderStatsDocument(r *rand.Rand, pk string) *model.FinalityProviderStatsDocument {
return &model.FinalityProviderStatsDocument{
FinalityProviderPkHex: pk,
Expand Down
23 changes: 23 additions & 0 deletions tests/integration_test/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"net/http/httptest"
"strings"
"testing"
Expand All @@ -15,11 +17,13 @@ import (
"github.com/babylonlabs-io/staking-queue-client/client"
"github.com/go-chi/chi"
"github.com/rabbitmq/amqp091-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

queueConfig "github.com/babylonlabs-io/staking-queue-client/config"

"github.com/babylonlabs-io/staking-api-service/internal/api"
"github.com/babylonlabs-io/staking-api-service/internal/api/handlers"
"github.com/babylonlabs-io/staking-api-service/internal/api/middlewares"
"github.com/babylonlabs-io/staking-api-service/internal/clients"
"github.com/babylonlabs-io/staking-api-service/internal/config"
Expand Down Expand Up @@ -263,3 +267,22 @@ func buildActiveStakingEvent(t *testing.T, numOfEvenet int) []*client.ActiveStak
func attachRandomSeedsToFuzzer(f *testing.F, numOfSeeds int) {
bbndatagen.AddRandomSeedsToFuzzer(f, uint(numOfSeeds))
}

func fetchSuccessfulResponse[T any](t *testing.T, url string) handlers.PublicResponse[T] {
// Make a GET request to the finality providers endpoint
resp, err := http.Get(url)
assert.NoError(t, err)
defer resp.Body.Close()

// Check that the status code is HTTP 200 OK
assert.Equal(t, http.StatusOK, resp.StatusCode, "expected HTTP 200 OK status")

// Read the response body
bodyBytes, err := io.ReadAll(resp.Body)
assert.NoError(t, err, "reading response body should not fail")

var responseBody handlers.PublicResponse[T]
err = json.Unmarshal(bodyBytes, &responseBody)
assert.NoError(t, err, "unmarshalling response body should not fail")
return responseBody
}

0 comments on commit dc2b2ff

Please sign in to comment.