github.com/status-im/status-go@v1.1.0/services/wallet/activity/service_test.go (about)

     1  package activity
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"math/big"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/golang/mock/gomock"
    11  
    12  	eth "github.com/ethereum/go-ethereum/common"
    13  	"github.com/ethereum/go-ethereum/event"
    14  
    15  	"github.com/status-im/status-go/appdatabase"
    16  	"github.com/status-im/status-go/multiaccounts/accounts"
    17  	"github.com/status-im/status-go/rpc/chain"
    18  	mock_rpcclient "github.com/status-im/status-go/rpc/mock/client"
    19  	"github.com/status-im/status-go/services/wallet/bigint"
    20  	"github.com/status-im/status-go/services/wallet/common"
    21  	"github.com/status-im/status-go/services/wallet/thirdparty"
    22  	"github.com/status-im/status-go/services/wallet/token"
    23  	mock_token "github.com/status-im/status-go/services/wallet/token/mock/token"
    24  	"github.com/status-im/status-go/services/wallet/transfer"
    25  	"github.com/status-im/status-go/services/wallet/walletevent"
    26  	"github.com/status-im/status-go/t/helpers"
    27  	"github.com/status-im/status-go/transactions"
    28  	"github.com/status-im/status-go/walletdatabase"
    29  
    30  	"github.com/stretchr/testify/mock"
    31  	"github.com/stretchr/testify/require"
    32  )
    33  
    34  const shouldNotWaitTimeout = 19999 * time.Second
    35  
    36  // mockCollectiblesManager implements the collectibles.ManagerInterface
    37  type mockCollectiblesManager struct {
    38  	mock.Mock
    39  }
    40  
    41  func (m *mockCollectiblesManager) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID, asyncFetch bool) ([]thirdparty.FullCollectibleData, error) {
    42  	args := m.Called(uniqueIDs)
    43  	res := args.Get(0)
    44  	if res == nil {
    45  		return nil, args.Error(1)
    46  	}
    47  	return res.([]thirdparty.FullCollectibleData), args.Error(1)
    48  }
    49  
    50  func (m *mockCollectiblesManager) FetchCollectionSocialsAsync(contractID thirdparty.ContractID) error {
    51  	args := m.Called(contractID)
    52  	res := args.Get(0)
    53  	if res == nil {
    54  		return args.Error(1)
    55  	}
    56  	return nil
    57  }
    58  
    59  type testState struct {
    60  	service          *Service
    61  	eventFeed        *event.Feed
    62  	tokenMock        *mock_token.MockManagerInterface
    63  	collectiblesMock *mockCollectiblesManager
    64  	close            func()
    65  	pendingTracker   *transactions.PendingTxTracker
    66  	chainClient      *transactions.MockChainClient
    67  	rpcClient        *mock_rpcclient.MockClientInterface
    68  }
    69  
    70  func setupTestService(tb testing.TB) (state testState) {
    71  	db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{})
    72  	require.NoError(tb, err)
    73  
    74  	appDB, err := helpers.SetupTestMemorySQLDB(appdatabase.DbInitializer{})
    75  	require.NoError(tb, err)
    76  	accountsDB, err := accounts.NewDB(appDB)
    77  	require.NoError(tb, err)
    78  
    79  	state.eventFeed = new(event.Feed)
    80  	mockCtrl := gomock.NewController(tb)
    81  	state.tokenMock = mock_token.NewMockManagerInterface(mockCtrl)
    82  	state.collectiblesMock = &mockCollectiblesManager{}
    83  
    84  	state.chainClient = transactions.NewMockChainClient()
    85  	state.rpcClient = mock_rpcclient.NewMockClientInterface(mockCtrl)
    86  	state.rpcClient.EXPECT().AbstractEthClient(gomock.Any()).DoAndReturn(func(chainID common.ChainID) (chain.BatchCallClient, error) {
    87  		return state.chainClient.AbstractEthClient(chainID)
    88  	}).AnyTimes()
    89  
    90  	// Ensure we process pending transactions as needed, only once
    91  	pendingCheckInterval := time.Second
    92  	state.pendingTracker = transactions.NewPendingTxTracker(db, state.rpcClient, nil, state.eventFeed, pendingCheckInterval)
    93  
    94  	state.service = NewService(db, accountsDB, state.tokenMock, state.collectiblesMock, state.eventFeed, state.pendingTracker)
    95  	state.service.debounceDuration = 0
    96  	state.close = func() {
    97  		require.NoError(tb, state.pendingTracker.Stop())
    98  		require.NoError(tb, db.Close())
    99  		defer mockCtrl.Finish()
   100  	}
   101  
   102  	return state
   103  }
   104  
   105  type arg struct {
   106  	chainID         common.ChainID
   107  	tokenAddressStr string
   108  	tokenIDStr      string
   109  	tokenID         *big.Int
   110  	tokenAddress    *eth.Address
   111  }
   112  
   113  // insertStubTransfersWithCollectibles will insert nil if tokenIDStr is empty
   114  func insertStubTransfersWithCollectibles(t *testing.T, db *sql.DB, args []arg) (fromAddresses, toAddresses []eth.Address) {
   115  	trs, fromAddresses, toAddresses := transfer.GenerateTestTransfers(t, db, 0, len(args))
   116  	for i := range args {
   117  		trs[i].ChainID = args[i].chainID
   118  		if args[i].tokenIDStr == "" {
   119  			args[i].tokenID = nil
   120  		} else {
   121  			args[i].tokenID = new(big.Int)
   122  			args[i].tokenID.SetString(args[i].tokenIDStr, 0)
   123  		}
   124  		args[i].tokenAddress = new(eth.Address)
   125  		*args[i].tokenAddress = eth.HexToAddress(args[i].tokenAddressStr)
   126  		transfer.InsertTestTransferWithOptions(t, db, trs[i].To, &trs[i], &transfer.TestTransferOptions{
   127  			TokenAddress: *args[i].tokenAddress,
   128  			TokenID:      args[i].tokenID,
   129  		})
   130  	}
   131  	return fromAddresses, toAddresses
   132  }
   133  
   134  func TestService_UpdateCollectibleInfo(t *testing.T) {
   135  	state := setupTestService(t)
   136  	defer state.close()
   137  
   138  	args := []arg{
   139  		{5, "0xA2838FDA19EB6EED3F8B9EFF411D4CD7D2DE0313", "0x0D", nil, nil},
   140  		{5, "0xA2838FDA19EB6EED3F8B9EFF411D4CD7D2DE0313", "0x762AD3E4934E687F8701F24C7274E5209213FD6208FF952ACEB325D028866949", nil, nil},
   141  		{5, "0xA2838FDA19EB6EED3F8B9EFF411D4CD7D2DE0313", "0x762AD3E4934E687F8701F24C7274E5209213FD6208FF952ACEB325D028866949", nil, nil},
   142  		{5, "0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a", "", nil, nil},
   143  		{5, "0xA2838FDA19EB6EED3F8B9EFF411D4CD7D2DE0313", "0x0F", nil, nil},
   144  	}
   145  	fromAddresses, toAddresses := insertStubTransfersWithCollectibles(t, state.service.db, args)
   146  
   147  	ch := make(chan walletevent.Event)
   148  	sub := state.eventFeed.Subscribe(ch)
   149  
   150  	// Expect one call for the fungible token
   151  	state.tokenMock.EXPECT().LookupTokenIdentity(uint64(5), eth.HexToAddress("0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a"), false).Return(
   152  		&token.Token{
   153  			ChainID: 5,
   154  			Address: eth.HexToAddress("0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a"),
   155  			Symbol:  "STT",
   156  		},
   157  	).Times(1)
   158  	state.collectiblesMock.On("FetchAssetsByCollectibleUniqueID", []thirdparty.CollectibleUniqueID{
   159  		{
   160  			ContractID: thirdparty.ContractID{
   161  				ChainID: args[4].chainID,
   162  				Address: *args[4].tokenAddress},
   163  			TokenID: &bigint.BigInt{Int: args[4].tokenID},
   164  		}, {
   165  			ContractID: thirdparty.ContractID{
   166  				ChainID: args[1].chainID,
   167  				Address: *args[1].tokenAddress},
   168  			TokenID: &bigint.BigInt{Int: args[1].tokenID},
   169  		}, {
   170  			ContractID: thirdparty.ContractID{
   171  				ChainID: args[0].chainID,
   172  				Address: *args[0].tokenAddress},
   173  			TokenID: &bigint.BigInt{Int: args[0].tokenID},
   174  		},
   175  	}).Return([]thirdparty.FullCollectibleData{
   176  		{
   177  			CollectibleData: thirdparty.CollectibleData{
   178  				ID: thirdparty.CollectibleUniqueID{
   179  					ContractID: thirdparty.ContractID{
   180  						ChainID: args[4].chainID,
   181  						Address: *args[4].tokenAddress},
   182  					TokenID: &bigint.BigInt{Int: args[4].tokenID},
   183  				},
   184  				Name:     "Test 4",
   185  				ImageURL: "test://url/4"},
   186  			CollectionData: nil,
   187  		}, {
   188  			CollectibleData: thirdparty.CollectibleData{
   189  				ID: thirdparty.CollectibleUniqueID{
   190  					ContractID: thirdparty.ContractID{
   191  						ChainID: args[1].chainID,
   192  						Address: *args[1].tokenAddress},
   193  					TokenID: &bigint.BigInt{Int: args[1].tokenID},
   194  				},
   195  				Name:     "Test 1",
   196  				ImageURL: "test://url/1"},
   197  			CollectionData: nil,
   198  		},
   199  		{
   200  			CollectibleData: thirdparty.CollectibleData{
   201  				ID: thirdparty.CollectibleUniqueID{
   202  					ContractID: thirdparty.ContractID{
   203  						ChainID: args[0].chainID,
   204  						Address: *args[0].tokenAddress},
   205  					TokenID: &bigint.BigInt{Int: args[0].tokenID},
   206  				},
   207  				Name:     "Test 0",
   208  				ImageURL: "test://url/0"},
   209  			CollectionData: nil,
   210  		},
   211  	}, nil).Once()
   212  
   213  	state.service.FilterActivityAsync(0, append(fromAddresses, toAddresses...), allNetworksFilter(), Filter{}, 0, 10)
   214  
   215  	filterResponseCount := 0
   216  	var updates []EntryData
   217  
   218  	for i := 0; i < 2; i++ {
   219  		select {
   220  		case res := <-ch:
   221  			switch res.Type {
   222  			case EventActivityFilteringDone:
   223  				payload, err := walletevent.GetPayload[FilterResponse](res)
   224  				require.NoError(t, err)
   225  				require.Equal(t, ErrorCodeSuccess, payload.ErrorCode)
   226  				require.Equal(t, 5, len(payload.Activities))
   227  				filterResponseCount++
   228  			case EventActivityFilteringUpdate:
   229  				err := walletevent.ExtractPayload(res, &updates)
   230  				require.NoError(t, err)
   231  			}
   232  		case <-time.NewTimer(shouldNotWaitTimeout).C:
   233  			require.Fail(t, "timeout while waiting for event")
   234  		}
   235  	}
   236  
   237  	// FetchAssetsByCollectibleUniqueID will receive only unique ids, while number of entries can be bigger
   238  	require.Equal(t, 1, filterResponseCount)
   239  	require.Equal(t, 4, len(updates))
   240  	require.Equal(t, "Test 4", *updates[0].NftName)
   241  	require.Equal(t, "test://url/4", *updates[0].NftURL)
   242  	require.Equal(t, "Test 1", *updates[1].NftName)
   243  	require.Equal(t, "test://url/1", *updates[1].NftURL)
   244  	require.Equal(t, "Test 1", *updates[2].NftName)
   245  	require.Equal(t, "test://url/1", *updates[2].NftURL)
   246  	require.Equal(t, "Test 0", *updates[3].NftName)
   247  	require.Equal(t, "test://url/0", *updates[3].NftURL)
   248  
   249  	sub.Unsubscribe()
   250  }
   251  
   252  func TestService_UpdateCollectibleInfo_Error(t *testing.T) {
   253  	state := setupTestService(t)
   254  	defer state.close()
   255  
   256  	args := []arg{
   257  		{5, "0xA2838FDA19EB6EED3F8B9EFF411D4CD7D2DE0313", "0x762AD3E4934E687F8701F24C7274E5209213FD6208FF952ACEB325D028866949", nil, nil},
   258  		{5, "0xA2838FDA19EB6EED3F8B9EFF411D4CD7D2DE0313", "0x0D", nil, nil},
   259  	}
   260  
   261  	ch := make(chan walletevent.Event, 4)
   262  	sub := state.eventFeed.Subscribe(ch)
   263  
   264  	fromAddresses, toAddresses := insertStubTransfersWithCollectibles(t, state.service.db, args)
   265  
   266  	state.collectiblesMock.On("FetchAssetsByCollectibleUniqueID", mock.Anything).Return(nil, thirdparty.ErrChainIDNotSupported).Once()
   267  
   268  	state.service.FilterActivityAsync(0, append(fromAddresses, toAddresses...), allNetworksFilter(), Filter{}, 0, 5)
   269  
   270  	filterResponseCount := 0
   271  	updatesCount := 0
   272  
   273  	for i := 0; i < 2; i++ {
   274  		select {
   275  		case res := <-ch:
   276  			switch res.Type {
   277  			case EventActivityFilteringDone:
   278  				payload, err := walletevent.GetPayload[FilterResponse](res)
   279  				require.NoError(t, err)
   280  				require.Equal(t, ErrorCodeSuccess, payload.ErrorCode)
   281  				require.Equal(t, 2, len(payload.Activities))
   282  				filterResponseCount++
   283  			case EventActivityFilteringUpdate:
   284  				updatesCount++
   285  			}
   286  		case <-time.NewTimer(20 * time.Millisecond).C:
   287  			// We wait to ensure the EventActivityFilteringUpdate is never sent
   288  		}
   289  	}
   290  
   291  	require.Equal(t, 1, filterResponseCount)
   292  	require.Equal(t, 0, updatesCount)
   293  
   294  	sub.Unsubscribe()
   295  }
   296  
   297  func setupTransactions(t *testing.T, state testState, txCount int, testTxs []transactions.TestTxSummary) (allAddresses []eth.Address, pendings []transactions.PendingTransaction, ch chan walletevent.Event, cleanup func()) {
   298  	ch = make(chan walletevent.Event, 4)
   299  	sub := state.eventFeed.Subscribe(ch)
   300  
   301  	pendings = transactions.MockTestTransactions(t, state.chainClient, testTxs)
   302  	for _, p := range pendings {
   303  		allAddresses = append(allAddresses, p.From, p.To)
   304  	}
   305  
   306  	txs, fromTrs, toTrs := transfer.GenerateTestTransfers(t, state.service.db, len(pendings), txCount)
   307  	for i := range txs {
   308  		transfer.InsertTestTransfer(t, state.service.db, txs[i].To, &txs[i])
   309  	}
   310  
   311  	allAddresses = append(append(allAddresses, fromTrs...), toTrs...)
   312  
   313  	state.tokenMock.EXPECT().LookupTokenIdentity(gomock.Any(), gomock.Any(), gomock.Any()).Return(
   314  		&token.Token{
   315  			ChainID: 5,
   316  			Address: eth.Address{},
   317  			Symbol:  "ETH",
   318  		},
   319  	).AnyTimes()
   320  
   321  	state.tokenMock.EXPECT().LookupToken(gomock.Any(), gomock.Any()).Return(
   322  		&token.Token{
   323  			ChainID: 5,
   324  			Address: eth.Address{},
   325  			Symbol:  "ETH",
   326  		}, true,
   327  	).AnyTimes()
   328  
   329  	return allAddresses, pendings, ch, func() {
   330  		sub.Unsubscribe()
   331  	}
   332  }
   333  
   334  func getValidateSessionUpdateHasNewOnTopFn(t *testing.T) func(payload SessionUpdate) bool {
   335  	return func(payload SessionUpdate) bool {
   336  		require.NotNil(t, payload.HasNewOnTop)
   337  		require.True(t, *payload.HasNewOnTop)
   338  		return false
   339  	}
   340  }
   341  
   342  // validateSessionUpdateEvent expects will give up early if checkPayloadFn return true and not wait for expectCount
   343  func validateSessionUpdateEvent(t *testing.T, ch chan walletevent.Event, filterResponseCount *int, expectCount int, checkPayloadFn func(payload SessionUpdate) bool) (pendingTransactionUpdate, sessionUpdatesCount int) {
   344  	for sessionUpdatesCount < expectCount {
   345  		select {
   346  		case res := <-ch:
   347  			switch res.Type {
   348  			case transactions.EventPendingTransactionUpdate:
   349  				pendingTransactionUpdate++
   350  			case EventActivitySessionUpdated:
   351  				payload, err := walletevent.GetPayload[SessionUpdate](res)
   352  				require.NoError(t, err)
   353  
   354  				if checkPayloadFn != nil && checkPayloadFn(*payload) {
   355  					return
   356  				}
   357  
   358  				sessionUpdatesCount++
   359  			case EventActivityFilteringDone:
   360  				(*filterResponseCount)++
   361  			}
   362  		case <-time.NewTimer(shouldNotWaitTimeout).C:
   363  			require.Fail(t, "timeout while waiting for EventActivitySessionUpdated")
   364  		}
   365  	}
   366  	return
   367  }
   368  
   369  type extraExpect struct {
   370  	offset    *int
   371  	errorCode *ErrorCode
   372  }
   373  
   374  func getOptionalExpectations(e *extraExpect) (expectOffset int, expectErrorCode ErrorCode) {
   375  	expectOffset = 0
   376  	expectErrorCode = ErrorCodeSuccess
   377  
   378  	if e != nil {
   379  		if e.offset != nil {
   380  			expectOffset = *e.offset
   381  		}
   382  		if e.errorCode != nil {
   383  			expectErrorCode = *e.errorCode
   384  		}
   385  	}
   386  	return
   387  }
   388  
   389  func validateFilteringDone(t *testing.T, ch chan walletevent.Event, resCount int, checkPayloadFn func(payload FilterResponse), extra *extraExpect) (filterResponseCount int) {
   390  	for filterResponseCount < 1 {
   391  		select {
   392  		case res := <-ch:
   393  			switch res.Type {
   394  			case EventActivityFilteringDone:
   395  				payload, err := walletevent.GetPayload[FilterResponse](res)
   396  				require.NoError(t, err)
   397  
   398  				expectOffset, expectErrorCode := getOptionalExpectations(extra)
   399  
   400  				require.Equal(t, expectErrorCode, payload.ErrorCode)
   401  				require.Equal(t, resCount, len(payload.Activities))
   402  
   403  				require.Equal(t, expectOffset, payload.Offset)
   404  				filterResponseCount++
   405  
   406  				if checkPayloadFn != nil {
   407  					checkPayloadFn(*payload)
   408  				}
   409  			}
   410  		case <-time.NewTimer(shouldNotWaitTimeout).C:
   411  			require.Fail(t, "timeout while waiting for EventActivityFilteringDone")
   412  		}
   413  	}
   414  	return
   415  }
   416  
   417  func TestService_IncrementalUpdateOnTop(t *testing.T) {
   418  	state := setupTestService(t)
   419  	defer state.close()
   420  
   421  	transactionCount := 2
   422  	allAddresses, pendings, ch, cleanup := setupTransactions(t, state, transactionCount, []transactions.TestTxSummary{{DontConfirm: true, Timestamp: transactionCount + 1}})
   423  	defer cleanup()
   424  
   425  	sessionID := state.service.StartFilterSession(allAddresses, allNetworksFilter(), Filter{}, 5)
   426  	require.Greater(t, sessionID, SessionID(0))
   427  	defer state.service.StopFilterSession(sessionID)
   428  
   429  	filterResponseCount := validateFilteringDone(t, ch, 2, nil, nil)
   430  
   431  	exp := pendings[0]
   432  	err := state.pendingTracker.StoreAndTrackPendingTx(&exp)
   433  	require.NoError(t, err)
   434  
   435  	vFn := getValidateSessionUpdateHasNewOnTopFn(t)
   436  	pendingTransactionUpdate, sessionUpdatesCount := validateSessionUpdateEvent(t, ch, &filterResponseCount, 1, vFn)
   437  
   438  	err = state.service.ResetFilterSession(sessionID, 5)
   439  	require.NoError(t, err)
   440  
   441  	// Validate the reset data
   442  	eventActivityDoneCount := validateFilteringDone(t, ch, 3, func(payload FilterResponse) {
   443  		require.True(t, payload.Activities[0].isNew)
   444  		require.False(t, payload.Activities[1].isNew)
   445  		require.False(t, payload.Activities[2].isNew)
   446  
   447  		// Check the new transaction data
   448  		newTx := payload.Activities[0]
   449  		require.Equal(t, PendingTransactionPT, newTx.payloadType)
   450  		// We don't keep type in the DB
   451  		require.Equal(t, (*int)(nil), newTx.transferType)
   452  		require.Equal(t, SendAT, newTx.activityType)
   453  		require.Equal(t, PendingAS, newTx.activityStatus)
   454  		require.Equal(t, exp.ChainID, newTx.transaction.ChainID)
   455  		require.Equal(t, exp.ChainID, *newTx.chainIDOut)
   456  		require.Equal(t, (*common.ChainID)(nil), newTx.chainIDIn)
   457  		require.Equal(t, exp.Hash, newTx.transaction.Hash)
   458  		// Pending doesn't have address as part of identity
   459  		require.Equal(t, eth.Address{}, newTx.transaction.Address)
   460  		require.Equal(t, exp.From, *newTx.sender)
   461  		require.Equal(t, exp.To, *newTx.recipient)
   462  		require.Equal(t, 0, exp.Value.Int.Cmp((*big.Int)(newTx.amountOut)))
   463  		require.Equal(t, exp.Timestamp, uint64(newTx.timestamp))
   464  		require.Equal(t, exp.Symbol, *newTx.symbolOut)
   465  		require.Equal(t, (*string)(nil), newTx.symbolIn)
   466  		require.Equal(t, &Token{
   467  			TokenType: Native,
   468  			ChainID:   5,
   469  		}, newTx.tokenOut)
   470  		require.Equal(t, (*Token)(nil), newTx.tokenIn)
   471  		require.Equal(t, (*eth.Address)(nil), newTx.contractAddress)
   472  
   473  		// Check the order of the following transaction data
   474  		require.Equal(t, SimpleTransactionPT, payload.Activities[1].payloadType)
   475  		require.Equal(t, int64(transactionCount), payload.Activities[1].timestamp)
   476  		require.Equal(t, SimpleTransactionPT, payload.Activities[2].payloadType)
   477  		require.Equal(t, int64(transactionCount-1), payload.Activities[2].timestamp)
   478  	}, nil)
   479  
   480  	require.Equal(t, 1, pendingTransactionUpdate)
   481  	require.Equal(t, 1, filterResponseCount)
   482  	require.Equal(t, 1, sessionUpdatesCount)
   483  	require.Equal(t, 1, eventActivityDoneCount)
   484  }
   485  
   486  func TestService_IncrementalUpdateMixed(t *testing.T) {
   487  	state := setupTestService(t)
   488  	defer state.close()
   489  
   490  	transactionCount := 5
   491  	allAddresses, pendings, ch, cleanup := setupTransactions(t, state, transactionCount,
   492  		[]transactions.TestTxSummary{
   493  			{DontConfirm: true, Timestamp: 2},
   494  			{DontConfirm: true, Timestamp: 4},
   495  			{DontConfirm: true, Timestamp: 6},
   496  		},
   497  	)
   498  	defer cleanup()
   499  
   500  	sessionID := state.service.StartFilterSession(allAddresses, allNetworksFilter(), Filter{}, 5)
   501  	require.Greater(t, sessionID, SessionID(0))
   502  	defer state.service.StopFilterSession(sessionID)
   503  
   504  	filterResponseCount := validateFilteringDone(t, ch, 5, nil, nil)
   505  
   506  	for i := range pendings {
   507  		err := state.pendingTracker.StoreAndTrackPendingTx(&pendings[i])
   508  		require.NoError(t, err)
   509  	}
   510  
   511  	pendingTransactionUpdate, sessionUpdatesCount := validateSessionUpdateEvent(t, ch, &filterResponseCount, 2, func(payload SessionUpdate) bool {
   512  		require.Nil(t, payload.HasNewOnTop)
   513  		require.NotEmpty(t, payload.New)
   514  		for _, update := range payload.New {
   515  			require.True(t, update.Entry.isNew)
   516  			foundIdx := -1
   517  			for i, pTx := range pendings {
   518  				if pTx.Hash == update.Entry.transaction.Hash && pTx.ChainID == update.Entry.transaction.ChainID {
   519  					foundIdx = i
   520  					break
   521  				}
   522  			}
   523  			require.Greater(t, foundIdx, -1, "the updated transaction should be found in the pending list")
   524  			pendings = append(pendings[:foundIdx], pendings[foundIdx+1:]...)
   525  		}
   526  		return len(pendings) == 1
   527  	})
   528  
   529  	// Validate that the last one (oldest) is out of the window
   530  	require.Equal(t, 1, len(pendings))
   531  	require.Equal(t, uint64(2), pendings[0].Timestamp)
   532  
   533  	require.Equal(t, 3, pendingTransactionUpdate)
   534  	require.LessOrEqual(t, sessionUpdatesCount, 3)
   535  	require.Equal(t, 1, filterResponseCount)
   536  
   537  }
   538  
   539  func TestService_IncrementalUpdateFetchWindow(t *testing.T) {
   540  	state := setupTestService(t)
   541  	defer state.close()
   542  
   543  	transactionCount := 5
   544  	allAddresses, pendings, ch, cleanup := setupTransactions(t, state, transactionCount, []transactions.TestTxSummary{{DontConfirm: true, Timestamp: transactionCount + 1}})
   545  	defer cleanup()
   546  
   547  	sessionID := state.service.StartFilterSession(allAddresses, allNetworksFilter(), Filter{}, 2)
   548  	require.Greater(t, sessionID, SessionID(0))
   549  	defer state.service.StopFilterSession(sessionID)
   550  
   551  	filterResponseCount := validateFilteringDone(t, ch, 2, nil, nil)
   552  
   553  	exp := pendings[0]
   554  	err := state.pendingTracker.StoreAndTrackPendingTx(&exp)
   555  	require.NoError(t, err)
   556  
   557  	vFn := getValidateSessionUpdateHasNewOnTopFn(t)
   558  	pendingTransactionUpdate, sessionUpdatesCount := validateSessionUpdateEvent(t, ch, &filterResponseCount, 1, vFn)
   559  
   560  	err = state.service.ResetFilterSession(sessionID, 2)
   561  	require.NoError(t, err)
   562  
   563  	// Validate the reset data
   564  	eventActivityDoneCount := validateFilteringDone(t, ch, 2, func(payload FilterResponse) {
   565  		require.True(t, payload.Activities[0].isNew)
   566  		require.Equal(t, int64(transactionCount+1), payload.Activities[0].timestamp)
   567  		require.False(t, payload.Activities[1].isNew)
   568  		require.Equal(t, int64(transactionCount), payload.Activities[1].timestamp)
   569  	}, nil)
   570  
   571  	require.Equal(t, 1, pendingTransactionUpdate)
   572  	require.Equal(t, 1, filterResponseCount)
   573  	require.Equal(t, 1, sessionUpdatesCount)
   574  	require.Equal(t, 1, eventActivityDoneCount)
   575  
   576  	err = state.service.GetMoreForFilterSession(sessionID, 2)
   577  	require.NoError(t, err)
   578  
   579  	eventActivityDoneCount = validateFilteringDone(t, ch, 2, func(payload FilterResponse) {
   580  		require.False(t, payload.Activities[0].isNew)
   581  		require.Equal(t, int64(transactionCount-1), payload.Activities[0].timestamp)
   582  		require.False(t, payload.Activities[1].isNew)
   583  		require.Equal(t, int64(transactionCount-2), payload.Activities[1].timestamp)
   584  	}, common.NewAndSet(extraExpect{common.NewAndSet(2), nil}))
   585  	require.Equal(t, 1, eventActivityDoneCount)
   586  }
   587  
   588  func TestService_IncrementalUpdateFetchWindowNoReset(t *testing.T) {
   589  	state := setupTestService(t)
   590  	defer state.close()
   591  
   592  	transactionCount := 5
   593  	allAddresses, pendings, ch, cleanup := setupTransactions(t, state, transactionCount, []transactions.TestTxSummary{{DontConfirm: true, Timestamp: transactionCount + 1}})
   594  	defer cleanup()
   595  
   596  	sessionID := state.service.StartFilterSession(allAddresses, allNetworksFilter(), Filter{}, 2)
   597  	require.Greater(t, sessionID, SessionID(0))
   598  	defer state.service.StopFilterSession(sessionID)
   599  
   600  	filterResponseCount := validateFilteringDone(t, ch, 2, func(payload FilterResponse) {
   601  		require.Equal(t, int64(transactionCount), payload.Activities[0].timestamp)
   602  		require.Equal(t, int64(transactionCount-1), payload.Activities[1].timestamp)
   603  	}, nil)
   604  
   605  	exp := pendings[0]
   606  	err := state.pendingTracker.StoreAndTrackPendingTx(&exp)
   607  	require.NoError(t, err)
   608  
   609  	vFn := getValidateSessionUpdateHasNewOnTopFn(t)
   610  	pendingTransactionUpdate, sessionUpdatesCount := validateSessionUpdateEvent(t, ch, &filterResponseCount, 1, vFn)
   611  	require.Equal(t, 1, pendingTransactionUpdate)
   612  	require.Equal(t, 1, filterResponseCount)
   613  	require.Equal(t, 1, sessionUpdatesCount)
   614  
   615  	err = state.service.GetMoreForFilterSession(sessionID, 2)
   616  	require.NoError(t, err)
   617  
   618  	// Validate that client continue loading the next window without being affected by the internal state of new
   619  	eventActivityDoneCount := validateFilteringDone(t, ch, 2, func(payload FilterResponse) {
   620  		require.False(t, payload.Activities[0].isNew)
   621  		require.Equal(t, int64(transactionCount-2), payload.Activities[0].timestamp)
   622  		require.False(t, payload.Activities[1].isNew)
   623  		require.Equal(t, int64(transactionCount-3), payload.Activities[1].timestamp)
   624  	}, common.NewAndSet(extraExpect{common.NewAndSet(2), nil}))
   625  	require.Equal(t, 1, eventActivityDoneCount)
   626  }
   627  
   628  // Simulate and validate a multi-step user flow that was also a regression in the original implementation
   629  func TestService_FilteredIncrementalUpdateResetAndClear(t *testing.T) {
   630  	state := setupTestService(t)
   631  	defer state.close()
   632  
   633  	transactionCount := 5
   634  	allAddresses, pendings, ch, cleanup := setupTransactions(t, state, transactionCount, []transactions.TestTxSummary{{DontConfirm: true, Timestamp: transactionCount + 1}})
   635  	defer cleanup()
   636  
   637  	// Generate new transaction for step 5
   638  	newOffset := transactionCount + 2
   639  	newTxs, newFromTrs, newToTrs := transfer.GenerateTestTransfers(t, state.service.db, newOffset, 1)
   640  	allAddresses = append(append(allAddresses, newFromTrs...), newToTrs...)
   641  
   642  	// 1. User visualizes transactions for the first time
   643  	sessionID := state.service.StartFilterSession(allAddresses, allNetworksFilter(), Filter{}, 4)
   644  	require.Greater(t, sessionID, SessionID(0))
   645  	defer state.service.StopFilterSession(sessionID)
   646  
   647  	validateFilteringDone(t, ch, 4, nil, nil)
   648  
   649  	// 2. User applies a filter for pending transactions
   650  	err := state.service.UpdateFilterForSession(sessionID, Filter{Statuses: []Status{PendingAS}}, 4)
   651  	require.NoError(t, err)
   652  
   653  	filterResponseCount := validateFilteringDone(t, ch, 0, nil, nil)
   654  
   655  	// 3. A pending transaction is added
   656  	exp := pendings[0]
   657  	err = state.pendingTracker.StoreAndTrackPendingTx(&exp)
   658  	require.NoError(t, err)
   659  
   660  	vFn := getValidateSessionUpdateHasNewOnTopFn(t)
   661  	pendingTransactionUpdate, sessionUpdatesCount := validateSessionUpdateEvent(t, ch, &filterResponseCount, 1, vFn)
   662  
   663  	// 4. User resets the view and the new pending transaction has the new flag
   664  	err = state.service.ResetFilterSession(sessionID, 2)
   665  	require.NoError(t, err)
   666  
   667  	// Validate the reset data
   668  	eventActivityDoneCount := validateFilteringDone(t, ch, 1, func(payload FilterResponse) {
   669  		require.True(t, payload.Activities[0].isNew)
   670  		require.Equal(t, int64(transactionCount+1), payload.Activities[0].timestamp)
   671  	}, nil)
   672  
   673  	require.Equal(t, 1, pendingTransactionUpdate)
   674  	require.Equal(t, 1, filterResponseCount)
   675  	require.Equal(t, 1, sessionUpdatesCount)
   676  	require.Equal(t, 1, eventActivityDoneCount)
   677  
   678  	// 5. A new transaction is downloaded
   679  	transfer.InsertTestTransfer(t, state.service.db, newTxs[0].To, &newTxs[0])
   680  
   681  	// 6. User clears the filter and only the new transaction should have the new flag
   682  	err = state.service.UpdateFilterForSession(sessionID, Filter{}, 4)
   683  	require.NoError(t, err)
   684  
   685  	eventActivityDoneCount = validateFilteringDone(t, ch, 4, func(payload FilterResponse) {
   686  		require.True(t, payload.Activities[0].isNew)
   687  		require.Equal(t, int64(newOffset), payload.Activities[0].timestamp)
   688  		require.False(t, payload.Activities[1].isNew)
   689  		require.Equal(t, int64(newOffset-1), payload.Activities[1].timestamp)
   690  		require.False(t, payload.Activities[2].isNew)
   691  		require.Equal(t, int64(newOffset-2), payload.Activities[2].timestamp)
   692  		require.False(t, payload.Activities[3].isNew)
   693  		require.Equal(t, int64(newOffset-3), payload.Activities[3].timestamp)
   694  	}, nil)
   695  	require.Equal(t, 1, eventActivityDoneCount)
   696  }