Skip to content

Commit f20c2eb

Browse files
authored
fix(collectibles): reduce redundant nft fetches (#7392)
* check context cancellation on every iteration fixes status-im/status-app#20307
1 parent 3eb935c commit f20c2eb

7 files changed

Lines changed: 548 additions & 52 deletions

File tree

services/wallet/collectibles/ownership/controller.go

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -466,28 +466,28 @@ func (c *Controller) loadWithPeriodicalLoaderIfFound(ctx context.Context, chainI
466466
_ = loader.Load(ctx)
467467
}()
468468

469-
select {
470-
case <-ctx.Done():
471-
err = ctx.Err()
472-
return
473-
case finishedEvent, ok := <-finishedCh:
474-
if !ok {
475-
return
476-
}
477-
if finishedEvent.ChainID == chainID && finishedEvent.Account == account {
478-
return
479-
}
480-
case errEvent, ok := <-errCh:
481-
if !ok {
482-
return
483-
}
484-
if errEvent.ChainID == chainID && errEvent.Account == account {
485-
err = errEvent.Error
469+
for {
470+
select {
471+
case <-ctx.Done():
472+
err = ctx.Err()
486473
return
474+
case finishedEvent, ok := <-finishedCh:
475+
if !ok {
476+
return
477+
}
478+
if finishedEvent.ChainID == chainID && finishedEvent.Account == account {
479+
return
480+
}
481+
case errEvent, ok := <-errCh:
482+
if !ok {
483+
return
484+
}
485+
if errEvent.ChainID == chainID && errEvent.Account == account {
486+
err = errEvent.Error
487+
return
488+
}
487489
}
488490
}
489-
490-
return
491491
}
492492

493493
func (c *Controller) checkPeriodicalLoaders() {

services/wallet/collectibles/ownership/controller_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ package ownership_test
33
import (
44
"context"
55
"math/big"
6+
"sync/atomic"
67
"testing"
78
"time"
89

910
"github.com/ethereum/go-ethereum/common"
11+
coretypes "github.com/ethereum/go-ethereum/core/types"
1012

1113
"github.com/status-im/go-wallet-sdk/pkg/balance/multistandardfetcher"
14+
"github.com/status-im/go-wallet-sdk/pkg/contracts/erc1155"
15+
"github.com/status-im/go-wallet-sdk/pkg/contracts/erc721"
16+
"github.com/status-im/go-wallet-sdk/pkg/eventlog"
1217

1318
"github.com/stretchr/testify/require"
1419

@@ -25,6 +30,7 @@ import (
2530
walletCommon "github.com/status-im/status-go/services/wallet/common"
2631
"github.com/status-im/status-go/services/wallet/multistandardbalance"
2732
"github.com/status-im/status-go/services/wallet/thirdparty"
33+
"github.com/status-im/status-go/services/wallet/transferdetector"
2834

2935
"go.uber.org/mock/gomock"
3036
"go.uber.org/zap"
@@ -805,3 +811,128 @@ func TestControllerPeriodicalLoads(t *testing.T) {
805811

806812
controller.Stop()
807813
}
814+
815+
func TestControllerTransferDetectionEvents(t *testing.T) {
816+
mockCtrl := gomock.NewController(t)
817+
defer mockCtrl.Finish()
818+
819+
accountsProvider := mock_ownership.NewMockAccountsProvider(mockCtrl)
820+
fakeAddress := types.HexToAddress("0x123")
821+
accountsProvider.EXPECT().GetWalletAddresses().Return([]types.Address{fakeAddress}, nil).AnyTimes()
822+
823+
accountsPublisher := pubsub.NewPublisher()
824+
networksProvider := mock_ownership.NewMockNetworksProvider(mockCtrl)
825+
networksPublisher := pubsub.NewPublisher()
826+
827+
networksProvider.EXPECT().GetActiveNetworks().Return([]*params.Network{
828+
{ChainID: 1, IsActive: true},
829+
}, nil).AnyTimes()
830+
networksProvider.EXPECT().GetPublisher().Return(networksPublisher).AnyTimes()
831+
832+
walletDB, err := testutils.SetupTestMemorySQLDB(walletdatabase.DbInitializer{})
833+
require.NoError(t, err)
834+
835+
emptyCollectiblesContainer := &thirdparty.CollectibleOwnershipContainer{
836+
Items: []thirdparty.CollectibleIDBalance{},
837+
NextCursor: "",
838+
PreviousCursor: "",
839+
Provider: "mockProvider",
840+
}
841+
ownershipFetcher := mock_ownership.NewMockCollectibleOwnershipFetcher(mockCtrl)
842+
ownershipFetcher.EXPECT().FetchCollectibleOwnershipByOwner(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
843+
func(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, cursor string, limit int, providerID string) (*thirdparty.CollectibleOwnershipContainer, error) {
844+
return emptyCollectiblesContainer, nil
845+
}).AnyTimes()
846+
847+
multistandardBalancePublisher := pubsub.NewPublisher()
848+
transferDetectorPublisher := pubsub.NewPublisher()
849+
blockChainStateProvider := mock_ownership.NewMockBlockChainStateProvider(mockCtrl)
850+
var estimatedBlockCalls atomic.Int32
851+
blockChainStateProvider.EXPECT().GetEstimatedBlockTime(gomock.Any(), uint64(1), gomock.Any()).DoAndReturn(
852+
func(ctx context.Context, chainID uint64, blockNumber uint64) (time.Time, error) {
853+
estimatedBlockCalls.Add(1)
854+
return time.Now(), nil
855+
},
856+
).AnyTimes()
857+
858+
publisher := pubsub.NewPublisher()
859+
logger := zaptest.NewLogger(t).WithOptions(zap.AddCallerSkip(1))
860+
861+
controller := ownership.NewController(
862+
ownership.NewOwnershipDB(walletDB),
863+
accountsProvider,
864+
accountsPublisher,
865+
networksProvider,
866+
multistandardBalancePublisher,
867+
transferDetectorPublisher,
868+
blockChainStateProvider,
869+
ownershipFetcher,
870+
publisher,
871+
logger,
872+
)
873+
874+
params := ownership.PeriodicalLoaderParams{
875+
StartDelay: 1 * time.Second,
876+
LoadInterval: 10 * time.Second,
877+
LoaderParams: ownership.LoaderParams{
878+
LoadDelay: 0 * time.Second,
879+
FetchLimit: 50,
880+
},
881+
}
882+
controller.StartWithLoaderParams(params)
883+
defer controller.Stop()
884+
885+
// Ensure initial load completed so ownership timestamp exists.
886+
require.Eventually(t, func() bool {
887+
return controller.GetLoaderState(walletCommon.ChainID(1), common.Address(fakeAddress)) == ownership.LoaderStateIdle
888+
}, 2*time.Second, 50*time.Millisecond)
889+
890+
otherAddress := common.HexToAddress("0x456")
891+
msg := transferdetector.EventTransferDetectionFinished{
892+
ChainID: 1,
893+
Accounts: []common.Address{common.Address(fakeAddress)},
894+
FromBlock: 100,
895+
ToBlock: 200,
896+
Events: []eventlog.Event{
897+
{
898+
EventKey: eventlog.ERC721Transfer,
899+
Unpacked: erc721.Erc721Transfer{
900+
From: common.Address(fakeAddress),
901+
To: otherAddress,
902+
Raw: coretypes.Log{
903+
BlockNumber: 123,
904+
},
905+
},
906+
},
907+
{
908+
EventKey: eventlog.ERC1155TransferSingle,
909+
Unpacked: erc1155.Erc1155TransferSingle{
910+
From: common.Address(fakeAddress),
911+
To: otherAddress,
912+
Raw: coretypes.Log{
913+
BlockNumber: 124,
914+
},
915+
},
916+
},
917+
{
918+
EventKey: eventlog.ERC1155TransferBatch,
919+
Unpacked: erc1155.Erc1155TransferBatch{
920+
From: common.Address(fakeAddress),
921+
To: otherAddress,
922+
Raw: coretypes.Log{
923+
BlockNumber: 125,
924+
},
925+
},
926+
},
927+
{
928+
EventKey: eventlog.ERC721Transfer,
929+
Unpacked: struct{}{},
930+
},
931+
},
932+
}
933+
pubsub.Publish(transferDetectorPublisher, msg)
934+
935+
require.Eventually(t, func() bool {
936+
return estimatedBlockCalls.Load() >= 3
937+
}, 2*time.Second, 50*time.Millisecond)
938+
}

services/wallet/collectibles/service.go

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -212,24 +212,31 @@ func (s *Service) GetOwnedCollectibles(
212212
}, err
213213
}
214214

215-
func (s *Service) needsToFetch(chainID walletCommon.ChainID, address common.Address, fetchCriteria FetchCriteria) (bool, error) {
216-
mustFetch := false
215+
func (s *Service) shouldTriggerLoad(chainID walletCommon.ChainID, address common.Address, fetchCriteria FetchCriteria) (bool, error) {
217216
switch fetchCriteria.FetchType {
218-
case FetchTypeAlwaysFetch:
219-
mustFetch = true
220217
case FetchTypeNeverFetch:
221-
mustFetch = false
218+
return false, nil
219+
case FetchTypeAlwaysFetch:
220+
return true, nil
222221
case FetchTypeFetchIfNotCached, FetchTypeFetchIfCacheOld:
223222
timestamp, err := s.ownershipDB.GetOwnershipUpdateTimestamp(address, chainID)
224223
if err != nil {
225224
return false, err
226225
}
227-
if timestamp == ownership.InvalidTimestamp ||
228-
(fetchCriteria.FetchType == FetchTypeFetchIfCacheOld && timestamp+fetchCriteria.MaxCacheAgeSeconds < time.Now().Unix()) {
229-
mustFetch = true
226+
if timestamp != ownership.InvalidTimestamp &&
227+
!(fetchCriteria.FetchType == FetchTypeFetchIfCacheOld && timestamp+fetchCriteria.MaxCacheAgeSeconds < time.Now().Unix()) {
228+
return false, nil
229+
}
230+
// Skip duplicate load unless the loader is missing or errored
231+
if fetchCriteria.FetchType == FetchTypeFetchIfNotCached {
232+
state := s.ownershipController.GetLoaderState(chainID, address)
233+
if state != ownership.LoaderStateNotAvailable && state != ownership.LoaderStateError {
234+
return false, nil
235+
}
230236
}
237+
return true, nil
231238
}
232-
return mustFetch, nil
239+
return false, nil
233240
}
234241

235242
func (s *Service) fetchOwnedCollectiblesIfNeeded(ctx context.Context, chainIDs []walletCommon.ChainID, addresses []common.Address, fetchCriteria FetchCriteria) error {
@@ -241,21 +248,20 @@ func (s *Service) fetchOwnedCollectiblesIfNeeded(ctx context.Context, chainIDs [
241248
wgErr := atomic.NewError(nil)
242249
for _, address := range addresses {
243250
for _, chainID := range chainIDs {
244-
mustFetch, err := s.needsToFetch(chainID, address, fetchCriteria)
245-
if err != nil {
251+
if ok, err := s.shouldTriggerLoad(chainID, address, fetchCriteria); err != nil {
246252
return err
253+
} else if !ok {
254+
continue
247255
}
248-
if mustFetch {
249-
wg.Add(1)
250-
go func() {
251-
defer gocommon.LogOnPanic()
252-
defer wg.Done()
253-
err := s.ownershipController.TriggerLoad(ctx, chainID, address)
254-
if err != nil {
255-
wgErr.Store(err)
256-
}
257-
}()
258-
}
256+
257+
wg.Add(1)
258+
go func() {
259+
defer gocommon.LogOnPanic()
260+
defer wg.Done()
261+
if err := s.ownershipController.TriggerLoad(ctx, chainID, address); err != nil {
262+
wgErr.Store(err)
263+
}
264+
}()
259265
}
260266
}
261267

0 commit comments

Comments
 (0)