diff --git a/Makefile b/Makefile index a2d6de6d..7380184c 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,7 @@ run-unprocessed-events-replay-local: generate-mock-interface: cd internal/db && mockery --name=DBClient --output=../../tests/mocks --outpkg=mocks --filename=mock_db_client.go cd internal/clients/ordinals && mockery --name=OrdinalsClientInterface --output=../../../tests/mocks --outpkg=mocks --filename=mock_ordinal_client.go + cd internal/clients/coinmarketcap && mockery --name=CoinMarketCapClientInterface --output=../../../tests/mocks --outpkg=mocks --filename=mock_coinmarketcap_client.go test: ./bin/local-startup.sh; diff --git a/config/config-docker.yml b/config/config-docker.yml index ed016921..a9b719f1 100644 --- a/config/config-docker.yml +++ b/config/config-docker.yml @@ -36,3 +36,9 @@ assets: timeout: 1000 terms_acceptance_logging: enabled: true +external_apis: + coinmarketcap: + api_key: ${COINMARKETCAP_API_KEY} + base_url: "https://pro-api.coinmarketcap.com/v1" + timeout: 10s # http client timeout + cache_ttl: 300s # mongodb ttl diff --git a/config/config-local.yml b/config/config-local.yml index 97f8f53b..ab3d2199 100644 --- a/config/config-local.yml +++ b/config/config-local.yml @@ -36,3 +36,9 @@ assets: timeout: 5000 terms_acceptance_logging: enabled: true +external_apis: + coinmarketcap: + api_key: ${COINMARKETCAP_API_KEY} + base_url: "https://pro-api.coinmarketcap.com/v1" + timeout: 10s # http client timeout + cache_ttl: 300s # mongodb ttl diff --git a/internal/clients/clients.go b/internal/clients/clients.go index 2e042428..c7bf789b 100644 --- a/internal/clients/clients.go +++ b/internal/clients/clients.go @@ -1,12 +1,14 @@ package clients import ( + "github.com/babylonlabs-io/staking-api-service/internal/clients/coinmarketcap" "github.com/babylonlabs-io/staking-api-service/internal/clients/ordinals" "github.com/babylonlabs-io/staking-api-service/internal/config" ) type Clients struct { - Ordinals ordinals.OrdinalsClientInterface + Ordinals ordinals.OrdinalsClientInterface + CoinMarketCap coinmarketcap.CoinMarketCapClientInterface } func New(cfg *config.Config) *Clients { @@ -16,7 +18,13 @@ func New(cfg *config.Config) *Clients { ordinalsClient = ordinals.NewOrdinalsClient(cfg.Assets.Ordinals) } + var coinMarketCapClient *coinmarketcap.CoinMarketCapClient + if cfg.ExternalAPIs != nil && cfg.ExternalAPIs.CoinMarketCap != nil { + coinMarketCapClient = coinmarketcap.NewCoinMarketCapClient(cfg.ExternalAPIs.CoinMarketCap) + } + return &Clients{ - Ordinals: ordinalsClient, + Ordinals: ordinalsClient, + CoinMarketCap: coinMarketCapClient, } } diff --git a/internal/clients/coinmarketcap/coinmarketcap.go b/internal/clients/coinmarketcap/coinmarketcap.go new file mode 100644 index 00000000..5b9ef3bf --- /dev/null +++ b/internal/clients/coinmarketcap/coinmarketcap.go @@ -0,0 +1,99 @@ +package coinmarketcap + +import ( + "context" + "net/http" + + baseclient "github.com/babylonlabs-io/staking-api-service/internal/clients/base" + "github.com/babylonlabs-io/staking-api-service/internal/config" + "github.com/babylonlabs-io/staking-api-service/internal/types" +) + +type CoinMarketCapClient struct { + config *config.CoinMarketCapConfig + defaultHeaders map[string]string + httpClient *http.Client +} + +type CMCResponse struct { + Data map[string]CryptoData `json:"data"` +} + +type CryptoData struct { + Quote map[string]QuoteData `json:"quote"` +} + +type QuoteData struct { + Price float64 `json:"price"` +} + +func NewCoinMarketCapClient(config *config.CoinMarketCapConfig) *CoinMarketCapClient { + // Client is disabled if config is nil + if config == nil { + return nil + } + + httpClient := &http.Client{} + headers := map[string]string{ + "X-CMC_PRO_API_KEY": config.APIKey, + "Accept": "application/json", + } + + return &CoinMarketCapClient{ + config, + headers, + httpClient, + } +} + +// Necessary for the BaseClient interface +func (c *CoinMarketCapClient) GetBaseURL() string { + return c.config.BaseURL +} + +func (c *CoinMarketCapClient) GetDefaultRequestTimeout() int { + return int(c.config.Timeout.Milliseconds()) +} + +func (c *CoinMarketCapClient) GetHttpClient() *http.Client { + return c.httpClient +} + +func (c *CoinMarketCapClient) GetLatestBtcPrice(ctx context.Context) (float64, *types.Error) { + path := "/cryptocurrency/quotes/latest" + + opts := &baseclient.BaseClientOptions{ + Path: path + "?symbol=BTC", + TemplatePath: path, + Headers: c.defaultHeaders, + } + + // Use struct{} for input (no request body) + // Use CMCResponse for response type + response, err := baseclient.SendRequest[struct{}, CMCResponse]( + ctx, c, http.MethodGet, opts, nil, + ) + if err != nil { + return 0, err + } + + btcData, exists := response.Data["BTC"] + if !exists { + return 0, types.NewErrorWithMsg( + http.StatusInternalServerError, + types.InternalServiceError, + "BTC data not found in response", + ) + } + + usdQuote, exists := btcData.Quote["USD"] + if !exists { + return 0, types.NewErrorWithMsg( + http.StatusInternalServerError, + types.InternalServiceError, + "USD quote not found in response", + ) + } + + return usdQuote.Price, nil +} diff --git a/internal/clients/coinmarketcap/interface.go b/internal/clients/coinmarketcap/interface.go new file mode 100644 index 00000000..d5992406 --- /dev/null +++ b/internal/clients/coinmarketcap/interface.go @@ -0,0 +1,11 @@ +package coinmarketcap + +import ( + "context" + + "github.com/babylonlabs-io/staking-api-service/internal/types" +) + +type CoinMarketCapClientInterface interface { + GetLatestBtcPrice(ctx context.Context) (float64, *types.Error) +} diff --git a/internal/config/config.go b/internal/config/config.go index 3ebad611..53a921b6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -16,6 +16,7 @@ type Config struct { Metrics *MetricsConfig `mapstructure:"metrics"` Assets *AssetsConfig `mapstructure:"assets"` TermsAcceptanceLogging *TermsAcceptanceConfig `mapstructure:"terms_acceptance_logging"` + ExternalAPIs *ExternalAPIsConfig `mapstructure:"external_apis"` } func (cfg *Config) Validate() error { @@ -42,6 +43,13 @@ func (cfg *Config) Validate() error { } } + // ExternalAPIs is optional + if cfg.ExternalAPIs != nil { + if err := cfg.ExternalAPIs.Validate(); err != nil { + return err + } + } + return nil } diff --git a/internal/config/external_apis.go b/internal/config/external_apis.go new file mode 100644 index 00000000..c39d3a86 --- /dev/null +++ b/internal/config/external_apis.go @@ -0,0 +1,49 @@ +package config + +import ( + "fmt" + "time" +) + +type ExternalAPIsConfig struct { + CoinMarketCap *CoinMarketCapConfig `mapstructure:"coinmarketcap"` +} + +type CoinMarketCapConfig struct { + APIKey string `mapstructure:"api_key"` + BaseURL string `mapstructure:"base_url"` + Timeout time.Duration `mapstructure:"timeout"` + CacheTTL time.Duration `mapstructure:"cache_ttl"` +} + +func (cfg *ExternalAPIsConfig) Validate() error { + if cfg.CoinMarketCap == nil { + return fmt.Errorf("missing coinmarketcap config") + } + + if err := cfg.CoinMarketCap.Validate(); err != nil { + return err + } + + return nil +} + +func (cfg *CoinMarketCapConfig) Validate() error { + if cfg.APIKey == "" { + return fmt.Errorf("missing coinmarketcap api key") + } + + if cfg.BaseURL == "" { + return fmt.Errorf("missing coinmarketcap base url") + } + + if cfg.Timeout <= 0 { + return fmt.Errorf("invalid coinmarketcap timeout") + } + + if cfg.CacheTTL <= 0 { + return fmt.Errorf("invalid coinmarketcap cache ttl") + } + + return nil +} diff --git a/internal/db/btc_price.go b/internal/db/btc_price.go new file mode 100644 index 00000000..f9c65e61 --- /dev/null +++ b/internal/db/btc_price.go @@ -0,0 +1,40 @@ +package db + +import ( + "context" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + + "github.com/babylonlabs-io/staking-api-service/internal/db/model" +) + +func (db *Database) GetLatestBtcPrice(ctx context.Context) (*model.BtcPrice, error) { + client := db.Client.Database(db.DbName).Collection(model.BtcPriceCollection) + + var btcPrice model.BtcPrice + err := client.FindOne(ctx, bson.M{"_id": model.BtcPriceDocID}).Decode(&btcPrice) + if err != nil { + return nil, err + } + + return &btcPrice, nil +} + +func (db *Database) SetBtcPrice(ctx context.Context, price float64) error { + client := db.Client.Database(db.DbName).Collection(model.BtcPriceCollection) + + btcPrice := model.BtcPrice{ + ID: model.BtcPriceDocID, // Fixed ID for single document + Price: price, + CreatedAt: time.Now(), // For TTL index + } + + opts := options.Update().SetUpsert(true) + filter := bson.M{"_id": model.BtcPriceDocID} + update := bson.M{"$set": btcPrice} + + _, err := client.UpdateOne(ctx, filter, update, opts) + return err +} diff --git a/internal/db/interface.go b/internal/db/interface.go index d64280ba..b16f598e 100644 --- a/internal/db/interface.go +++ b/internal/db/interface.go @@ -106,6 +106,10 @@ type DBClient interface { ) (*DbResultMap[model.DelegationDocument], error) // SaveTermsAcceptance saves the acceptance of the terms of service of the public key SaveTermsAcceptance(ctx context.Context, termsAcceptance *model.TermsAcceptance) error + // GetLatestBtcPrice fetches the BTC price from the database. + GetLatestBtcPrice(ctx context.Context) (*model.BtcPrice, error) + // SetBtcPrice sets the latest BTC price in the database. + SetBtcPrice(ctx context.Context, price float64) error } type DelegationFilter struct { diff --git a/internal/db/model/btc_price.go b/internal/db/model/btc_price.go new file mode 100644 index 00000000..79aef812 --- /dev/null +++ b/internal/db/model/btc_price.go @@ -0,0 +1,11 @@ +package model + +import "time" + +const BtcPriceDocID = "btc_price" + +type BtcPrice struct { + ID string `bson:"_id"` // primary key, will always be "btc_price" to ensure single document + Price float64 `bson:"price"` + CreatedAt time.Time `bson:"created_at"` // TTL index will be on this field +} diff --git a/internal/db/model/setup.go b/internal/db/model/setup.go index 212157c5..afee237d 100644 --- a/internal/db/model/setup.go +++ b/internal/db/model/setup.go @@ -3,6 +3,7 @@ package model import ( "context" "fmt" + "strings" "time" "github.com/babylonlabs-io/staking-api-service/internal/config" @@ -22,6 +23,7 @@ const ( TimeLockCollection = "timelock_queue" UnbondingCollection = "unbonding_queue" BtcInfoCollection = "btc_info" + BtcPriceCollection = "btc_price" UnprocessableMsgCollection = "unprocessable_messages" PkAddressMappingsCollection = "pk_address_mappings" TermsAcceptanceCollection = "terms_acceptance" @@ -81,6 +83,14 @@ func Setup(ctx context.Context, cfg *config.Config) error { } } + // If external APIs are configured, create TTL index for BTC price collection + if cfg.ExternalAPIs != nil { + if err := createTTLIndexes(ctx, database, cfg.ExternalAPIs.CoinMarketCap.CacheTTL); err != nil { + log.Error().Err(err).Msg("Failed to create TTL index for BTC price") + return err + } + } + log.Info().Msg("Collections and Indexes created successfully.") return nil } @@ -123,3 +133,28 @@ func createIndex(ctx context.Context, database *mongo.Database, collectionName s log.Debug().Msg("Index created successfully on collection: " + collectionName) } + +func createTTLIndexes(ctx context.Context, database *mongo.Database, cacheTTL time.Duration) error { + collection := database.Collection(BtcPriceCollection) + + // First, drop the existing TTL index if it exists + _, err := collection.Indexes().DropOne(ctx, "created_at_1") + if err != nil && !strings.Contains(err.Error(), "not found") { + return fmt.Errorf("failed to drop existing TTL index: %w", err) + } + + // Create new TTL index + model := mongo.IndexModel{ + Keys: bson.D{{Key: "created_at", Value: 1}}, + Options: options.Index(). + SetExpireAfterSeconds(int32(cacheTTL.Seconds())). + SetName("created_at_1"), + } + + _, err = collection.Indexes().CreateOne(ctx, model) + if err != nil { + return fmt.Errorf("failed to create TTL index: %w", err) + } + + return nil +} diff --git a/internal/services/btc_price.go b/internal/services/btc_price.go new file mode 100644 index 00000000..cbbe73d4 --- /dev/null +++ b/internal/services/btc_price.go @@ -0,0 +1,34 @@ +package services + +import ( + "context" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/mongo" +) + +func (s *Services) GetLatestBtcPriceUsd(ctx context.Context) (float64, error) { + // Try to get price from MongoDB first + btcPrice, err := s.DbClient.GetLatestBtcPrice(ctx) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + // Document not found, fetch from CoinMarketCap + price, err := s.Clients.CoinMarketCap.GetLatestBtcPrice(ctx) + if err != nil { + return 0, fmt.Errorf("failed to fetch price from CoinMarketCap: %w", err) + } + + // Store in MongoDB with TTL + if err := s.DbClient.SetBtcPrice(ctx, price); err != nil { + return 0, fmt.Errorf("failed to cache btc price: %w", err) + } + + return price, nil + } + // Handle other database errors + return 0, fmt.Errorf("database error: %w", err) + } + + return btcPrice.Price, nil +} diff --git a/internal/services/stats.go b/internal/services/stats.go index 7d4a9a34..218627fc 100644 --- a/internal/services/stats.go +++ b/internal/services/stats.go @@ -3,6 +3,7 @@ package services import ( "context" "fmt" + "math" "net/http" "github.com/babylonlabs-io/staking-api-service/internal/db" @@ -11,13 +12,14 @@ import ( ) type OverallStatsPublic struct { - ActiveTvl int64 `json:"active_tvl"` - TotalTvl int64 `json:"total_tvl"` - ActiveDelegations int64 `json:"active_delegations"` - TotalDelegations int64 `json:"total_delegations"` - TotalStakers uint64 `json:"total_stakers"` - UnconfirmedTvl uint64 `json:"unconfirmed_tvl"` - PendingTvl uint64 `json:"pending_tvl"` + ActiveTvl int64 `json:"active_tvl"` + TotalTvl int64 `json:"total_tvl"` + ActiveDelegations int64 `json:"active_delegations"` + TotalDelegations int64 `json:"total_delegations"` + TotalStakers uint64 `json:"total_stakers"` + UnconfirmedTvl uint64 `json:"unconfirmed_tvl"` + PendingTvl uint64 `json:"pending_tvl"` + BtcPriceUsd *float64 `json:"btc_price_usd,omitempty"` // Optional field } type StakerStatsPublic struct { @@ -184,6 +186,19 @@ func (s *Services) GetOverallStats( pendingTvl = unconfirmedTvl - confirmedTvl } + // Only fetch BTC price if ExternalAPIs are configured + var btcPrice *float64 + if s.cfg.ExternalAPIs != nil && s.cfg.ExternalAPIs.CoinMarketCap != nil { + price, err := s.GetLatestBtcPriceUsd(ctx) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("error while fetching latest btc price") + btcPrice = nil // return empty field if error + } else { + roundedPrice := math.Round(price*100) / 100 + btcPrice = &roundedPrice + } + } + return &OverallStatsPublic{ ActiveTvl: int64(confirmedTvl), TotalTvl: stats.TotalTvl, @@ -192,6 +207,7 @@ func (s *Services) GetOverallStats( TotalStakers: stats.TotalStakers, UnconfirmedTvl: unconfirmedTvl, PendingTvl: pendingTvl, + BtcPriceUsd: btcPrice, }, nil } diff --git a/tests/config/config-test.yml b/tests/config/config-test.yml index db0e31c9..b3bd416b 100644 --- a/tests/config/config-test.yml +++ b/tests/config/config-test.yml @@ -36,3 +36,9 @@ assets: timeout: 100 terms_acceptance_logging: enabled: true +external_apis: + coinmarketcap: + api_key: ${COINMARKETCAP_API_KEY} + base_url: "https://pro-api.coinmarketcap.com/v1" + timeout: 10s # http client timeout + cache_ttl: 300s # mongodb ttl diff --git a/tests/integration_test/stats_test.go b/tests/integration_test/stats_test.go index 1bc312a6..380fd32c 100644 --- a/tests/integration_test/stats_test.go +++ b/tests/integration_test/stats_test.go @@ -11,12 +11,16 @@ import ( "time" "github.com/babylonlabs-io/staking-api-service/internal/api/handlers" + "github.com/babylonlabs-io/staking-api-service/internal/clients" "github.com/babylonlabs-io/staking-api-service/internal/config" "github.com/babylonlabs-io/staking-api-service/internal/db/model" "github.com/babylonlabs-io/staking-api-service/internal/services" + "github.com/babylonlabs-io/staking-api-service/internal/types" + "github.com/babylonlabs-io/staking-api-service/tests/mocks" "github.com/babylonlabs-io/staking-api-service/tests/testutils" "github.com/babylonlabs-io/staking-queue-client/client" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -466,6 +470,108 @@ func FuzzTestTopStakersWithPaginationResponse(f *testing.F) { }) } +func TestOverallStatsEndpointBTCPriceIntegration(t *testing.T) { + t.Run("should return BTC price when successfully fetched from CoinMarketCap", func(t *testing.T) { + // Setup mock + mockCMC := new(mocks.CoinMarketCapClientInterface) + mockCMC.On("GetLatestBtcPrice", mock.Anything). + Return(42000.50, nil) + + mockedClients := &clients.Clients{ + CoinMarketCap: mockCMC, + } + + testServer := setupTestServer(t, &TestServerDependency{ + MockedClients: mockedClients, + }) + defer testServer.Close() + + // Fetch stats and verify + stats := fetchOverallStatsEndpoint(t, testServer) + assert.NotNil(t, stats.BtcPriceUsd) + assert.Equal(t, 42000.50, *stats.BtcPriceUsd) + mockCMC.AssertExpectations(t) + }) + + t.Run("should return nil BTC price when CoinMarketCap fetch fails", func(t *testing.T) { + mockCMC := new(mocks.CoinMarketCapClientInterface) + mockCMC.On("GetLatestBtcPrice", mock.Anything). + Return(0.0, types.NewErrorWithMsg( + http.StatusInternalServerError, + types.InternalServiceError, + "failed to fetch BTC price", + )) + + mockedClients := &clients.Clients{ + CoinMarketCap: mockCMC, + } + + testServer := setupTestServer(t, &TestServerDependency{ + MockedClients: mockedClients, + }) + defer testServer.Close() + + // Fetch stats and verify + stats := fetchOverallStatsEndpoint(t, testServer) + assert.Nil(t, stats.BtcPriceUsd) + mockCMC.AssertExpectations(t) + }) + + t.Run("should return nil BTC price when external APIs are disabled", func(t *testing.T) { + // Setup mock + mockCMC := new(mocks.CoinMarketCapClientInterface) + mockCMC.On("GetLatestBtcPrice", mock.Anything). + Return(42000.50, nil) + + mockedClients := &clients.Clients{ + CoinMarketCap: mockCMC, + } + + testConfig, err := config.New("../config/config-test.yml") + if err != nil { + t.Fatalf("Failed to load test config: %v", err) + } + testConfig.ExternalAPIs = nil + + testServer := setupTestServer(t, &TestServerDependency{ + ConfigOverrides: testConfig, + MockedClients: mockedClients, + }) + defer testServer.Close() + + // Fetch stats and verify + stats := fetchOverallStatsEndpoint(t, testServer) + assert.Nil(t, stats.BtcPriceUsd) + }) + + t.Run("should return nil BTC price when CoinMarketCap is disabled", func(t *testing.T) { + // Setup mock + mockCMC := new(mocks.CoinMarketCapClientInterface) + mockCMC.On("GetLatestBtcPrice", mock.Anything). + Return(42000.50, nil) + + mockedClients := &clients.Clients{ + CoinMarketCap: mockCMC, + } + + testConfig, err := config.New("../config/config-test.yml") + if err != nil { + t.Fatalf("Failed to load test config: %v", err) + } + testConfig.ExternalAPIs.CoinMarketCap = nil + + testServer := setupTestServer(t, &TestServerDependency{ + ConfigOverrides: testConfig, + MockedClients: mockedClients, + }) + defer testServer.Close() + + // Fetch stats and verify + stats := fetchOverallStatsEndpoint(t, testServer) + assert.Nil(t, stats.BtcPriceUsd) + }) +} + func fetchFinalityEndpoint(t *testing.T, testServer *TestServer) []services.FpDetailsPublic { url := testServer.Server.URL + finalityProvidersPath // Make a GET request to the finality providers endpoint diff --git a/tests/mocks/mock_coinmarketcap_client.go b/tests/mocks/mock_coinmarketcap_client.go new file mode 100644 index 00000000..e4aab81f --- /dev/null +++ b/tests/mocks/mock_coinmarketcap_client.go @@ -0,0 +1,59 @@ +// Code generated by mockery v2.44.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + types "github.com/babylonlabs-io/staking-api-service/internal/types" + mock "github.com/stretchr/testify/mock" +) + +// CoinMarketCapClientInterface is an autogenerated mock type for the CoinMarketCapClientInterface type +type CoinMarketCapClientInterface struct { + mock.Mock +} + +// GetLatestBtcPrice provides a mock function with given fields: ctx +func (_m *CoinMarketCapClientInterface) GetLatestBtcPrice(ctx context.Context) (float64, *types.Error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetLatestBtcPrice") + } + + var r0 float64 + var r1 *types.Error + if rf, ok := ret.Get(0).(func(context.Context) (float64, *types.Error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) float64); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(context.Context) *types.Error); ok { + r1 = rf(ctx) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*types.Error) + } + } + + return r0, r1 +} + +// NewCoinMarketCapClientInterface creates a new instance of CoinMarketCapClientInterface. 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 NewCoinMarketCapClientInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CoinMarketCapClientInterface { + mock := &CoinMarketCapClientInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/mocks/mock_db_client.go b/tests/mocks/mock_db_client.go index 57fcbd60..ddbd7d76 100644 --- a/tests/mocks/mock_db_client.go +++ b/tests/mocks/mock_db_client.go @@ -334,6 +334,36 @@ func (_m *DBClient) GetLatestBtcInfo(ctx context.Context) (*model.BtcInfo, error return r0, r1 } +// GetLatestBtcPrice provides a mock function with given fields: ctx +func (_m *DBClient) GetLatestBtcPrice(ctx context.Context) (*model.BtcPrice, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetLatestBtcPrice") + } + + var r0 *model.BtcPrice + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*model.BtcPrice, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *model.BtcPrice); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.BtcPrice) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetOrCreateStatsLock provides a mock function with given fields: ctx, stakingTxHashHex, state func (_m *DBClient) GetOrCreateStatsLock(ctx context.Context, stakingTxHashHex string, state string) (*model.StatsLockDocument, error) { ret := _m.Called(ctx, stakingTxHashHex, state) @@ -634,6 +664,24 @@ func (_m *DBClient) ScanDelegationsPaginated(ctx context.Context, paginationToke return r0, r1 } +// SetBtcPrice provides a mock function with given fields: ctx, price +func (_m *DBClient) SetBtcPrice(ctx context.Context, price float64) error { + ret := _m.Called(ctx, price) + + if len(ret) == 0 { + panic("no return value specified for SetBtcPrice") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, float64) error); ok { + r0 = rf(ctx, price) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // SubtractFinalityProviderStats provides a mock function with given fields: ctx, stakingTxHashHex, fpPkHex, amount func (_m *DBClient) SubtractFinalityProviderStats(ctx context.Context, stakingTxHashHex string, fpPkHex string, amount uint64) error { ret := _m.Called(ctx, stakingTxHashHex, fpPkHex, amount)