Skip to content
Merged
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
7 changes: 7 additions & 0 deletions OpenApi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,13 @@ paths:
"200":
description: ""
summary: Get Pegin Reports
/reports/pegout:
get:
description: ' Get the last pegouts on the API. Included in the management API.'
responses:
"200":
description: ""
summary: Get Pegout Reports
/userQuotes:
get:
description: ' Returns user quotes for address.'
Expand Down
31 changes: 31 additions & 0 deletions internal/adapters/dataproviders/database/mongo/pegout.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,37 @@ func (repo *pegoutMongoRepository) GetQuote(ctx context.Context, hash string) (*
return &result.PegoutQuote, nil
}

func (repo *pegoutMongoRepository) GetQuotes(ctx context.Context, hashes []string) ([]quote.PegoutQuote, error) {
dbCtx, cancel := context.WithTimeout(ctx, repo.conn.timeout)
defer cancel()

for _, hash := range hashes {
if err := quote.ValidateQuoteHash(hash); err != nil {
return nil, err
}
}

collection := repo.conn.Collection(PegoutQuoteCollection)
filter := bson.M{"hash": bson.M{"$in": hashes}}

quotesReturn := make([]quote.PegoutQuote, 0)

cursor, err := collection.Find(dbCtx, filter)
if err != nil {
return nil, err
}
for cursor.Next(ctx) {
var result StoredPegoutQuote
err = cursor.Decode(&result)
if err != nil {
return nil, err
}
quotesReturn = append(quotesReturn, result.PegoutQuote)
}
logDbInteraction(Read, quotesReturn)
return quotesReturn, nil
}

func (repo *pegoutMongoRepository) GetRetainedQuote(ctx context.Context, hash string) (*quote.RetainedPegoutQuote, error) {
var result quote.RetainedPegoutQuote
dbCtx, cancel := context.WithTimeout(ctx, repo.conn.timeout)
Expand Down
43 changes: 43 additions & 0 deletions internal/adapters/dataproviders/database/mongo/pegout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -570,3 +570,46 @@ func TestPegoutMongoRepository_UpdateRetainedQuotes(t *testing.T) {
require.ErrorContains(t, err, "mismatch on updated documents. Expected 2, updated 1")
})
}

func TestPegoutMongoRepository_GetQuotes(t *testing.T) {
t.Run("Successfully retrieves quotes", func(t *testing.T) {
client, collection := getClientAndCollectionMocks(mongo.PegoutQuoteCollection)
log.SetLevel(log.DebugLevel)
hashes := []string{testRetainedPegoutQuote.QuoteHash}
repo := mongo.NewPegoutMongoRepository(mongo.NewConnection(client, time.Duration(1)))
collection.On("Find", mock.Anything,
bson.M{"hash": bson.M{"$in": hashes}},
).Return(mongoDb.NewCursorFromDocuments([]any{testPegoutQuote}, nil, nil)).Once()
result, err := repo.GetQuotes(context.Background(), hashes)
collection.AssertExpectations(t)
require.NoError(t, err)
assert.Equal(t, []quote.PegoutQuote{testPegoutQuote}, result)
})

t.Run("Fails validation for hashes", func(t *testing.T) {
client, _ := getClientAndCollectionMocks(mongo.PegoutQuoteCollection)

invalidHashes := []string{"invalidHash"}
conn := mongo.NewConnection(client, time.Duration(1))
repo := mongo.NewPegoutMongoRepository(conn)

_, err := repo.GetQuotes(context.Background(), invalidHashes)
require.Error(t, err)
assert.Equal(t, "invalid quote hash length: expected 64 characters, got 11", err.Error())
})

t.Run("error reading quotes from DB", func(t *testing.T) {
client, collection := getClientAndCollectionMocks(mongo.PegoutQuoteCollection)

expectedHashes := []string{testRetainedPegoutQuote.QuoteHash}
collection.On("Find", mock.Anything, bson.M{"hash": bson.M{"$in": expectedHashes}}).Return(nil, mongoDb.ErrNoDocuments).Once()

conn := mongo.NewConnection(client, time.Duration(1))
repo := mongo.NewPegoutMongoRepository(conn)

quotes, err := repo.GetQuotes(context.Background(), expectedHashes)
require.Error(t, err)
assert.Equal(t, "mongo: no documents in result", err.Error())
assert.Nil(t, quotes)
})
}
35 changes: 35 additions & 0 deletions internal/adapters/entrypoints/rest/handlers/get_reports_pegout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package handlers

import (
"github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest"
"github.com/rsksmart/liquidity-provider-server/internal/usecases/pegout"
"github.com/rsksmart/liquidity-provider-server/pkg"
"net/http"
)

// NewGetReportsPegoutHandler
// @Title Get Pegout Reports
// @Description Get the last pegouts on the API. Included in the management API.
// @Success 200 pkg.GetPegoutReportResponse
// @Route /reports/pegout [get]
func NewGetReportsPegoutHandler(useCase *pegout.GetPegoutReportUseCase) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
var err error

pegoutReport, err := useCase.Run(req.Context())
if err != nil {
jsonErr := rest.NewErrorResponseWithDetails(UnknownErrorMessage, rest.DetailsFromError(err), false)
rest.JsonErrorResponse(w, http.StatusInternalServerError, jsonErr)
return
}
response := pkg.GetPegoutReportResponse{
NumberOfQuotes: pegoutReport.NumberOfQuotes,
MinimumQuoteValue: pegoutReport.MinimumQuoteValue.AsBigInt(),
MaximumQuoteValue: pegoutReport.MaximumQuoteValue.AsBigInt(),
AverageQuoteValue: pegoutReport.AverageQuoteValue.AsBigInt(),
TotalFeesCollected: pegoutReport.TotalFeesCollected.AsBigInt(),
AverageFeePerQuote: pegoutReport.AverageFeePerQuote.AsBigInt(),
}
rest.JsonResponseWithBody(w, http.StatusOK, &response)
}
}
1 change: 1 addition & 0 deletions internal/adapters/entrypoints/rest/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ type UseCaseRegistry interface {
GetAvailableLiquidityUseCase() *liquidity_provider.GetAvailableLiquidityUseCase
GetServerInfoUseCase() *liquidity_provider.ServerInfoUseCase
GetPeginReportUseCase() *pegin.GetPeginReportUseCase
GetPegoutReportUseCase() *pegout.GetPegoutReportUseCase
}
5 changes: 5 additions & 0 deletions internal/adapters/entrypoints/rest/routes/management.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ func GetManagementEndpoints(env environment.Environment, useCaseRegistry registr
Method: http.MethodGet,
Handler: handlers.NewGetReportsPeginHandler(useCaseRegistry.GetPeginReportUseCase()),
},
{
Path: "/reports/pegout",
Method: http.MethodGet,
Handler: handlers.NewGetReportsPegoutHandler(useCaseRegistry.GetPegoutReportUseCase()),
},
{
Path: LoginPath,
Method: http.MethodPost,
Expand Down
3 changes: 2 additions & 1 deletion internal/adapters/entrypoints/rest/routes/management_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func TestGetManagementEndpoints(t *testing.T) {
registryMock.EXPECT().LoginUseCase().Return(&liquidity_provider.LoginUseCase{})
registryMock.EXPECT().GetManagementUiDataUseCase().Return(&liquidity_provider.GetManagementUiDataUseCase{})
registryMock.EXPECT().GetPeginReportUseCase().Return(&pegin.GetPeginReportUseCase{})
registryMock.EXPECT().GetPegoutReportUseCase().Return(&pegout.GetPegoutReportUseCase{})

endpoints := routes.GetManagementEndpoints(environment.Environment{}, registryMock, &mocks.StoreMock{})
specBytes := test.ReadFile(t, "OpenApi.yml")
Expand All @@ -40,7 +41,7 @@ func TestGetManagementEndpoints(t *testing.T) {
err := yaml.Unmarshal(specBytes, spec)
require.NoError(t, err)

assert.Len(t, endpoints, 18)
assert.Len(t, endpoints, 19)
for _, endpoint := range endpoints {
if endpoint.Path != routes.IconPath && endpoint.Path != routes.StaticPath {
lowerCaseMethod := strings.ToLower(endpoint.Method)
Expand Down
1 change: 1 addition & 0 deletions internal/adapters/entrypoints/rest/routes/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ func setupRegistryMock(registryMock *mocks.UseCaseRegistryMock) {
registryMock.EXPECT().GetManagementUiDataUseCase().Return(&liquidity_provider.GetManagementUiDataUseCase{})
registryMock.EXPECT().GetServerInfoUseCase().Return(&liquidity_provider.ServerInfoUseCase{})
registryMock.EXPECT().GetPeginReportUseCase().Return(&pegin.GetPeginReportUseCase{})
registryMock.EXPECT().GetPegoutReportUseCase().Return(&pegout.GetPegoutReportUseCase{})
}

func assertHasCorsHeaders(t *testing.T, recorder *httptest.ResponseRecorder) {
Expand Down
6 changes: 6 additions & 0 deletions internal/configuration/registry/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type UseCaseRegistry struct {
updatePeginDepositUseCase *watcher.UpdatePeginDepositUseCase
getServerInfoUseCase *liquidity_provider.ServerInfoUseCase
getPeginReportUseCase *pegin.GetPeginReportUseCase
getPegoutReportUseCase *pegout.GetPegoutReportUseCase
}

// NewUseCaseRegistry
Expand Down Expand Up @@ -235,6 +236,7 @@ func NewUseCaseRegistry(
updatePeginDepositUseCase: watcher.NewUpdatePeginDepositUseCase(databaseRegistry.PeginRepository),
getServerInfoUseCase: liquidity_provider.NewServerInfoUseCase(),
getPeginReportUseCase: pegin.NewGetPeginReportUseCase(databaseRegistry.PeginRepository),
getPegoutReportUseCase: pegout.NewGetPegoutReportUseCase(databaseRegistry.PegoutRepository),
}
}

Expand Down Expand Up @@ -353,3 +355,7 @@ func (registry *UseCaseRegistry) GetServerInfoUseCase() *liquidity_provider.Serv
func (registry *UseCaseRegistry) GetPeginReportUseCase() *pegin.GetPeginReportUseCase {
return registry.getPeginReportUseCase
}

func (registry *UseCaseRegistry) GetPegoutReportUseCase() *pegout.GetPegoutReportUseCase {
return registry.getPegoutReportUseCase
}
1 change: 1 addition & 0 deletions internal/entities/quote/pegout_quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
type PegoutQuoteRepository interface {
InsertQuote(ctx context.Context, hash string, quote PegoutQuote) error
GetQuote(ctx context.Context, hash string) (*PegoutQuote, error)
GetQuotes(ctx context.Context, hashes []string) ([]PegoutQuote, error)
GetRetainedQuote(ctx context.Context, hash string) (*RetainedPegoutQuote, error)
InsertRetainedQuote(ctx context.Context, quote RetainedPegoutQuote) error
ListPegoutDepositsByAddress(ctx context.Context, address string) ([]PegoutDeposit, error)
Expand Down
1 change: 1 addition & 0 deletions internal/usecases/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const (
UpdatePeginDepositId UseCaseId = "UpdatePeginDeposit"
ServerInfoId UseCaseId = "ServerInfo"
GetPeginReportId UseCaseId = "GetPeginReport"
GetPegoutReportId UseCaseId = "GetPegoutReport"
)

var (
Expand Down
156 changes: 156 additions & 0 deletions internal/usecases/pegout/get_pegout_report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package pegout

import (
"context"
"github.com/rsksmart/liquidity-provider-server/internal/entities"
"github.com/rsksmart/liquidity-provider-server/internal/entities/quote"
"github.com/rsksmart/liquidity-provider-server/internal/usecases"
)

type GetPegoutReportUseCase struct {
pegoutQuoteRepository quote.PegoutQuoteRepository
}

func NewGetPegoutReportUseCase(
pegoutQuoteRepository quote.PegoutQuoteRepository,
) *GetPegoutReportUseCase {
return &GetPegoutReportUseCase{
pegoutQuoteRepository: pegoutQuoteRepository,
}
}

type GetPegoutReportResult struct {
NumberOfQuotes int
MinimumQuoteValue *entities.Wei
MaximumQuoteValue *entities.Wei
AverageQuoteValue *entities.Wei
TotalFeesCollected *entities.Wei
AverageFeePerQuote *entities.Wei
}

func (useCase *GetPegoutReportUseCase) Run(ctx context.Context) (GetPegoutReportResult, error) {
var err error
var retained []quote.RetainedPegoutQuote
var minimumQuoteValue *entities.Wei
var maximumQuoteValue *entities.Wei
var averageQuoteValue *entities.Wei
var totalFeesCollected *entities.Wei
var averageFeePerQuote *entities.Wei

retained, err = useCase.pegoutQuoteRepository.GetRetainedQuoteByState(ctx, quote.PegoutStateRefundPegOutSucceeded)

if err != nil {
return GetPegoutReportResult{}, usecases.WrapUseCaseError(usecases.GetPegoutReportId, err)
}
if len(retained) == 0 {
return GetPegoutReportResult{
NumberOfQuotes: 0,
MinimumQuoteValue: entities.NewWei(0),
MaximumQuoteValue: entities.NewWei(0),
AverageQuoteValue: entities.NewWei(0),
TotalFeesCollected: entities.NewWei(0),
AverageFeePerQuote: entities.NewWei(0),
}, nil
}

quoteHashes := make([]string, 0)
for _, q := range retained {
quoteHashes = append(quoteHashes, q.QuoteHash)
}

quotes, err := useCase.pegoutQuoteRepository.GetQuotes(ctx, quoteHashes)
if err != nil {
return GetPegoutReportResult{}, usecases.WrapUseCaseError(usecases.GetPegoutReportId, err)
}

minimumQuoteValue = useCase.calculateMinimumQuoteValue(quotes)
maximumQuoteValue = useCase.calculateMaximumQuoteValue(quotes)
averageQuoteValue, err = useCase.calculateAverageQuoteValue(quotes)
if err != nil {
return GetPegoutReportResult{}, usecases.WrapUseCaseError(usecases.GetPegoutReportId, err)
}
totalFeesCollected = useCase.calculateTotalFeesCollected(quotes)
averageFeePerQuote, err = useCase.calculateAverageFeePerQuote(quotes)
if err != nil {
return GetPegoutReportResult{}, usecases.WrapUseCaseError(usecases.GetPegoutReportId, err)
}

return GetPegoutReportResult{
NumberOfQuotes: len(quotes),
MinimumQuoteValue: minimumQuoteValue,
MaximumQuoteValue: maximumQuoteValue,
AverageQuoteValue: averageQuoteValue,
TotalFeesCollected: totalFeesCollected,
AverageFeePerQuote: averageFeePerQuote,
}, nil
}

func (useCase *GetPegoutReportUseCase) calculateMinimumQuoteValue(quotes []quote.PegoutQuote) *entities.Wei {
if len(quotes) == 0 {
return entities.NewWei(0)
}
minimum := quotes[0].Value

for _, q := range quotes {
if q.Value.Cmp(minimum) < 0 {
minimum = q.Value
}
}

return minimum
}

func (useCase *GetPegoutReportUseCase) calculateMaximumQuoteValue(quotes []quote.PegoutQuote) *entities.Wei {
if len(quotes) == 0 {
return entities.NewWei(0)
}
maximum := quotes[0].Value

for _, q := range quotes {
if q.Value.Cmp(maximum) > 0 {
maximum = q.Value
}
}

return maximum
}

func (useCase *GetPegoutReportUseCase) calculateAverageQuoteValue(quotes []quote.PegoutQuote) (*entities.Wei, error) {
if len(quotes) == 0 {
return entities.NewWei(0), nil
}

total := entities.NewWei(0)

for _, q := range quotes {
total = total.Add(total, q.Value)
}

average, err := total.Div(total, entities.NewWei(int64(len(quotes))))
if err != nil {
return entities.NewWei(0), err
}

return average, nil
}

func (useCase *GetPegoutReportUseCase) calculateTotalFeesCollected(quotes []quote.PegoutQuote) *entities.Wei {
totalFees := entities.NewWei(0)

for _, q := range quotes {
totalFees = totalFees.Add(totalFees, q.CallFee)
}

return totalFees
}

func (useCase *GetPegoutReportUseCase) calculateAverageFeePerQuote(quotes []quote.PegoutQuote) (*entities.Wei, error) {
totalFees := useCase.calculateTotalFeesCollected(quotes)

averageFee, err := totalFees.Div(totalFees, entities.NewWei(int64(len(quotes))))
if err != nil {
return entities.NewWei(0), err
}

return averageFee, nil
}
Loading
Loading