Skip to content

Commit 84ee4b3

Browse files
committed
feat: Add EFP following address support
Introduces Ethereum Follow Protocol (EFP) integration for fetching following addresses and stats. Adds a following manager, EFP client, and related API endpoints, along with tests for both manager and client functionality.
1 parent a442bdb commit 84ee4b3

File tree

7 files changed

+597
-0
lines changed

7 files changed

+597
-0
lines changed

services/wallet/api.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/status-im/status-go/services/wallet/router"
3838
"github.com/status-im/status-go/services/wallet/router/fees"
3939
"github.com/status-im/status-go/services/wallet/thirdparty"
40+
"github.com/status-im/status-go/services/wallet/thirdparty/efp"
4041
"github.com/status-im/status-go/services/wallet/token"
4142
tokenTypes "github.com/status-im/status-go/services/wallet/token/types"
4243
"github.com/status-im/status-go/services/wallet/tokenbalances"
@@ -826,3 +827,30 @@ func (api *API) UnsubscribeFromLeaderboard() error {
826827
logutils.ZapLogger().Debug("call to UnsubscribeFromLeaderboard")
827828
return api.s.leaderboardService.UnsubscribeFromLeaderboard()
828829
}
830+
831+
// GetFollowingAddresses fetches the list of addresses that the given user is following via EFP
832+
func (api *API) GetFollowingAddresses(ctx context.Context, userAddress common.Address, search string, limit, offset int) ([]efp.FollowingAddress, error) {
833+
logutils.ZapLogger().Debug("call to GetFollowingAddresses",
834+
zap.String("userAddress", userAddress.Hex()),
835+
zap.String("search", search),
836+
zap.Int("limit", limit),
837+
zap.Int("offset", offset))
838+
839+
if api.s.followingManager == nil {
840+
return nil, errors.New("following manager not initialized")
841+
}
842+
843+
return api.s.followingManager.FetchFollowingAddresses(ctx, userAddress, search, limit, offset)
844+
}
845+
846+
// GetFollowingStats fetches the stats (following count) for a user
847+
func (api *API) GetFollowingStats(ctx context.Context, userAddress common.Address) (int, error) {
848+
logutils.ZapLogger().Debug("call to GetFollowingStats",
849+
zap.String("userAddress", userAddress.Hex()))
850+
851+
if api.s.followingManager == nil {
852+
return 0, errors.New("following manager not initialized")
853+
}
854+
855+
return api.s.followingManager.FetchFollowingStats(ctx, userAddress)
856+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package following
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"go.uber.org/zap"
8+
9+
"github.com/ethereum/go-ethereum/common"
10+
11+
"github.com/status-im/status-go/logutils"
12+
"github.com/status-im/status-go/services/wallet/thirdparty/efp"
13+
)
14+
15+
// Manager handles following address operations using EFP providers
16+
type Manager struct {
17+
providers []efp.FollowingDataProvider
18+
}
19+
20+
// NewManager creates a new following manager with the provided EFP providers
21+
func NewManager(providers []efp.FollowingDataProvider) *Manager {
22+
return &Manager{
23+
providers: providers,
24+
}
25+
}
26+
27+
// FetchFollowingAddresses fetches the list of addresses that the given user is following
28+
// Uses the first available provider (can be enhanced later with fallback logic)
29+
func (m *Manager) FetchFollowingAddresses(ctx context.Context, userAddress common.Address, search string, limit, offset int) ([]efp.FollowingAddress, error) {
30+
logutils.ZapLogger().Debug("following.Manager.FetchFollowingAddresses",
31+
zap.String("userAddress", userAddress.Hex()),
32+
zap.String("search", search),
33+
zap.Int("limit", limit),
34+
zap.Int("offset", offset),
35+
zap.Int("providers.len", len(m.providers)),
36+
)
37+
38+
if len(m.providers) == 0 {
39+
return []efp.FollowingAddress{}, nil
40+
}
41+
42+
// Use the first provider (EFP client)
43+
provider := m.providers[0]
44+
if !provider.IsConnected() {
45+
logutils.ZapLogger().Warn("EFP provider not connected", zap.String("providerID", provider.ID()))
46+
return []efp.FollowingAddress{}, nil
47+
}
48+
49+
startTime := time.Now()
50+
addresses, err := provider.FetchFollowingAddresses(ctx, userAddress, search, limit, offset)
51+
duration := time.Since(startTime)
52+
53+
logutils.ZapLogger().Debug("following.Manager.FetchFollowingAddresses completed",
54+
zap.String("userAddress", userAddress.Hex()),
55+
zap.String("providerID", provider.ID()),
56+
zap.Int("addresses.len", len(addresses)),
57+
zap.Duration("duration", duration),
58+
zap.Error(err),
59+
)
60+
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
return addresses, nil
66+
}
67+
68+
// FetchFollowingStats fetches the stats (following count) for a user
69+
func (m *Manager) FetchFollowingStats(ctx context.Context, userAddress common.Address) (int, error) {
70+
logutils.ZapLogger().Debug("following.Manager.FetchFollowingStats",
71+
zap.String("userAddress", userAddress.Hex()),
72+
)
73+
74+
if len(m.providers) == 0 {
75+
return 0, nil
76+
}
77+
78+
provider := m.providers[0]
79+
if !provider.IsConnected() {
80+
logutils.ZapLogger().Warn("EFP provider not connected", zap.String("providerID", provider.ID()))
81+
return 0, nil
82+
}
83+
84+
count, err := provider.FetchFollowingStats(ctx, userAddress)
85+
if err != nil {
86+
logutils.ZapLogger().Error("following.Manager.FetchFollowingStats error", zap.Error(err))
87+
return 0, err
88+
}
89+
90+
logutils.ZapLogger().Debug("following.Manager.FetchFollowingStats completed",
91+
zap.String("userAddress", userAddress.Hex()),
92+
zap.Int("count", count),
93+
)
94+
95+
return count, nil
96+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package following
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/stretchr/testify/require"
10+
"go.uber.org/mock/gomock"
11+
12+
"github.com/status-im/status-go/services/wallet/thirdparty/efp"
13+
mock_efp "github.com/status-im/status-go/services/wallet/thirdparty/efp/mock"
14+
)
15+
16+
func TestFetchFollowingAddressesSuccess(t *testing.T) {
17+
mockCtrl := gomock.NewController(t)
18+
defer mockCtrl.Finish()
19+
20+
ctx := context.TODO()
21+
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")
22+
23+
expected := []efp.FollowingAddress{
24+
{
25+
Address: common.HexToAddress("0x983110309620D911731Ac0932219af06091b6744"),
26+
ENSName: "vitalik.eth",
27+
},
28+
}
29+
30+
mockProvider := mock_efp.NewMockFollowingDataProvider(mockCtrl)
31+
mockProvider.EXPECT().ID().Return("efp").AnyTimes()
32+
mockProvider.EXPECT().IsConnected().Return(true)
33+
mockProvider.EXPECT().FetchFollowingAddresses(ctx, userAddress, "", 10, 0).Return(expected, nil)
34+
35+
manager := NewManager([]efp.FollowingDataProvider{mockProvider})
36+
37+
result, err := manager.FetchFollowingAddresses(ctx, userAddress, "", 10, 0)
38+
39+
require.NoError(t, err)
40+
require.Len(t, result, 1)
41+
require.Equal(t, expected[0].Address, result[0].Address)
42+
require.Equal(t, expected[0].ENSName, result[0].ENSName)
43+
}
44+
45+
func TestFetchFollowingStatsSuccess(t *testing.T) {
46+
mockCtrl := gomock.NewController(t)
47+
defer mockCtrl.Finish()
48+
49+
ctx := context.TODO()
50+
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")
51+
expectedCount := 150
52+
53+
mockProvider := mock_efp.NewMockFollowingDataProvider(mockCtrl)
54+
mockProvider.EXPECT().IsConnected().Return(true)
55+
mockProvider.EXPECT().FetchFollowingStats(ctx, userAddress).Return(expectedCount, nil)
56+
57+
manager := NewManager([]efp.FollowingDataProvider{mockProvider})
58+
59+
result, err := manager.FetchFollowingStats(ctx, userAddress)
60+
61+
require.NoError(t, err)
62+
require.Equal(t, expectedCount, result)
63+
}
64+
65+
func TestFetchFollowingAddressesProviderNotConnected(t *testing.T) {
66+
mockCtrl := gomock.NewController(t)
67+
defer mockCtrl.Finish()
68+
69+
ctx := context.TODO()
70+
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")
71+
72+
mockProvider := mock_efp.NewMockFollowingDataProvider(mockCtrl)
73+
mockProvider.EXPECT().IsConnected().Return(false)
74+
mockProvider.EXPECT().ID().Return("efp").AnyTimes()
75+
76+
manager := NewManager([]efp.FollowingDataProvider{mockProvider})
77+
78+
result, err := manager.FetchFollowingAddresses(ctx, userAddress, "", 10, 0)
79+
80+
require.NoError(t, err)
81+
require.Len(t, result, 0)
82+
}
83+
84+
func TestFetchFollowingAddressesProviderError(t *testing.T) {
85+
mockCtrl := gomock.NewController(t)
86+
defer mockCtrl.Finish()
87+
88+
ctx := context.TODO()
89+
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")
90+
expectedError := errors.New("provider error")
91+
92+
mockProvider := mock_efp.NewMockFollowingDataProvider(mockCtrl)
93+
mockProvider.EXPECT().ID().Return("efp").AnyTimes()
94+
mockProvider.EXPECT().IsConnected().Return(true)
95+
mockProvider.EXPECT().FetchFollowingAddresses(ctx, userAddress, "", 10, 0).Return(nil, expectedError)
96+
97+
manager := NewManager([]efp.FollowingDataProvider{mockProvider})
98+
99+
result, err := manager.FetchFollowingAddresses(ctx, userAddress, "", 10, 0)
100+
101+
require.Error(t, err)
102+
require.Nil(t, result)
103+
require.Equal(t, expectedError, err)
104+
}
105+
106+
func TestFetchFollowingAddressesNoProviders(t *testing.T) {
107+
ctx := context.TODO()
108+
userAddress := common.HexToAddress("0x742d35cc6cf4c7c7")
109+
110+
manager := NewManager([]efp.FollowingDataProvider{})
111+
112+
result, err := manager.FetchFollowingAddresses(ctx, userAddress, "", 10, 0)
113+
114+
require.NoError(t, err)
115+
require.Len(t, result, 0)
116+
}

services/wallet/service.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
collectibles_ownership "github.com/status-im/status-go/services/wallet/collectibles/ownership"
4242
"github.com/status-im/status-go/services/wallet/community"
4343
"github.com/status-im/status-go/services/wallet/currency"
44+
"github.com/status-im/status-go/services/wallet/following"
4445
"github.com/status-im/status-go/services/wallet/leaderboard"
4546
"github.com/status-im/status-go/services/wallet/market"
4647
"github.com/status-im/status-go/services/wallet/onramp"
@@ -51,6 +52,7 @@ import (
5152
activityfetcher_alchemy "github.com/status-im/status-go/services/wallet/thirdparty/activity/alchemy"
5253
"github.com/status-im/status-go/services/wallet/thirdparty/collectibles/alchemy"
5354
"github.com/status-im/status-go/services/wallet/thirdparty/collectibles/rarible"
55+
"github.com/status-im/status-go/services/wallet/thirdparty/efp"
5456
"github.com/status-im/status-go/services/wallet/thirdparty/market/coingecko"
5557
"github.com/status-im/status-go/services/wallet/token"
5658
"github.com/status-im/status-go/services/wallet/transfer"
@@ -246,6 +248,13 @@ func NewService(
246248
collectiblesOwnershipController,
247249
collectiblesPublisher)
248250

251+
// EFP (Ethereum Follow Protocol) providers
252+
efpClient := efp.NewClient()
253+
followingProviders := []efp.FollowingDataProvider{
254+
efpClient,
255+
}
256+
followingManager := following.NewManager(followingProviders)
257+
249258
activity := activity.NewService(db, accountsDB, tokenManager, collectiblesManager, feed)
250259

251260
router := router.NewRouter(rpcClient, transactor, tokenManager, tokenBalancesFetcher, marketManager, collectibles,
@@ -281,6 +290,7 @@ func NewService(
281290
cryptoOnRampManager: cryptoOnRampManager,
282291
collectiblesManager: collectiblesManager,
283292
collectibles: collectibles,
293+
followingManager: followingManager,
284294
gethManager: gethManager,
285295
marketManager: marketManager,
286296
transactor: transactor,
@@ -379,6 +389,7 @@ type Service struct {
379389
cryptoOnRampManager *onramp.Manager
380390
collectiblesManager *collectibles.Manager
381391
collectibles *collectibles.Service
392+
followingManager *following.Manager
382393
gethManager *accsmanagement.AccountsManager
383394
marketManager *market.Manager
384395
transactor *transactions.Transactor

0 commit comments

Comments
 (0)