diff --git a/OpenApi.yml b/OpenApi.yml index 3a84f8bc..b68c70db 100644 --- a/OpenApi.yml +++ b/OpenApi.yml @@ -145,6 +145,13 @@ components: - quote - quoteHash type: object + GetReportsPeginPegoutRequest: + properties: + endDate: + type: string + startDate: + type: string + type: object HealthResponse: properties: services: @@ -716,6 +723,13 @@ components: required: - collateral type: object + pkg.GetReportsPeginPegoutRequest: + properties: + endDate: + type: string + startDate: + type: string + type: object info: title: Liquidity Provider Server version: 1.2.1 @@ -1034,6 +1048,32 @@ paths: "204": description: "" summary: Withdraw PegIn Collateral + /reports/pegin: + get: + description: ' Get the last pegins on the API. Included in the management API.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GetReportsPeginPegoutRequest' + required: true + responses: + "200": + description: "" + summary: Get Pegin Reports + /reports/pegout: + get: + description: ' Get the last pegouts on the API. Included in the management API.' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GetReportsPeginPegoutRequest' + required: true + responses: + "200": + description: "" + summary: Get Pegout Reports /userQuotes: get: description: ' Returns user quotes for address.' diff --git a/go.mod b/go.mod index 22010039..aae6909e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btcd/btcutil v1.1.5 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 github.com/ethereum/go-ethereum v1.14.5 github.com/go-playground/validator/v10 v10.17.0 github.com/gorilla/csrf v1.7.2 @@ -50,7 +51,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect diff --git a/go.mongodb.org/mongo-driver/mongo/cursor_mock.go b/go.mongodb.org/mongo-driver/mongo/cursor_mock.go new file mode 100644 index 00000000..a02fb935 --- /dev/null +++ b/go.mongodb.org/mongo-driver/mongo/cursor_mock.go @@ -0,0 +1,111 @@ +// Code generated by mockery v2.53.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + ses "github.com/aws/aws-sdk-go-v2/service/ses" + mock "github.com/stretchr/testify/mock" +) + +// SesClientMock is an autogenerated mock type for the sesClient type +type SesClientMock struct { + mock.Mock +} + +type SesClientMock_Expecter struct { + mock *mock.Mock +} + +func (_m *SesClientMock) EXPECT() *SesClientMock_Expecter { + return &SesClientMock_Expecter{mock: &_m.Mock} +} + +// SendEmail provides a mock function with given fields: ctx, params, optFns +func (_m *SesClientMock) SendEmail(ctx context.Context, params *ses.SendEmailInput, optFns ...func(*ses.Options)) (*ses.SendEmailOutput, error) { + _va := make([]interface{}, len(optFns)) + for _i := range optFns { + _va[_i] = optFns[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, params) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for SendEmail") + } + + var r0 *ses.SendEmailOutput + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *ses.SendEmailInput, ...func(*ses.Options)) (*ses.SendEmailOutput, error)); ok { + return rf(ctx, params, optFns...) + } + if rf, ok := ret.Get(0).(func(context.Context, *ses.SendEmailInput, ...func(*ses.Options)) *ses.SendEmailOutput); ok { + r0 = rf(ctx, params, optFns...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ses.SendEmailOutput) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *ses.SendEmailInput, ...func(*ses.Options)) error); ok { + r1 = rf(ctx, params, optFns...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SesClientMock_SendEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendEmail' +type SesClientMock_SendEmail_Call struct { + *mock.Call +} + +// SendEmail is a helper method to define mock.On call +// - ctx context.Context +// - params *ses.SendEmailInput +// - optFns ...func(*ses.Options) +func (_e *SesClientMock_Expecter) SendEmail(ctx interface{}, params interface{}, optFns ...interface{}) *SesClientMock_SendEmail_Call { + return &SesClientMock_SendEmail_Call{Call: _e.mock.On("SendEmail", + append([]interface{}{ctx, params}, optFns...)...)} +} + +func (_c *SesClientMock_SendEmail_Call) Run(run func(ctx context.Context, params *ses.SendEmailInput, optFns ...func(*ses.Options))) *SesClientMock_SendEmail_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]func(*ses.Options), len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(func(*ses.Options)) + } + } + run(args[0].(context.Context), args[1].(*ses.SendEmailInput), variadicArgs...) + }) + return _c +} + +func (_c *SesClientMock_SendEmail_Call) Return(_a0 *ses.SendEmailOutput, _a1 error) *SesClientMock_SendEmail_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *SesClientMock_SendEmail_Call) RunAndReturn(run func(context.Context, *ses.SendEmailInput, ...func(*ses.Options)) (*ses.SendEmailOutput, error)) *SesClientMock_SendEmail_Call { + _c.Call.Return(run) + return _c +} + +// NewSesClientMock creates a new instance of SesClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSesClientMock(t interface { + mock.TestingT + Cleanup(func()) +}) *SesClientMock { + mock := &SesClientMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/adapters/dataproviders/database/mongo/pegin.go b/internal/adapters/dataproviders/database/mongo/pegin.go index f5aa13c4..b868b088 100644 --- a/internal/adapters/dataproviders/database/mongo/pegin.go +++ b/internal/adapters/dataproviders/database/mongo/pegin.go @@ -10,6 +10,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" + "time" ) const ( @@ -107,6 +108,48 @@ func (repo *peginMongoRepository) GetQuote(ctx context.Context, hash string) (*q return &result.PeginQuote, nil } +func (repo *peginMongoRepository) GetQuotesByHashesAndDate( + ctx context.Context, + hashes []string, + startDate, + endDate time.Time, +) ([]quote.PeginQuote, 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(PeginQuoteCollection) + filter := bson.M{ + "hash": bson.M{"$in": hashes}, + "agreement_timestamp": bson.M{ + "$gte": startDate.Unix(), + "$lte": endDate.Unix(), + }, + } + + quotesReturn := make([]quote.PeginQuote, 0) + + cursor, err := collection.Find(dbCtx, filter) + if err != nil { + return nil, err + } + for cursor.Next(ctx) { + var result StoredPeginQuote + err = cursor.Decode(&result) + if err != nil { + return nil, err + } + quotesReturn = append(quotesReturn, result.PeginQuote) + } + logDbInteraction(Read, quotesReturn) + return quotesReturn, nil +} + func (repo *peginMongoRepository) GetRetainedQuote(ctx context.Context, hash string) (*quote.RetainedPeginQuote, error) { var result quote.RetainedPeginQuote dbCtx, cancel := context.WithTimeout(ctx, repo.conn.timeout) diff --git a/internal/adapters/dataproviders/database/mongo/pegin_test.go b/internal/adapters/dataproviders/database/mongo/pegin_test.go index 8bc93422..aa486f1d 100644 --- a/internal/adapters/dataproviders/database/mongo/pegin_test.go +++ b/internal/adapters/dataproviders/database/mongo/pegin_test.go @@ -444,3 +444,49 @@ func TestPeginMongoRepository_GetPeginCreationData(t *testing.T) { assert.Equal(t, quote.PeginCreationDataZeroValue(), result) }) } + +func TestPeginMongoRepository_GetQuotes(t *testing.T) { + t.Run("Get quotes with hash filters and timestamp filters", func(t *testing.T) { + client, db := getClientAndDatabaseMocks() + peginCollection := &mocks.CollectionBindingMock{} + + db.EXPECT().Collection(mongo.PeginQuoteCollection).Return(peginCollection) + + hashList := []string{"27d70ec2bc2c3154dc9a5b53b118a755441b22bc1c8ccde967ed33609970c25f"} + expectedQuotes := []quote.PeginQuote{testPeginQuote} + peginCollection.On("Find", mock.Anything, mock.MatchedBy(func(filter bson.M) bool { + return true + }), mock.Anything).Return(mongoDb.NewCursorFromDocuments([]any{testPeginQuote}, nil, nil)) + conn := mongo.NewConnection(client, time.Duration(1)) + repo := mongo.NewPeginMongoRepository(conn) + + startDateTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + endDateTime := time.Date(2025, 1, 1, 23, 59, 59, 0, time.UTC) + + result, err := repo.GetQuotesByHashesAndDate(context.Background(), hashList, startDateTime, endDateTime) + + require.NoError(t, err) + assert.Equal(t, expectedQuotes, result) + + peginCollection.AssertExpectations(t) + peginCollection.AssertExpectations(t) + }) + + t.Run("error reading quotes from DB", func(t *testing.T) { + client, collection := getClientAndCollectionMocks(mongo.PeginQuoteCollection) + + collection.On("Find", mock.Anything, mock.Anything).Return(nil, mongoDb.ErrNoDocuments).Once() + + conn := mongo.NewConnection(client, time.Duration(1)) + repo := mongo.NewPeginMongoRepository(conn) + + hashList := []string{"27d70ec2bc2c3154dc9a5b53b118a755441b22bc1c8ccde967ed33609970c25f"} + startDateTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + endDateTime := time.Date(2025, 1, 1, 23, 59, 59, 0, time.UTC) + + quotes, err := repo.GetQuotesByHashesAndDate(context.Background(), hashList, startDateTime, endDateTime) + require.Error(t, err) + assert.Equal(t, "mongo: no documents in result", err.Error()) + assert.Nil(t, quotes) + }) +} diff --git a/internal/adapters/dataproviders/database/mongo/pegout.go b/internal/adapters/dataproviders/database/mongo/pegout.go index 4fb4112d..a40d9b69 100644 --- a/internal/adapters/dataproviders/database/mongo/pegout.go +++ b/internal/adapters/dataproviders/database/mongo/pegout.go @@ -12,6 +12,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "regexp" + "time" ) const ( @@ -110,6 +111,46 @@ func (repo *pegoutMongoRepository) GetQuote(ctx context.Context, hash string) (* return &result.PegoutQuote, nil } +func (repo *pegoutMongoRepository) GetQuotesByHashesAndDate( + ctx context.Context, + hashes []string, + startDate, endDate time.Time, +) ([]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) + quotesReturn := make([]quote.PegoutQuote, 0) + filter := bson.M{ + "hash": bson.M{"$in": hashes}, + "agreement_timestamp": bson.M{ + "$gte": startDate.Unix(), + "$lte": endDate.Unix(), + }, + } + + 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) diff --git a/internal/adapters/dataproviders/database/mongo/pegout_test.go b/internal/adapters/dataproviders/database/mongo/pegout_test.go index d514a038..72bb2092 100644 --- a/internal/adapters/dataproviders/database/mongo/pegout_test.go +++ b/internal/adapters/dataproviders/database/mongo/pegout_test.go @@ -677,3 +677,49 @@ func TestPegoutMongoRepository_GetPegoutCreationData(t *testing.T) { assert.Equal(t, quote.PegoutCreationDataZeroValue(), result) }) } + +func TestPegoutMongoRepository_GetQuotes(t *testing.T) { + t.Run("Get quotes with hash filters and timestamp filters", func(t *testing.T) { + client, db := getClientAndDatabaseMocks() + pegoutCollection := &mocks.CollectionBindingMock{} + + db.EXPECT().Collection(mongo.PegoutQuoteCollection).Return(pegoutCollection) + + hashList := []string{"27d70ec2bc2c3154dc9a5b53b118a755441b22bc1c8ccde967ed33609970c25f"} + expectedQuotes := []quote.PegoutQuote{testPegoutQuote} + pegoutCollection.On("Find", mock.Anything, mock.MatchedBy(func(filter bson.M) bool { + return true + }), mock.Anything).Return(mongoDb.NewCursorFromDocuments([]any{testPegoutQuote}, nil, nil)) + conn := mongo.NewConnection(client, time.Duration(1)) + repo := mongo.NewPegoutMongoRepository(conn) + + startDateTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + endDateTime := time.Date(2025, 1, 1, 23, 59, 59, 0, time.UTC) + + result, err := repo.GetQuotesByHashesAndDate(context.Background(), hashList, startDateTime, endDateTime) + + require.NoError(t, err) + assert.Equal(t, expectedQuotes, result) + + pegoutCollection.AssertExpectations(t) + pegoutCollection.AssertExpectations(t) + }) + + t.Run("error reading quotes from DB", func(t *testing.T) { + client, collection := getClientAndCollectionMocks(mongo.PegoutQuoteCollection) + + collection.On("Find", mock.Anything, mock.Anything).Return(nil, mongoDb.ErrNoDocuments).Once() + + conn := mongo.NewConnection(client, time.Duration(1)) + repo := mongo.NewPegoutMongoRepository(conn) + + hashList := []string{"27d70ec2bc2c3154dc9a5b53b118a755441b22bc1c8ccde967ed33609970c25f"} + startDateTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + endDateTime := time.Date(2025, 1, 1, 23, 59, 59, 0, time.UTC) + + quotes, err := repo.GetQuotesByHashesAndDate(context.Background(), hashList, startDateTime, endDateTime) + require.Error(t, err) + assert.Equal(t, "mongo: no documents in result", err.Error()) + assert.Nil(t, quotes) + }) +} diff --git a/internal/adapters/entrypoints/rest/handlers/get_reports_pegin.go b/internal/adapters/entrypoints/rest/handlers/get_reports_pegin.go new file mode 100644 index 00000000..1fc0b3e6 --- /dev/null +++ b/internal/adapters/entrypoints/rest/handlers/get_reports_pegin.go @@ -0,0 +1,53 @@ +package handlers + +import ( + "github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest" + "github.com/rsksmart/liquidity-provider-server/internal/usecases/pegin" + "github.com/rsksmart/liquidity-provider-server/pkg" + "net/http" +) + +// NewGetReportsPeginHandler +// @Title Get Pegin Reports +// @Description Get the last pegins on the API. Included in the management API. +// @Param startDate query string true "Start date for the report" +// @Param endDate query string true "End date for the report" +// @Success 200 pkg.GetPeginReportResponse +// @Route /reports/pegin [get] +func NewGetReportsPeginHandler(useCase *pegin.GetPeginReportUseCase) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + var requestParams pkg.GetReportsPeginPegoutRequest + var err error + requestParams.StartDate = req.URL.Query().Get("startDate") + requestParams.EndDate = req.URL.Query().Get("endDate") + + if err = requestParams.ValidateGetReportsPeginPegoutRequest(); err != nil { + jsonErr := rest.NewErrorResponseWithDetails("Validation error", rest.DetailsFromError(err), false) + rest.JsonErrorResponse(w, http.StatusBadRequest, jsonErr) + return + } + + startTime, endTime, err := requestParams.GetTimestamps() + if err != nil { + jsonErr := rest.NewErrorResponseWithDetails("Date conversion error", rest.DetailsFromError(err), false) + rest.JsonErrorResponse(w, http.StatusBadRequest, jsonErr) + return + } + + peginReport, err := useCase.Run(req.Context(), startTime, endTime) + if err != nil { + jsonErr := rest.NewErrorResponseWithDetails(UnknownErrorMessage, rest.DetailsFromError(err), false) + rest.JsonErrorResponse(w, http.StatusInternalServerError, jsonErr) + return + } + response := pkg.GetPeginReportResponse{ + NumberOfQuotes: peginReport.NumberOfQuotes, + MinimumQuoteValue: peginReport.MinimumQuoteValue.AsBigInt(), + MaximumQuoteValue: peginReport.MaximumQuoteValue.AsBigInt(), + AverageQuoteValue: peginReport.AverageQuoteValue.AsBigInt(), + TotalFeesCollected: peginReport.TotalFeesCollected.AsBigInt(), + AverageFeePerQuote: peginReport.AverageFeePerQuote.AsBigInt(), + } + rest.JsonResponseWithBody(w, http.StatusOK, &response) + } +} diff --git a/internal/adapters/entrypoints/rest/handlers/get_reports_pegout.go b/internal/adapters/entrypoints/rest/handlers/get_reports_pegout.go new file mode 100644 index 00000000..cd2b35c0 --- /dev/null +++ b/internal/adapters/entrypoints/rest/handlers/get_reports_pegout.go @@ -0,0 +1,53 @@ +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. +// @Param startDate query string true "Start date for the report" +// @Param endDate query string true "End date for the report" +// @Success 200 pkg.GetPegoutReportResponse +// @Route /reports/pegout [get] +func NewGetReportsPegoutHandler(useCase *pegout.GetPegoutReportUseCase) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + var requestParams pkg.GetReportsPeginPegoutRequest + var err error + requestParams.StartDate = req.URL.Query().Get("startDate") + requestParams.EndDate = req.URL.Query().Get("endDate") + + if err = requestParams.ValidateGetReportsPeginPegoutRequest(); err != nil { + jsonErr := rest.NewErrorResponseWithDetails("Validation error", rest.DetailsFromError(err), false) + rest.JsonErrorResponse(w, http.StatusBadRequest, jsonErr) + return + } + + startTime, endTime, err := requestParams.GetTimestamps() + if err != nil { + jsonErr := rest.NewErrorResponseWithDetails("Date conversion error", rest.DetailsFromError(err), false) + rest.JsonErrorResponse(w, http.StatusBadRequest, jsonErr) + return + } + + pegoutReport, err := useCase.Run(req.Context(), startTime, endTime) + 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) + } +} diff --git a/internal/adapters/entrypoints/rest/registry/registry.go b/internal/adapters/entrypoints/rest/registry/registry.go index 6fa8d942..74fc42c8 100644 --- a/internal/adapters/entrypoints/rest/registry/registry.go +++ b/internal/adapters/entrypoints/rest/registry/registry.go @@ -35,4 +35,6 @@ type UseCaseRegistry interface { GetPegoutStatusUseCase() *pegout.StatusUseCase GetAvailableLiquidityUseCase() *liquidity_provider.GetAvailableLiquidityUseCase GetServerInfoUseCase() *liquidity_provider.ServerInfoUseCase + GetPeginReportUseCase() *pegin.GetPeginReportUseCase + GetPegoutReportUseCase() *pegout.GetPegoutReportUseCase } diff --git a/internal/adapters/entrypoints/rest/routes/management.go b/internal/adapters/entrypoints/rest/routes/management.go index 73c8da45..6b9c2f47 100644 --- a/internal/adapters/entrypoints/rest/routes/management.go +++ b/internal/adapters/entrypoints/rest/routes/management.go @@ -76,6 +76,16 @@ func GetManagementEndpoints(env environment.Environment, useCaseRegistry registr Method: http.MethodPost, Handler: handlers.NewSetPegoutConfigHandler(useCaseRegistry.SetPegoutConfigUseCase()), }, + { + Path: "/reports/pegin", + Method: http.MethodGet, + Handler: handlers.NewGetReportsPeginHandler(useCaseRegistry.GetPeginReportUseCase()), + }, + { + Path: "/reports/pegout", + Method: http.MethodGet, + Handler: handlers.NewGetReportsPegoutHandler(useCaseRegistry.GetPegoutReportUseCase()), + }, { Path: LoginPath, Method: http.MethodPost, diff --git a/internal/adapters/entrypoints/rest/routes/management_test.go b/internal/adapters/entrypoints/rest/routes/management_test.go index 25c36a82..3bc195b0 100644 --- a/internal/adapters/entrypoints/rest/routes/management_test.go +++ b/internal/adapters/entrypoints/rest/routes/management_test.go @@ -31,6 +31,8 @@ func TestGetManagementEndpoints(t *testing.T) { registryMock.EXPECT().SetCredentialsUseCase().Return(&liquidity_provider.SetCredentialsUseCase{}) 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") @@ -39,7 +41,7 @@ func TestGetManagementEndpoints(t *testing.T) { err := yaml.Unmarshal(specBytes, spec) require.NoError(t, err) - assert.Len(t, endpoints, 17) + assert.Len(t, endpoints, 19) for _, endpoint := range endpoints { if endpoint.Path != routes.IconPath && endpoint.Path != routes.StaticPath { lowerCaseMethod := strings.ToLower(endpoint.Method) diff --git a/internal/adapters/entrypoints/rest/routes/routes_test.go b/internal/adapters/entrypoints/rest/routes/routes_test.go index e26e4d92..9b5ddc89 100644 --- a/internal/adapters/entrypoints/rest/routes/routes_test.go +++ b/internal/adapters/entrypoints/rest/routes/routes_test.go @@ -238,6 +238,8 @@ func setupRegistryMock(registryMock *mocks.UseCaseRegistryMock) { registryMock.EXPECT().LoginUseCase().Return(&liquidity_provider.LoginUseCase{}) 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) { diff --git a/internal/configuration/registry/usecase.go b/internal/configuration/registry/usecase.go index 3e7d3aa1..62323e62 100644 --- a/internal/configuration/registry/usecase.go +++ b/internal/configuration/registry/usecase.go @@ -58,6 +58,8 @@ type UseCaseRegistry struct { availableLiquidityUseCase *liquidity_provider.GetAvailableLiquidityUseCase updatePeginDepositUseCase *watcher.UpdatePeginDepositUseCase getServerInfoUseCase *liquidity_provider.ServerInfoUseCase + getPeginReportUseCase *pegin.GetPeginReportUseCase + getPegoutReportUseCase *pegout.GetPegoutReportUseCase } // NewUseCaseRegistry @@ -233,6 +235,8 @@ func NewUseCaseRegistry( availableLiquidityUseCase: liquidity_provider.NewGetAvailableLiquidityUseCase(liquidityProvider, liquidityProvider, liquidityProvider), updatePeginDepositUseCase: watcher.NewUpdatePeginDepositUseCase(databaseRegistry.PeginRepository), getServerInfoUseCase: liquidity_provider.NewServerInfoUseCase(), + getPeginReportUseCase: pegin.NewGetPeginReportUseCase(databaseRegistry.PeginRepository), + getPegoutReportUseCase: pegout.NewGetPegoutReportUseCase(databaseRegistry.PegoutRepository), } } @@ -347,3 +351,11 @@ func (registry *UseCaseRegistry) GetAvailableLiquidityUseCase() *liquidity_provi func (registry *UseCaseRegistry) GetServerInfoUseCase() *liquidity_provider.ServerInfoUseCase { return registry.getServerInfoUseCase } + +func (registry *UseCaseRegistry) GetPeginReportUseCase() *pegin.GetPeginReportUseCase { + return registry.getPeginReportUseCase +} + +func (registry *UseCaseRegistry) GetPegoutReportUseCase() *pegout.GetPegoutReportUseCase { + return registry.getPegoutReportUseCase +} diff --git a/internal/entities/common.go b/internal/entities/common.go index 8cf58a0d..2b57cdb8 100644 --- a/internal/entities/common.go +++ b/internal/entities/common.go @@ -14,6 +14,7 @@ var ( SerializationError = errors.New("error during value serialization") IntegrityError = errors.New("error during value integrity check, stored hash doesn't match actual hash") validate = validator.New(validator.WithRequiredStructEnabled()) + DivideByZeroError = errors.New("divide by zero error") ) func ValidateStruct(s any) error { diff --git a/internal/entities/quote/pegin_quote.go b/internal/entities/quote/pegin_quote.go index 66fbc0bf..c7e0a085 100644 --- a/internal/entities/quote/pegin_quote.go +++ b/internal/entities/quote/pegin_quote.go @@ -2,10 +2,9 @@ package quote import ( "context" - "time" - "github.com/rsksmart/liquidity-provider-server/internal/entities" "github.com/rsksmart/liquidity-provider-server/internal/entities/utils" + "time" ) const ( @@ -30,6 +29,7 @@ type PeginQuoteRepository interface { InsertQuote(ctx context.Context, quote CreatedPeginQuote) error GetQuote(ctx context.Context, hash string) (*PeginQuote, error) GetPeginCreationData(ctx context.Context, hash string) PeginCreationData + GetQuotesByHashesAndDate(ctx context.Context, hashes []string, startDate, endDate time.Time) ([]PeginQuote, error) GetRetainedQuote(ctx context.Context, hash string) (*RetainedPeginQuote, error) InsertRetainedQuote(ctx context.Context, quote RetainedPeginQuote) error UpdateRetainedQuote(ctx context.Context, quote RetainedPeginQuote) error diff --git a/internal/entities/quote/pegout_quote.go b/internal/entities/quote/pegout_quote.go index ac13fbba..51a6ab56 100644 --- a/internal/entities/quote/pegout_quote.go +++ b/internal/entities/quote/pegout_quote.go @@ -33,6 +33,7 @@ type PegoutQuoteRepository interface { InsertQuote(ctx context.Context, quote CreatedPegoutQuote) error GetPegoutCreationData(ctx context.Context, hash string) PegoutCreationData GetQuote(ctx context.Context, hash string) (*PegoutQuote, error) + GetQuotesByHashesAndDate(ctx context.Context, hashes []string, startDate, endDate time.Time) ([]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) diff --git a/internal/entities/wei.go b/internal/entities/wei.go index aa8fe8a9..fd0e1005 100644 --- a/internal/entities/wei.go +++ b/internal/entities/wei.go @@ -137,3 +137,11 @@ func (w *Wei) Mul(x, y *Wei) *Wei { w.AsBigInt().Mul(x.AsBigInt(), y.AsBigInt()) return w } + +func (w *Wei) Div(x, y *Wei) (*Wei, error) { + if y.AsBigInt().Cmp(big.NewInt(0)) == 0 { + return nil, DivideByZeroError + } + w.AsBigInt().Div(x.AsBigInt(), y.AsBigInt()) + return w, nil +} diff --git a/internal/entities/wei_test.go b/internal/entities/wei_test.go index c5b18f10..2768044f 100644 --- a/internal/entities/wei_test.go +++ b/internal/entities/wei_test.go @@ -486,5 +486,52 @@ func TestWei_UnmarshalBSONValue(t *testing.T) { assert.Equal(t, bson.TypeString, bsonTypeResult) return bytes }) +} +func TestWei_Div(t *testing.T) { + tests := []struct { + name string + w *entities.Wei + x *entities.Wei + y *entities.Wei + want *entities.Wei + wantErr bool + }{ + { + name: "divide two positive Wei values", + w: entities.NewWei(0), + x: entities.NewWei(10), + y: entities.NewWei(5), + want: entities.NewWei(2), + wantErr: false, + }, + { + name: "divide by zero", + w: entities.NewWei(0), + x: entities.NewWei(10), + y: entities.NewWei(0), + want: nil, + wantErr: true, + }, + { + name: "divide zero by a number", + w: entities.NewWei(0), + x: entities.NewWei(0), + y: entities.NewWei(5), + want: entities.NewWei(0), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.w.Div(tt.x, tt.y) + if (err != nil) != tt.wantErr { + t.Errorf("Div() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Div() got = %v, want %v", got, tt.want) + } + }) + } } diff --git a/internal/usecases/common.go b/internal/usecases/common.go index d026227b..31779d7c 100644 --- a/internal/usecases/common.go +++ b/internal/usecases/common.go @@ -58,6 +58,8 @@ const ( GetAvailableLiquidityId UseCaseId = "GetAvailableLiquidity" UpdatePeginDepositId UseCaseId = "UpdatePeginDeposit" ServerInfoId UseCaseId = "ServerInfo" + GetPeginReportId UseCaseId = "GetPeginReport" + GetPegoutReportId UseCaseId = "GetPegoutReport" ) var ( diff --git a/internal/usecases/pegin/get_pegin_report.go b/internal/usecases/pegin/get_pegin_report.go new file mode 100644 index 00000000..63e94b3a --- /dev/null +++ b/internal/usecases/pegin/get_pegin_report.go @@ -0,0 +1,153 @@ +package pegin + +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" + "time" +) + +type GetPeginReportUseCase struct { + peginQuoteRepository quote.PeginQuoteRepository +} + +func NewGetPeginReportUseCase( + peginQuoteRepository quote.PeginQuoteRepository, +) *GetPeginReportUseCase { + return &GetPeginReportUseCase{ + peginQuoteRepository: peginQuoteRepository, + } +} + +type GetPeginReportResult struct { + NumberOfQuotes int + MinimumQuoteValue *entities.Wei + MaximumQuoteValue *entities.Wei + AverageQuoteValue *entities.Wei + TotalFeesCollected *entities.Wei + AverageFeePerQuote *entities.Wei +} + +func (useCase *GetPeginReportUseCase) Run(ctx context.Context, startDate time.Time, endDate time.Time) (GetPeginReportResult, error) { + var err error + var quotes []quote.PeginQuote + var response = GetPeginReportResult{ + NumberOfQuotes: 0, + MinimumQuoteValue: entities.NewWei(0), + MaximumQuoteValue: entities.NewWei(0), + AverageQuoteValue: entities.NewWei(0), + TotalFeesCollected: entities.NewWei(0), + AverageFeePerQuote: entities.NewWei(0), + } + + states := []quote.PeginState{quote.PeginStateRegisterPegInSucceeded} + retainedQuotes, err := useCase.peginQuoteRepository.GetRetainedQuoteByState(ctx, states...) + + if err != nil { + return GetPeginReportResult{}, usecases.WrapUseCaseError(usecases.GetPeginReportId, err) + } + + quoteHashes := make([]string, 0, len(retainedQuotes)) + for _, q := range retainedQuotes { + quoteHashes = append(quoteHashes, q.QuoteHash) + } + + if len(quoteHashes) == 0 { + return response, nil + } + + quotes, err = useCase.peginQuoteRepository.GetQuotesByHashesAndDate(ctx, quoteHashes, startDate, endDate) + + if err != nil { + return GetPeginReportResult{}, usecases.WrapUseCaseError(usecases.GetPeginReportId, err) + } + if len(quotes) == 0 { + return response, nil + } + + response.NumberOfQuotes = len(quotes) + response.MinimumQuoteValue = useCase.calculateMinimumQuoteValue(quotes) + response.MaximumQuoteValue = useCase.calculateMaximumQuoteValue(quotes) + response.AverageQuoteValue, err = useCase.calculateAverageQuoteValue(quotes) + if err != nil { + return GetPeginReportResult{}, usecases.WrapUseCaseError(usecases.GetPeginReportId, err) + } + response.TotalFeesCollected = useCase.calculateTotalFeesCollected(quotes) + response.AverageFeePerQuote, err = useCase.calculateAverageFeePerQuote(quotes) + if err != nil { + return GetPeginReportResult{}, usecases.WrapUseCaseError(usecases.GetPeginReportId, err) + } + + return response, nil +} + +func (useCase *GetPeginReportUseCase) calculateMinimumQuoteValue(quotes []quote.PeginQuote) *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 *GetPeginReportUseCase) calculateMaximumQuoteValue(quotes []quote.PeginQuote) *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 *GetPeginReportUseCase) calculateAverageQuoteValue(quotes []quote.PeginQuote) (*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 *GetPeginReportUseCase) calculateTotalFeesCollected(quotes []quote.PeginQuote) *entities.Wei { + totalFees := entities.NewWei(0) + + for _, q := range quotes { + totalFees = totalFees.Add(totalFees, q.CallFee) + } + + return totalFees +} + +func (useCase *GetPeginReportUseCase) calculateAverageFeePerQuote(quotes []quote.PeginQuote) (*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 +} diff --git a/internal/usecases/pegin/get_pegin_report_test.go b/internal/usecases/pegin/get_pegin_report_test.go new file mode 100644 index 00000000..0918a69d --- /dev/null +++ b/internal/usecases/pegin/get_pegin_report_test.go @@ -0,0 +1,115 @@ +package pegin_test + +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/pegin" + "github.com/rsksmart/liquidity-provider-server/test/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +// nolint:funlen +func TestGetPeginReportUseCase_Run(t *testing.T) { + ctx := context.Background() + + retainedQuotes := []quote.RetainedPeginQuote{ + {QuoteHash: "hash1"}, + {QuoteHash: "hash2"}, + {QuoteHash: "hash3"}, + {QuoteHash: "hash4"}, + {QuoteHash: "hash5"}, + {QuoteHash: "hash6"}, + {QuoteHash: "hash7"}, + {QuoteHash: "hash8"}, + {QuoteHash: "hash9"}, + {QuoteHash: "hash10"}, + } + + quoteHashes := []string{"hash1", "hash2", "hash3", "hash4", "hash5", "hash6", "hash7", "hash8", "hash9", "hash10"} + + peginQuotes := []quote.PeginQuote{ + {Value: entities.NewWei(1000), CallFee: entities.NewWei(10)}, + {Value: entities.NewWei(2000), CallFee: entities.NewWei(20)}, + {Value: entities.NewWei(3000), CallFee: entities.NewWei(30)}, + {Value: entities.NewWei(4000), CallFee: entities.NewWei(40)}, + {Value: entities.NewWei(5000), CallFee: entities.NewWei(50)}, + {Value: entities.NewWei(6000), CallFee: entities.NewWei(60)}, + {Value: entities.NewWei(7000), CallFee: entities.NewWei(70)}, + {Value: entities.NewWei(8000), CallFee: entities.NewWei(80)}, + {Value: entities.NewWei(9000), CallFee: entities.NewWei(90)}, + {Value: entities.NewWei(10000), CallFee: entities.NewWei(100)}, + } + + // Calculate expected values + expectedMinimumValue := entities.NewWei(1000) + expectedMaximumValue := entities.NewWei(10000) + expectedAverageValue := entities.NewWei(5500) + expectedTotalFees := entities.NewWei(550) + expectedAverageFee := entities.NewWei(55) + + startDate := time.Now() + endDate := time.Now().Add(time.Hour * 24 * 365 * 10) + + peginQuoteRepository := &mocks.PeginQuoteRepositoryMock{} + + peginQuoteRepository.On("GetRetainedQuoteByState", ctx, quote.PeginStateRegisterPegInSucceeded). + Return(retainedQuotes, nil).Once() + + peginQuoteRepository.On("GetQuotesByHashesAndDate", ctx, quoteHashes, startDate, endDate).Return(peginQuotes, nil).Once() + + useCase := pegin.NewGetPeginReportUseCase(peginQuoteRepository) + + result, err := useCase.Run(ctx, startDate, endDate) + + peginQuoteRepository.AssertExpectations(t) + require.NoError(t, err) + assert.Equal(t, 10, result.NumberOfQuotes) + assert.Equal(t, expectedMinimumValue, result.MinimumQuoteValue) + assert.Equal(t, expectedMaximumValue, result.MaximumQuoteValue) + assert.Equal(t, expectedAverageValue, result.AverageQuoteValue) + assert.Equal(t, expectedTotalFees, result.TotalFeesCollected) + assert.Equal(t, expectedAverageFee, result.AverageFeePerQuote) +} + +func TestGetPeginReportUseCase_Run_EmptyQuotes(t *testing.T) { + ctx := context.Background() + + retainedQuotes := []quote.RetainedPeginQuote{} + + peginQuoteRepository := &mocks.PeginQuoteRepositoryMock{} + peginQuoteRepository.On("GetRetainedQuoteByState", ctx, quote.PeginStateRegisterPegInSucceeded). + Return(retainedQuotes, nil).Once() + + useCase := pegin.NewGetPeginReportUseCase(peginQuoteRepository) + + result, err := useCase.Run(ctx, time.Now(), time.Now().Add(time.Hour*24*365*10)) + + peginQuoteRepository.AssertExpectations(t) + require.NoError(t, err) + assert.Equal(t, 0, result.NumberOfQuotes) + assert.Equal(t, entities.NewWei(0), result.MinimumQuoteValue) + assert.Equal(t, entities.NewWei(0), result.MaximumQuoteValue) + assert.Equal(t, entities.NewWei(0), result.AverageQuoteValue) + assert.Equal(t, entities.NewWei(0), result.TotalFeesCollected) + assert.Equal(t, entities.NewWei(0), result.AverageFeePerQuote) +} + +func TestGetPeginReportUseCase_Run_ErrorFetchingQuotes(t *testing.T) { + ctx := context.Background() + + peginQuoteRepository := &mocks.PeginQuoteRepositoryMock{} + peginQuoteRepository.On("GetRetainedQuoteByState", ctx, quote.PeginStateRegisterPegInSucceeded). + Return(nil, assert.AnError).Once() + + useCase := pegin.NewGetPeginReportUseCase(peginQuoteRepository) + + result, err := useCase.Run(ctx, time.Now(), time.Now().Add(time.Hour*24*365*10)) + + peginQuoteRepository.AssertExpectations(t) + require.Error(t, err) + assert.Zero(t, result.NumberOfQuotes) +} diff --git a/internal/usecases/pegout/get_pegout_report.go b/internal/usecases/pegout/get_pegout_report.go new file mode 100644 index 00000000..60aa0a7e --- /dev/null +++ b/internal/usecases/pegout/get_pegout_report.go @@ -0,0 +1,153 @@ +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" + "time" +) + +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, startDate time.Time, endDate time.Time) (GetPegoutReportResult, error) { + var err error + var quotes []quote.PegoutQuote + response := GetPegoutReportResult{ + NumberOfQuotes: 0, + MinimumQuoteValue: entities.NewWei(0), + MaximumQuoteValue: entities.NewWei(0), + AverageQuoteValue: entities.NewWei(0), + TotalFeesCollected: entities.NewWei(0), + AverageFeePerQuote: entities.NewWei(0), + } + + states := []quote.PegoutState{quote.PegoutStateRefundPegOutSucceeded} + retainedQuotes, err := useCase.pegoutQuoteRepository.GetRetainedQuoteByState(ctx, states...) + + if err != nil { + return GetPegoutReportResult{}, usecases.WrapUseCaseError(usecases.GetPeginReportId, err) + } + + quoteHashes := make([]string, 0, len(retainedQuotes)) + for _, q := range retainedQuotes { + quoteHashes = append(quoteHashes, q.QuoteHash) + } + + if len(quoteHashes) == 0 { + return response, nil + } + + quotes, err = useCase.pegoutQuoteRepository.GetQuotesByHashesAndDate(ctx, quoteHashes, startDate, endDate) + + if err != nil { + return GetPegoutReportResult{}, usecases.WrapUseCaseError(usecases.GetPeginReportId, err) + } + if len(quotes) == 0 { + return response, nil + } + + response.NumberOfQuotes = len(quotes) + response.MinimumQuoteValue = useCase.calculateMinimumQuoteValue(quotes) + response.MaximumQuoteValue = useCase.calculateMaximumQuoteValue(quotes) + response.AverageQuoteValue, err = useCase.calculateAverageQuoteValue(quotes) + if err != nil { + return GetPegoutReportResult{}, usecases.WrapUseCaseError(usecases.GetPeginReportId, err) + } + response.TotalFeesCollected = useCase.calculateTotalFeesCollected(quotes) + response.AverageFeePerQuote, err = useCase.calculateAverageFeePerQuote(quotes) + if err != nil { + return GetPegoutReportResult{}, usecases.WrapUseCaseError(usecases.GetPeginReportId, err) + } + + return response, 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 +} diff --git a/internal/usecases/pegout/get_pegout_report_test.go b/internal/usecases/pegout/get_pegout_report_test.go new file mode 100644 index 00000000..3272f454 --- /dev/null +++ b/internal/usecases/pegout/get_pegout_report_test.go @@ -0,0 +1,115 @@ +package pegout_test + +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/pegout" + "github.com/rsksmart/liquidity-provider-server/test/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +// nolint:funlen +func TestGetPegoutReportUseCase_Run(t *testing.T) { + ctx := context.Background() + + retainedQuotes := []quote.RetainedPegoutQuote{ + {QuoteHash: "hash1"}, + {QuoteHash: "hash2"}, + {QuoteHash: "hash3"}, + {QuoteHash: "hash4"}, + {QuoteHash: "hash5"}, + {QuoteHash: "hash6"}, + {QuoteHash: "hash7"}, + {QuoteHash: "hash8"}, + {QuoteHash: "hash9"}, + {QuoteHash: "hash10"}, + } + + quoteHashes := []string{"hash1", "hash2", "hash3", "hash4", "hash5", "hash6", "hash7", "hash8", "hash9", "hash10"} + + pegoutQuotes := []quote.PegoutQuote{ + {Value: entities.NewWei(1000), CallFee: entities.NewWei(10)}, + {Value: entities.NewWei(2000), CallFee: entities.NewWei(20)}, + {Value: entities.NewWei(3000), CallFee: entities.NewWei(30)}, + {Value: entities.NewWei(4000), CallFee: entities.NewWei(40)}, + {Value: entities.NewWei(5000), CallFee: entities.NewWei(50)}, + {Value: entities.NewWei(6000), CallFee: entities.NewWei(60)}, + {Value: entities.NewWei(7000), CallFee: entities.NewWei(70)}, + {Value: entities.NewWei(8000), CallFee: entities.NewWei(80)}, + {Value: entities.NewWei(9000), CallFee: entities.NewWei(90)}, + {Value: entities.NewWei(10000), CallFee: entities.NewWei(100)}, + } + + // Calculate expected values + expectedMinimumValue := entities.NewWei(1000) + expectedMaximumValue := entities.NewWei(10000) + expectedAverageValue := entities.NewWei(5500) + expectedTotalFees := entities.NewWei(550) + expectedAverageFee := entities.NewWei(55) + + startDate := time.Now() + endDate := time.Now().Add(time.Hour * 24 * 365 * 10) + + pegoutQuoteRepository := &mocks.PegoutQuoteRepositoryMock{} + + pegoutQuoteRepository.On("GetRetainedQuoteByState", ctx, quote.PegoutStateRefundPegOutSucceeded). + Return(retainedQuotes, nil).Once() + + pegoutQuoteRepository.On("GetQuotesByHashesAndDate", ctx, quoteHashes, startDate, endDate).Return(pegoutQuotes, nil).Once() + + useCase := pegout.NewGetPegoutReportUseCase(pegoutQuoteRepository) + + result, err := useCase.Run(ctx, startDate, endDate) + + pegoutQuoteRepository.AssertExpectations(t) + require.NoError(t, err) + assert.Equal(t, 10, result.NumberOfQuotes) + assert.Equal(t, expectedMinimumValue, result.MinimumQuoteValue) + assert.Equal(t, expectedMaximumValue, result.MaximumQuoteValue) + assert.Equal(t, expectedAverageValue, result.AverageQuoteValue) + assert.Equal(t, expectedTotalFees, result.TotalFeesCollected) + assert.Equal(t, expectedAverageFee, result.AverageFeePerQuote) +} + +func TestGetPegoutReportUseCase_Run_EmptyQuotes(t *testing.T) { + ctx := context.Background() + + retainedQuotes := []quote.RetainedPegoutQuote{} + + pegoutQuoteRepository := &mocks.PegoutQuoteRepositoryMock{} + pegoutQuoteRepository.On("GetRetainedQuoteByState", ctx, quote.PegoutStateRefundPegOutSucceeded). + Return(retainedQuotes, nil).Once() + + useCase := pegout.NewGetPegoutReportUseCase(pegoutQuoteRepository) + + result, err := useCase.Run(ctx, time.Now(), time.Now().Add(time.Hour*24*365*10)) + + pegoutQuoteRepository.AssertExpectations(t) + require.NoError(t, err) + assert.Equal(t, 0, result.NumberOfQuotes) + assert.Equal(t, entities.NewWei(0), result.MinimumQuoteValue) + assert.Equal(t, entities.NewWei(0), result.MaximumQuoteValue) + assert.Equal(t, entities.NewWei(0), result.AverageQuoteValue) + assert.Equal(t, entities.NewWei(0), result.TotalFeesCollected) + assert.Equal(t, entities.NewWei(0), result.AverageFeePerQuote) +} + +func TestGetPegoutReportUseCase_Run_ErrorFetchingQuotes(t *testing.T) { + ctx := context.Background() + + pegoutQuoteRepository := &mocks.PegoutQuoteRepositoryMock{} + pegoutQuoteRepository.On("GetRetainedQuoteByState", ctx, quote.PegoutStateRefundPegOutSucceeded). + Return(nil, assert.AnError).Once() + + useCase := pegout.NewGetPegoutReportUseCase(pegoutQuoteRepository) + + result, err := useCase.Run(ctx, time.Now(), time.Now().Add(time.Hour*24*365*10)) + + pegoutQuoteRepository.AssertExpectations(t) + require.Error(t, err) + assert.Zero(t, result.NumberOfQuotes) +} diff --git a/pkg/liquidity_provider.go b/pkg/liquidity_provider.go index 6bd87e89..41a0cae8 100644 --- a/pkg/liquidity_provider.go +++ b/pkg/liquidity_provider.go @@ -1,7 +1,9 @@ package pkg import ( + "errors" "math/big" + "time" "github.com/rsksmart/liquidity-provider-server/internal/entities" "github.com/rsksmart/liquidity-provider-server/internal/entities/liquidity_provider" @@ -84,6 +86,66 @@ type CredentialsUpdateRequest struct { NewPassword string `json:"newPassword" validate:"required"` } +type GetReportsPeginPegoutRequest struct { + StartDate string `json:"startDate" validate:"required,datetime=2006-01-02"` + EndDate string `json:"endDate" validate:"required,datetime=2006-01-02"` +} + +func (r *GetReportsPeginPegoutRequest) ValidateGetReportsPeginPegoutRequest() error { + if r.StartDate == "" { + return errors.New("startDate is required") + } + if r.EndDate == "" { + return errors.New("endDate is required") + } + + startDate, err := time.Parse(time.DateOnly, r.StartDate) + if err != nil { + return errors.New("startDate must be in format YYYY-MM-DD") + } + + endDate, err := time.Parse(time.DateOnly, r.EndDate) + if err != nil { + return errors.New("endDate must be in format YYYY-MM-DD") + } + + if endDate.Before(startDate) || endDate.Equal(startDate) { + return errors.New("endDate must be after startDate") + } + + return nil +} + +func (r *GetReportsPeginPegoutRequest) GetTimestamps() (startTime, endTime time.Time, err error) { + startTime, err = time.Parse(time.DateOnly, r.StartDate) + if err != nil { + return time.Time{}, time.Time{}, err + } + + endTime, err = time.Parse(time.DateOnly, r.EndDate) + if err != nil { + return time.Time{}, time.Time{}, err + } + + startTime = time.Date( + startTime.Year(), + startTime.Month(), + startTime.Day(), + 0, 0, 0, 0, + time.UTC, + ) + + endTime = time.Date( + endTime.Year(), + endTime.Month(), + endTime.Day(), + 23, 59, 59, 0, + time.UTC, + ) + + return startTime, endTime, nil +} + type AvailableLiquidityDTO struct { PeginLiquidityAmount *big.Int `json:"peginLiquidityAmount" example:"5000000000000000000" description:"Available liquidity for PegIn operations in wei" required:""` PegoutLiquidityAmount *big.Int `json:"pegoutLiquidityAmount" example:"5000000000000000000" description:"Available liquidity for PegOut operations in wei" required:""` diff --git a/pkg/liquidity_provider_test.go b/pkg/liquidity_provider_test.go index 7ffa669f..0414797e 100644 --- a/pkg/liquidity_provider_test.go +++ b/pkg/liquidity_provider_test.go @@ -1,8 +1,10 @@ package pkg_test import ( + "github.com/stretchr/testify/require" "math/big" "testing" + "time" "github.com/rsksmart/liquidity-provider-server/internal/entities" "github.com/rsksmart/liquidity-provider-server/internal/entities/liquidity_provider" @@ -147,3 +149,145 @@ func TestLocalLiquidityProvider_ProviderDTOValidation(t *testing.T) { test.AssertNonZeroValues(t, dto) }) } + +// nolint:funlen +func TestGetReportsPeginPegoutRequest_ValidateGetReportsPeginPegoutRequest(t *testing.T) { + t.Run("valid dates", func(t *testing.T) { + request := pkg.GetReportsPeginPegoutRequest{ + StartDate: "2023-01-01", + EndDate: "2023-01-02", + } + err := request.ValidateGetReportsPeginPegoutRequest() + assert.NoError(t, err) + }) + + t.Run("empty startDate", func(t *testing.T) { + request := pkg.GetReportsPeginPegoutRequest{ + StartDate: "", + EndDate: "2023-01-02", + } + err := request.ValidateGetReportsPeginPegoutRequest() + require.Error(t, err) + assert.Equal(t, "startDate is required", err.Error()) + }) + + t.Run("empty endDate", func(t *testing.T) { + request := pkg.GetReportsPeginPegoutRequest{ + StartDate: "2023-01-01", + EndDate: "", + } + err := request.ValidateGetReportsPeginPegoutRequest() + require.Error(t, err) + assert.Equal(t, "endDate is required", err.Error()) + }) + + t.Run("invalid startDate format", func(t *testing.T) { + request := pkg.GetReportsPeginPegoutRequest{ + StartDate: "01/01/2023", + EndDate: "2023-01-02", + } + err := request.ValidateGetReportsPeginPegoutRequest() + require.Error(t, err) + assert.Equal(t, "startDate must be in format YYYY-MM-DD", err.Error()) + }) + + t.Run("invalid endDate format", func(t *testing.T) { + request := pkg.GetReportsPeginPegoutRequest{ + StartDate: "2023-01-01", + EndDate: "01/02/2023", + } + err := request.ValidateGetReportsPeginPegoutRequest() + require.Error(t, err) + assert.Equal(t, "endDate must be in format YYYY-MM-DD", err.Error()) + }) + + t.Run("endDate equal to startDate", func(t *testing.T) { + request := pkg.GetReportsPeginPegoutRequest{ + StartDate: "2023-01-01", + EndDate: "2023-01-01", + } + err := request.ValidateGetReportsPeginPegoutRequest() + require.Error(t, err) + assert.Equal(t, "endDate must be after startDate", err.Error()) + }) + + t.Run("endDate before startDate", func(t *testing.T) { + request := pkg.GetReportsPeginPegoutRequest{ + StartDate: "2023-01-02", + EndDate: "2023-01-01", + } + err := request.ValidateGetReportsPeginPegoutRequest() + require.Error(t, err) + assert.Equal(t, "endDate must be after startDate", err.Error()) + }) +} + +// nolint:funlen +func TestGetReportsPeginPegoutRequest_GetTimestamps(t *testing.T) { + t.Run("valid dates", func(t *testing.T) { + request := pkg.GetReportsPeginPegoutRequest{ + StartDate: "2023-01-01", + EndDate: "2023-01-02", + } + startTime, endTime, err := request.GetTimestamps() + require.NoError(t, err) + + expectedStartTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + expectedEndTime := time.Date(2023, 1, 2, 23, 59, 59, 0, time.UTC) + + assert.Equal(t, expectedStartTime, startTime) + assert.Equal(t, expectedEndTime, endTime) + }) + + t.Run("invalid startDate format", func(t *testing.T) { + request := pkg.GetReportsPeginPegoutRequest{ + StartDate: "01/01/2023", + EndDate: "2023-01-02", + } + startTime, endTime, err := request.GetTimestamps() + require.Error(t, err) + assert.True(t, startTime.IsZero()) + assert.True(t, endTime.IsZero()) + }) + + t.Run("invalid endDate format", func(t *testing.T) { + request := pkg.GetReportsPeginPegoutRequest{ + StartDate: "2023-01-01", + EndDate: "01/02/2023", + } + startTime, endTime, err := request.GetTimestamps() + require.Error(t, err) + assert.True(t, startTime.IsZero()) + assert.True(t, endTime.IsZero()) + }) + + t.Run("sets time component correctly", func(t *testing.T) { + request := pkg.GetReportsPeginPegoutRequest{ + StartDate: "2023-05-15", + EndDate: "2023-06-20", + } + startTime, endTime, err := request.GetTimestamps() + require.NoError(t, err) + + // Start time should be at 00:00:00 + assert.Equal(t, 0, startTime.Hour()) + assert.Equal(t, 0, startTime.Minute()) + assert.Equal(t, 0, startTime.Second()) + assert.Equal(t, 0, startTime.Nanosecond()) + + // End time should be at 23:59:59 + assert.Equal(t, 23, endTime.Hour()) + assert.Equal(t, 59, endTime.Minute()) + assert.Equal(t, 59, endTime.Second()) + assert.Equal(t, 0, endTime.Nanosecond()) + + // Dates should be preserved + assert.Equal(t, 2023, startTime.Year()) + assert.Equal(t, time.Month(5), startTime.Month()) + assert.Equal(t, 15, startTime.Day()) + + assert.Equal(t, 2023, endTime.Year()) + assert.Equal(t, time.Month(6), endTime.Month()) + assert.Equal(t, 20, endTime.Day()) + }) +} diff --git a/pkg/pegin.go b/pkg/pegin.go index e41e3c42..3a8917a3 100644 --- a/pkg/pegin.go +++ b/pkg/pegin.go @@ -69,6 +69,15 @@ type AcceptPeginRespose struct { BitcoinDepositAddressHash string `json:"bitcoinDepositAddressHash" required:"" example:"0x0" description:"Hash of the deposit BTC address"` } +type GetPeginReportResponse struct { + NumberOfQuotes int `json:"numberOfQuotes" required:"" description:"Number of finalized pegin quotes"` + MinimumQuoteValue *big.Int `json:"minimumQuoteValue" required:"" description:"Minimum value of the quote"` + MaximumQuoteValue *big.Int `json:"maximumQuoteValue" required:"" description:"Maximum value of the quote"` + AverageQuoteValue *big.Int `json:"averageQuoteValue" required:"" description:"Average value of the quote"` + TotalFeesCollected *big.Int `json:"totalFeesCollected" required:"" description:"Total fees collected"` + AverageFeePerQuote *big.Int `json:"averageFeePerQuote" required:"" description:"Average fee per quote"` +} + func FromPeginQuoteDTO(dto PeginQuoteDTO) quote.PeginQuote { return quote.PeginQuote{ FedBtcAddress: dto.FedBTCAddr, diff --git a/pkg/pegout.go b/pkg/pegout.go index 0518a331..2bfdec14 100644 --- a/pkg/pegout.go +++ b/pkg/pegout.go @@ -126,3 +126,12 @@ type DepositEventDTO struct { BlockNumber uint64 `json:"-"` From string `json:"from" example:"0x0" description:"From Address"` } + +type GetPegoutReportResponse struct { + NumberOfQuotes int `json:"numberOfQuotes" required:"" description:"Number of finalized pegout quotes"` + MinimumQuoteValue *big.Int `json:"minimumQuoteValue" required:"" description:"Minimum value of the quote"` + MaximumQuoteValue *big.Int `json:"maximumQuoteValue" required:"" description:"Maximum value of the quote"` + AverageQuoteValue *big.Int `json:"averageQuoteValue" required:"" description:"Average value of the quote"` + TotalFeesCollected *big.Int `json:"totalFeesCollected" required:"" description:"Total fees collected"` + AverageFeePerQuote *big.Int `json:"averageFeePerQuote" required:"" description:"Average fee per quote"` +} diff --git a/test/mocks/abstract_factory_mock.go b/test/mocks/abstract_factory_mock.go index d1ed4e68..7bb3f038 100644 --- a/test/mocks/abstract_factory_mock.go +++ b/test/mocks/abstract_factory_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/bitcoin_wallet_mock.go b/test/mocks/bitcoin_wallet_mock.go index e28e2c2f..4b181421 100644 --- a/test/mocks/bitcoin_wallet_mock.go +++ b/test/mocks/bitcoin_wallet_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/client_adapter_mock.go b/test/mocks/client_adapter_mock.go index cdae24b1..c6d639d8 100644 --- a/test/mocks/client_adapter_mock.go +++ b/test/mocks/client_adapter_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/collection_binding_mock.go b/test/mocks/collection_binding_mock.go index c5caef30..44d1601d 100644 --- a/test/mocks/collection_binding_mock.go +++ b/test/mocks/collection_binding_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/db_binding_mock.go b/test/mocks/db_binding_mock.go index b8d3b056..58ac1383 100644 --- a/test/mocks/db_binding_mock.go +++ b/test/mocks/db_binding_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/db_client_binding_mock.go b/test/mocks/db_client_binding_mock.go index faa2c1a6..2e8db0e7 100644 --- a/test/mocks/db_client_binding_mock.go +++ b/test/mocks/db_client_binding_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/default_credentials_provider_mock.go b/test/mocks/default_credentials_provider_mock.go index ca760ce5..32f07b3d 100644 --- a/test/mocks/default_credentials_provider_mock.go +++ b/test/mocks/default_credentials_provider_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/event_iterator_adapter_mock.go b/test/mocks/event_iterator_adapter_mock.go index 52af6eec..c5ec1e8e 100644 --- a/test/mocks/event_iterator_adapter_mock.go +++ b/test/mocks/event_iterator_adapter_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/http_client_mock.go b/test/mocks/http_client_mock.go index 89a0c7f1..b9cf6905 100644 --- a/test/mocks/http_client_mock.go +++ b/test/mocks/http_client_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/lbc_adapter_mock.go b/test/mocks/lbc_adapter_mock.go index 4be22263..5be232d9 100644 --- a/test/mocks/lbc_adapter_mock.go +++ b/test/mocks/lbc_adapter_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/lbc_binding_mock.go b/test/mocks/lbc_binding_mock.go index e3a748a5..54306ed3 100644 --- a/test/mocks/lbc_binding_mock.go +++ b/test/mocks/lbc_binding_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/liquidity_provider_repository_mock.go b/test/mocks/liquidity_provider_repository_mock.go index 3c0b298a..feb26bfa 100644 --- a/test/mocks/liquidity_provider_repository_mock.go +++ b/test/mocks/liquidity_provider_repository_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/peg_configuration_mock.go b/test/mocks/peg_configuration_mock.go index 7c57a61b..443674d7 100644 --- a/test/mocks/peg_configuration_mock.go +++ b/test/mocks/peg_configuration_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/pegin_quote_repository_mock.go b/test/mocks/pegin_quote_repository_mock.go index a2dbe820..24301308 100644 --- a/test/mocks/pegin_quote_repository_mock.go +++ b/test/mocks/pegin_quote_repository_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks @@ -7,6 +7,8 @@ import ( quote "github.com/rsksmart/liquidity-provider-server/internal/entities/quote" mock "github.com/stretchr/testify/mock" + + time "time" ) // PeginQuoteRepositoryMock is an autogenerated mock type for the PeginQuoteRepository type @@ -185,6 +187,67 @@ func (_c *PeginQuoteRepositoryMock_GetQuote_Call) RunAndReturn(run func(context. return _c } +// GetQuotesByHashesAndDate provides a mock function with given fields: ctx, hashes, startDate, endDate +func (_m *PeginQuoteRepositoryMock) GetQuotesByHashesAndDate(ctx context.Context, hashes []string, startDate time.Time, endDate time.Time) ([]quote.PeginQuote, error) { + ret := _m.Called(ctx, hashes, startDate, endDate) + + if len(ret) == 0 { + panic("no return value specified for GetQuotesByHashesAndDate") + } + + var r0 []quote.PeginQuote + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string, time.Time, time.Time) ([]quote.PeginQuote, error)); ok { + return rf(ctx, hashes, startDate, endDate) + } + if rf, ok := ret.Get(0).(func(context.Context, []string, time.Time, time.Time) []quote.PeginQuote); ok { + r0 = rf(ctx, hashes, startDate, endDate) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]quote.PeginQuote) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string, time.Time, time.Time) error); ok { + r1 = rf(ctx, hashes, startDate, endDate) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PeginQuoteRepositoryMock_GetQuotesByHashesAndDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetQuotesByHashesAndDate' +type PeginQuoteRepositoryMock_GetQuotesByHashesAndDate_Call struct { + *mock.Call +} + +// GetQuotesByHashesAndDate is a helper method to define mock.On call +// - ctx context.Context +// - hashes []string +// - startDate time.Time +// - endDate time.Time +func (_e *PeginQuoteRepositoryMock_Expecter) GetQuotesByHashesAndDate(ctx interface{}, hashes interface{}, startDate interface{}, endDate interface{}) *PeginQuoteRepositoryMock_GetQuotesByHashesAndDate_Call { + return &PeginQuoteRepositoryMock_GetQuotesByHashesAndDate_Call{Call: _e.mock.On("GetQuotesByHashesAndDate", ctx, hashes, startDate, endDate)} +} + +func (_c *PeginQuoteRepositoryMock_GetQuotesByHashesAndDate_Call) Run(run func(ctx context.Context, hashes []string, startDate time.Time, endDate time.Time)) *PeginQuoteRepositoryMock_GetQuotesByHashesAndDate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]string), args[2].(time.Time), args[3].(time.Time)) + }) + return _c +} + +func (_c *PeginQuoteRepositoryMock_GetQuotesByHashesAndDate_Call) Return(_a0 []quote.PeginQuote, _a1 error) *PeginQuoteRepositoryMock_GetQuotesByHashesAndDate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *PeginQuoteRepositoryMock_GetQuotesByHashesAndDate_Call) RunAndReturn(run func(context.Context, []string, time.Time, time.Time) ([]quote.PeginQuote, error)) *PeginQuoteRepositoryMock_GetQuotesByHashesAndDate_Call { + _c.Call.Return(run) + return _c +} + // GetRetainedQuote provides a mock function with given fields: ctx, hash func (_m *PeginQuoteRepositoryMock) GetRetainedQuote(ctx context.Context, hash string) (*quote.RetainedPeginQuote, error) { ret := _m.Called(ctx, hash) diff --git a/test/mocks/pegout_quote_repository_mock.go b/test/mocks/pegout_quote_repository_mock.go index 543d8837..43a315ef 100644 --- a/test/mocks/pegout_quote_repository_mock.go +++ b/test/mocks/pegout_quote_repository_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks @@ -7,6 +7,8 @@ import ( quote "github.com/rsksmart/liquidity-provider-server/internal/entities/quote" mock "github.com/stretchr/testify/mock" + + time "time" ) // PegoutQuoteRepositoryMock is an autogenerated mock type for the PegoutQuoteRepository type @@ -185,6 +187,67 @@ func (_c *PegoutQuoteRepositoryMock_GetQuote_Call) RunAndReturn(run func(context return _c } +// GetQuotesByHashesAndDate provides a mock function with given fields: ctx, hashes, startDate, endDate +func (_m *PegoutQuoteRepositoryMock) GetQuotesByHashesAndDate(ctx context.Context, hashes []string, startDate time.Time, endDate time.Time) ([]quote.PegoutQuote, error) { + ret := _m.Called(ctx, hashes, startDate, endDate) + + if len(ret) == 0 { + panic("no return value specified for GetQuotesByHashesAndDate") + } + + var r0 []quote.PegoutQuote + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string, time.Time, time.Time) ([]quote.PegoutQuote, error)); ok { + return rf(ctx, hashes, startDate, endDate) + } + if rf, ok := ret.Get(0).(func(context.Context, []string, time.Time, time.Time) []quote.PegoutQuote); ok { + r0 = rf(ctx, hashes, startDate, endDate) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]quote.PegoutQuote) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string, time.Time, time.Time) error); ok { + r1 = rf(ctx, hashes, startDate, endDate) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PegoutQuoteRepositoryMock_GetQuotesByHashesAndDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetQuotesByHashesAndDate' +type PegoutQuoteRepositoryMock_GetQuotesByHashesAndDate_Call struct { + *mock.Call +} + +// GetQuotesByHashesAndDate is a helper method to define mock.On call +// - ctx context.Context +// - hashes []string +// - startDate time.Time +// - endDate time.Time +func (_e *PegoutQuoteRepositoryMock_Expecter) GetQuotesByHashesAndDate(ctx interface{}, hashes interface{}, startDate interface{}, endDate interface{}) *PegoutQuoteRepositoryMock_GetQuotesByHashesAndDate_Call { + return &PegoutQuoteRepositoryMock_GetQuotesByHashesAndDate_Call{Call: _e.mock.On("GetQuotesByHashesAndDate", ctx, hashes, startDate, endDate)} +} + +func (_c *PegoutQuoteRepositoryMock_GetQuotesByHashesAndDate_Call) Run(run func(ctx context.Context, hashes []string, startDate time.Time, endDate time.Time)) *PegoutQuoteRepositoryMock_GetQuotesByHashesAndDate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]string), args[2].(time.Time), args[3].(time.Time)) + }) + return _c +} + +func (_c *PegoutQuoteRepositoryMock_GetQuotesByHashesAndDate_Call) Return(_a0 []quote.PegoutQuote, _a1 error) *PegoutQuoteRepositoryMock_GetQuotesByHashesAndDate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *PegoutQuoteRepositoryMock_GetQuotesByHashesAndDate_Call) RunAndReturn(run func(context.Context, []string, time.Time, time.Time) ([]quote.PegoutQuote, error)) *PegoutQuoteRepositoryMock_GetQuotesByHashesAndDate_Call { + _c.Call.Return(run) + return _c +} + // GetRetainedQuote provides a mock function with given fields: ctx, hash func (_m *PegoutQuoteRepositoryMock) GetRetainedQuote(ctx context.Context, hash string) (*quote.RetainedPegoutQuote, error) { ret := _m.Called(ctx, hash) diff --git a/test/mocks/rootstock_rpc_server_mock.go b/test/mocks/rootstock_rpc_server_mock.go index dad0ba54..eda7a18a 100644 --- a/test/mocks/rootstock_rpc_server_mock.go +++ b/test/mocks/rootstock_rpc_server_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/rpc_client_binding_mock.go b/test/mocks/rpc_client_binding_mock.go index ff8ed0d1..672d0f70 100644 --- a/test/mocks/rpc_client_binding_mock.go +++ b/test/mocks/rpc_client_binding_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/rpc_client_mock.go b/test/mocks/rpc_client_mock.go index dfb2ae73..72787d9a 100644 --- a/test/mocks/rpc_client_mock.go +++ b/test/mocks/rpc_client_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/rsk_bridge_binding_mock.go b/test/mocks/rsk_bridge_binding_mock.go index c5ef8947..d0844fbc 100644 --- a/test/mocks/rsk_bridge_binding_mock.go +++ b/test/mocks/rsk_bridge_binding_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/rsk_signer_wallet_mock.go b/test/mocks/rsk_signer_wallet_mock.go index e44cbadd..68d594b2 100644 --- a/test/mocks/rsk_signer_wallet_mock.go +++ b/test/mocks/rsk_signer_wallet_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/ses_client_mock.go b/test/mocks/ses_client_mock.go index 2bf58b69..a02fb935 100644 --- a/test/mocks/ses_client_mock.go +++ b/test/mocks/ses_client_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/session_binding_mock.go b/test/mocks/session_binding_mock.go index 5020768f..317a2f19 100644 --- a/test/mocks/session_binding_mock.go +++ b/test/mocks/session_binding_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/store_mock.go b/test/mocks/store_mock.go index 0e7f4543..af5912ed 100644 --- a/test/mocks/store_mock.go +++ b/test/mocks/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/ticker_mock.go b/test/mocks/ticker_mock.go index 16d1c6dc..090a83bf 100644 --- a/test/mocks/ticker_mock.go +++ b/test/mocks/ticker_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/transaction_signer_mock.go b/test/mocks/transaction_signer_mock.go index 01a45648..4ae80db2 100644 --- a/test/mocks/transaction_signer_mock.go +++ b/test/mocks/transaction_signer_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks diff --git a/test/mocks/use_case_registry_mock.go b/test/mocks/use_case_registry_mock.go index 7a392205..d2968258 100644 --- a/test/mocks/use_case_registry_mock.go +++ b/test/mocks/use_case_registry_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.51.1. DO NOT EDIT. +// Code generated by mockery v2.53.0. DO NOT EDIT. package mocks @@ -543,6 +543,53 @@ func (_c *UseCaseRegistryMock_GetPeginQuoteUseCase_Call) RunAndReturn(run func() return _c } +// GetPeginReportUseCase provides a mock function with no fields +func (_m *UseCaseRegistryMock) GetPeginReportUseCase() *pegin.GetPeginReportUseCase { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetPeginReportUseCase") + } + + var r0 *pegin.GetPeginReportUseCase + if rf, ok := ret.Get(0).(func() *pegin.GetPeginReportUseCase); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*pegin.GetPeginReportUseCase) + } + } + + return r0 +} + +// UseCaseRegistryMock_GetPeginReportUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPeginReportUseCase' +type UseCaseRegistryMock_GetPeginReportUseCase_Call struct { + *mock.Call +} + +// GetPeginReportUseCase is a helper method to define mock.On call +func (_e *UseCaseRegistryMock_Expecter) GetPeginReportUseCase() *UseCaseRegistryMock_GetPeginReportUseCase_Call { + return &UseCaseRegistryMock_GetPeginReportUseCase_Call{Call: _e.mock.On("GetPeginReportUseCase")} +} + +func (_c *UseCaseRegistryMock_GetPeginReportUseCase_Call) Run(run func()) *UseCaseRegistryMock_GetPeginReportUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UseCaseRegistryMock_GetPeginReportUseCase_Call) Return(_a0 *pegin.GetPeginReportUseCase) *UseCaseRegistryMock_GetPeginReportUseCase_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UseCaseRegistryMock_GetPeginReportUseCase_Call) RunAndReturn(run func() *pegin.GetPeginReportUseCase) *UseCaseRegistryMock_GetPeginReportUseCase_Call { + _c.Call.Return(run) + return _c +} + // GetPeginStatusUseCase provides a mock function with no fields func (_m *UseCaseRegistryMock) GetPeginStatusUseCase() *pegin.StatusUseCase { ret := _m.Called() @@ -684,6 +731,53 @@ func (_c *UseCaseRegistryMock_GetPegoutQuoteUseCase_Call) RunAndReturn(run func( return _c } +// GetPegoutReportUseCase provides a mock function with no fields +func (_m *UseCaseRegistryMock) GetPegoutReportUseCase() *pegout.GetPegoutReportUseCase { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetPegoutReportUseCase") + } + + var r0 *pegout.GetPegoutReportUseCase + if rf, ok := ret.Get(0).(func() *pegout.GetPegoutReportUseCase); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*pegout.GetPegoutReportUseCase) + } + } + + return r0 +} + +// UseCaseRegistryMock_GetPegoutReportUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPegoutReportUseCase' +type UseCaseRegistryMock_GetPegoutReportUseCase_Call struct { + *mock.Call +} + +// GetPegoutReportUseCase is a helper method to define mock.On call +func (_e *UseCaseRegistryMock_Expecter) GetPegoutReportUseCase() *UseCaseRegistryMock_GetPegoutReportUseCase_Call { + return &UseCaseRegistryMock_GetPegoutReportUseCase_Call{Call: _e.mock.On("GetPegoutReportUseCase")} +} + +func (_c *UseCaseRegistryMock_GetPegoutReportUseCase_Call) Run(run func()) *UseCaseRegistryMock_GetPegoutReportUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UseCaseRegistryMock_GetPegoutReportUseCase_Call) Return(_a0 *pegout.GetPegoutReportUseCase) *UseCaseRegistryMock_GetPegoutReportUseCase_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UseCaseRegistryMock_GetPegoutReportUseCase_Call) RunAndReturn(run func() *pegout.GetPegoutReportUseCase) *UseCaseRegistryMock_GetPegoutReportUseCase_Call { + _c.Call.Return(run) + return _c +} + // GetPegoutStatusUseCase provides a mock function with no fields func (_m *UseCaseRegistryMock) GetPegoutStatusUseCase() *pegout.StatusUseCase { ret := _m.Called()