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 }