Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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)
}
96 changes: 96 additions & 0 deletions services/wallet/following/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package following

import (
"context"
"time"

"go.uber.org/zap"

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

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

// Manager handles following address operations using EFP providers
type Manager struct {
providers []efp.FollowingDataProvider
}

// NewManager creates a new following manager with the provided EFP providers
func NewManager(providers []efp.FollowingDataProvider) *Manager {
return &Manager{
providers: providers,
}
}

// FetchFollowingAddresses fetches the list of addresses that the given user is following
// Uses the first available provider (can be enhanced later with fallback logic)
func (m *Manager) FetchFollowingAddresses(ctx context.Context, userAddress common.Address, search string, limit, offset int) ([]efp.FollowingAddress, error) {
logutils.ZapLogger().Debug("following.Manager.FetchFollowingAddresses",
zap.String("userAddress", userAddress.Hex()),
zap.String("search", search),
zap.Int("limit", limit),
zap.Int("offset", offset),
zap.Int("providers.len", len(m.providers)),
)

if len(m.providers) == 0 {
return []efp.FollowingAddress{}, nil
}

// Use the first provider (EFP client)
provider := m.providers[0]
if !provider.IsConnected() {
logutils.ZapLogger().Warn("EFP provider not connected", zap.String("providerID", provider.ID()))
return []efp.FollowingAddress{}, nil
}

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

logutils.ZapLogger().Debug("following.Manager.FetchFollowingAddresses completed",
zap.String("userAddress", userAddress.Hex()),
zap.String("providerID", 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) {
logutils.ZapLogger().Debug("following.Manager.FetchFollowingStats",
zap.String("userAddress", userAddress.Hex()),
)

if len(m.providers) == 0 {
return 0, nil
}

provider := m.providers[0]
if !provider.IsConnected() {
logutils.ZapLogger().Warn("EFP provider not connected", zap.String("providerID", provider.ID()))
return 0, nil
}

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

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

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

import (
"context"
"errors"
"testing"

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

"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 := context.TODO()
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([]efp.FollowingDataProvider{mockProvider})

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 := context.TODO()
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([]efp.FollowingDataProvider{mockProvider})

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 := context.TODO()
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")

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

manager := NewManager([]efp.FollowingDataProvider{mockProvider})

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 := context.TODO()
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([]efp.FollowingDataProvider{mockProvider})

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

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

func TestFetchFollowingAddressesNoProviders(t *testing.T) {
ctx := context.TODO()
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")

manager := NewManager([]efp.FollowingDataProvider{})

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

require.NoError(t, err)
require.Len(t, result, 0)
}
11 changes: 11 additions & 0 deletions services/wallet/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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 +52,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 @@ -246,6 +248,13 @@ func NewService(
collectiblesOwnershipController,
collectiblesPublisher)

// EFP (Ethereum Follow Protocol) providers
efpClient := efp.NewClient()
followingProviders := []efp.FollowingDataProvider{
efpClient,
}
followingManager := following.NewManager(followingProviders)

activity := activity.NewService(db, accountsDB, tokenManager, collectiblesManager, feed)

router := router.NewRouter(rpcClient, transactor, tokenManager, tokenBalancesFetcher, marketManager, collectibles,
Expand Down Expand Up @@ -281,6 +290,7 @@ func NewService(
cryptoOnRampManager: cryptoOnRampManager,
collectiblesManager: collectiblesManager,
collectibles: collectibles,
followingManager: followingManager,
gethManager: gethManager,
marketManager: marketManager,
transactor: transactor,
Expand Down Expand Up @@ -379,6 +389,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