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)
}
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use a slice of providers for cases where multiple providers can be used to fetch the same type of data. I'm not super well versed in EFP, but it doesn't look like that's something we expect to happen, right?
Moreover, we're just using m.providers[0] in all methods, so making this a slice is misleading, better just make it provider 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like we should return an error if the provider is not set and we make a request, otherwise we can't distinguish this from a "true" empty FollowingAddresses result.

}

// 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{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check how we conditionally populate the other provider lists https://github.com/status-im/status-go/blob/develop/services/wallet/service.go#L119
Please do the same thing here, we don't want accesses to the provider if thirdpartyServicesEnabled is false

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