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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions services/wallet/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/status-im/status-go/services/wallet/router"
"github.com/status-im/status-go/services/wallet/router/fees"
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/services/wallet/thirdparty/efp"
"github.com/status-im/status-go/services/wallet/token"
tokenTypes "github.com/status-im/status-go/services/wallet/token/types"
"github.com/status-im/status-go/services/wallet/tokenbalances"
Expand Down Expand Up @@ -826,3 +827,30 @@ func (api *API) UnsubscribeFromLeaderboard() error {
logutils.ZapLogger().Debug("call to UnsubscribeFromLeaderboard")
return api.s.leaderboardService.UnsubscribeFromLeaderboard()
}

// GetFollowingAddresses fetches the list of addresses that the given user is following via EFP
func (api *API) GetFollowingAddresses(ctx context.Context, userAddress common.Address, search string, limit, offset int) ([]efp.FollowingAddress, error) {
logutils.ZapLogger().Debug("call to GetFollowingAddresses",
zap.String("userAddress", userAddress.Hex()),
zap.String("search", search),
zap.Int("limit", limit),
zap.Int("offset", offset))

if api.s.followingManager == nil {
return nil, errors.New("following manager not initialized")
}

return api.s.followingManager.FetchFollowingAddresses(ctx, userAddress, search, limit, offset)
}

// GetFollowingStats fetches the stats (following count) for a user
func (api *API) GetFollowingStats(ctx context.Context, userAddress common.Address) (int, error) {
logutils.ZapLogger().Debug("call to GetFollowingStats",
zap.String("userAddress", userAddress.Hex()))

if api.s.followingManager == nil {
return 0, errors.New("following manager not initialized")
}

return api.s.followingManager.FetchFollowingStats(ctx, userAddress)
}
93 changes: 93 additions & 0 deletions services/wallet/following/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package following

import (
"context"
"errors"
"time"

"go.uber.org/zap"

"github.com/ethereum/go-ethereum/common"

"github.com/status-im/status-go/services/wallet/thirdparty/efp"
)

// Manager handles following address operations using EFP provider
type Manager struct {
provider efp.FollowingDataProvider
logger *zap.Logger
}

// NewManager creates a new following manager with the provided EFP provider
func NewManager(provider efp.FollowingDataProvider, logger *zap.Logger) *Manager {
return &Manager{
provider: provider,
logger: logger,
}
}

// FetchFollowingAddresses fetches the list of addresses that the given user is following
func (m *Manager) FetchFollowingAddresses(ctx context.Context, userAddress common.Address, search string, limit, offset int) ([]efp.FollowingAddress, error) {
m.logger.Debug("following.Manager.FetchFollowingAddresses",
zap.String("userAddress", userAddress.Hex()),
zap.String("search", search),
zap.Int("limit", limit),
zap.Int("offset", offset),
)

if m.provider == nil {
return nil, errors.New("EFP provider not initialized")
}

if !m.provider.IsConnected() {
m.logger.Warn("EFP provider not connected", zap.String("providerID", m.provider.ID()))
return []efp.FollowingAddress{}, nil
}

startTime := time.Now()
addresses, err := m.provider.FetchFollowingAddresses(ctx, userAddress, search, limit, offset)
duration := time.Since(startTime)

m.logger.Debug("following.Manager.FetchFollowingAddresses completed",
zap.String("userAddress", userAddress.Hex()),
zap.String("providerID", m.provider.ID()),
zap.Int("addresses.len", len(addresses)),
zap.Duration("duration", duration),
zap.Error(err),
)

if err != nil {
return nil, err
}

return addresses, nil
}

// FetchFollowingStats fetches the stats (following count) for a user
func (m *Manager) FetchFollowingStats(ctx context.Context, userAddress common.Address) (int, error) {
m.logger.Debug("following.Manager.FetchFollowingStats",
zap.String("userAddress", userAddress.Hex()),
)

if m.provider == nil {
return 0, errors.New("EFP provider not initialized")
}

if !m.provider.IsConnected() {
m.logger.Warn("EFP provider not connected", zap.String("providerID", m.provider.ID()))
return 0, nil
}

count, err := m.provider.FetchFollowingStats(ctx, userAddress)
if err != nil {
m.logger.Error("following.Manager.FetchFollowingStats error", zap.Error(err))
return 0, err
}

m.logger.Debug("following.Manager.FetchFollowingStats completed",
zap.String("userAddress", userAddress.Hex()),
zap.Int("count", count),
)

return count, nil
}
117 changes: 117 additions & 0 deletions services/wallet/following/manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package following

import (
"errors"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"go.uber.org/zap"

"github.com/status-im/status-go/services/wallet/thirdparty/efp"
mock_efp "github.com/status-im/status-go/services/wallet/thirdparty/efp/mock"
)

func TestFetchFollowingAddressesSuccess(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

ctx := t.Context()
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")

expected := []efp.FollowingAddress{
{
Address: common.HexToAddress("0x983110309620D911731Ac0932219af06091b6744"),
ENSName: "vitalik.eth",
},
}

mockProvider := mock_efp.NewMockFollowingDataProvider(mockCtrl)
mockProvider.EXPECT().ID().Return("efp").AnyTimes()
mockProvider.EXPECT().IsConnected().Return(true)
mockProvider.EXPECT().FetchFollowingAddresses(ctx, userAddress, "", 10, 0).Return(expected, nil)

manager := NewManager(mockProvider, zap.NewNop())

result, err := manager.FetchFollowingAddresses(ctx, userAddress, "", 10, 0)

require.NoError(t, err)
require.Len(t, result, 1)
require.Equal(t, expected[0].Address, result[0].Address)
require.Equal(t, expected[0].ENSName, result[0].ENSName)
}

func TestFetchFollowingStatsSuccess(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

ctx := t.Context()
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")
expectedCount := 150

mockProvider := mock_efp.NewMockFollowingDataProvider(mockCtrl)
mockProvider.EXPECT().IsConnected().Return(true)
mockProvider.EXPECT().FetchFollowingStats(ctx, userAddress).Return(expectedCount, nil)

manager := NewManager(mockProvider, zap.NewNop())

result, err := manager.FetchFollowingStats(ctx, userAddress)

require.NoError(t, err)
require.Equal(t, expectedCount, result)
}

func TestFetchFollowingAddressesProviderNotConnected(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

ctx := t.Context()
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")

mockProvider := mock_efp.NewMockFollowingDataProvider(mockCtrl)
mockProvider.EXPECT().IsConnected().Return(false)
mockProvider.EXPECT().ID().Return("efp").AnyTimes()

manager := NewManager(mockProvider, zap.NewNop())

result, err := manager.FetchFollowingAddresses(ctx, userAddress, "", 10, 0)

require.NoError(t, err)
require.Len(t, result, 0)
}

func TestFetchFollowingAddressesProviderError(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

ctx := t.Context()
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")
expectedError := errors.New("provider error")

mockProvider := mock_efp.NewMockFollowingDataProvider(mockCtrl)
mockProvider.EXPECT().ID().Return("efp").AnyTimes()
mockProvider.EXPECT().IsConnected().Return(true)
mockProvider.EXPECT().FetchFollowingAddresses(ctx, userAddress, "", 10, 0).Return(nil, expectedError)

manager := NewManager(mockProvider, zap.NewNop())

result, err := manager.FetchFollowingAddresses(ctx, userAddress, "", 10, 0)

require.Error(t, err)
require.Nil(t, result)
require.Equal(t, expectedError, err)
}

func TestFetchFollowingAddressesNoProvider(t *testing.T) {
ctx := t.Context()
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")

manager := NewManager(nil, zap.NewNop())

result, err := manager.FetchFollowingAddresses(ctx, userAddress, "", 10, 0)

require.Error(t, err)
require.Nil(t, result)
require.Contains(t, err.Error(), "EFP provider not initialized")
}
24 changes: 24 additions & 0 deletions services/wallet/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"time"

"github.com/golang/protobuf/proto"

Expand Down Expand Up @@ -41,6 +42,7 @@ import (
collectibles_ownership "github.com/status-im/status-go/services/wallet/collectibles/ownership"
"github.com/status-im/status-go/services/wallet/community"
"github.com/status-im/status-go/services/wallet/currency"
"github.com/status-im/status-go/services/wallet/following"
"github.com/status-im/status-go/services/wallet/leaderboard"
"github.com/status-im/status-go/services/wallet/market"
"github.com/status-im/status-go/services/wallet/onramp"
Expand All @@ -51,6 +53,7 @@ import (
activityfetcher_alchemy "github.com/status-im/status-go/services/wallet/thirdparty/activity/alchemy"
"github.com/status-im/status-go/services/wallet/thirdparty/collectibles/alchemy"
"github.com/status-im/status-go/services/wallet/thirdparty/collectibles/rarible"
"github.com/status-im/status-go/services/wallet/thirdparty/efp"
"github.com/status-im/status-go/services/wallet/thirdparty/market/coingecko"
"github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/services/wallet/transfer"
Expand Down Expand Up @@ -115,6 +118,7 @@ func NewService(
var collectibleProviders thirdparty.CollectibleProviders = thirdparty.CollectibleProviders{}
var pathProcessors []pathprocessor.PathProcessor = []pathprocessor.PathProcessor{}
var leaderboardConfig leaderboard.ServiceConfig = leaderboard.NewDefaultServiceConfig()
var followingManager *following.Manager

if thirdpartyServicesEnabled {

Expand Down Expand Up @@ -174,6 +178,24 @@ func NewService(
pathProcessors = buildPathProcessors(rpcClient, transactor, tokenManager, ensResolver, featureFlags)

leaderboardConfig = leaderboard.NewLeaderboardConfig(config.WalletConfig.MarketDataProxyConfig)

// EFP (Ethereum Follow Protocol) provider
efpHTTPClient := thirdparty.NewHTTPClient(
thirdparty.WithDetailedTimeouts(
5*time.Second, // dialTimeout
5*time.Second, // tlsHandshakeTimeout
5*time.Second, // responseHeaderTimeout
20*time.Second, // requestTimeout
),
thirdparty.WithMaxRetries(5),
)
efpClient := efp.NewClient(efpHTTPClient)
followingManager = following.NewManager(efpClient, logutils.ZapLogger().Named("FollowingManager"))
}

// Initialize followingManager with nil provider if third-party services are disabled
if followingManager == nil {
followingManager = following.NewManager(nil, logutils.ZapLogger().Named("FollowingManager"))
}

cryptoOnRampManager := onramp.NewManager(cryptoOnRampProviders)
Expand Down Expand Up @@ -281,6 +303,7 @@ func NewService(
cryptoOnRampManager: cryptoOnRampManager,
collectiblesManager: collectiblesManager,
collectibles: collectibles,
followingManager: followingManager,
gethManager: gethManager,
marketManager: marketManager,
transactor: transactor,
Expand Down Expand Up @@ -379,6 +402,7 @@ type Service struct {
cryptoOnRampManager *onramp.Manager
collectiblesManager *collectibles.Manager
collectibles *collectibles.Service
followingManager *following.Manager
gethManager *accsmanagement.AccountsManager
marketManager *market.Manager
transactor *transactions.Transactor
Expand Down
Loading
Loading