github.com/status-im/status-go@v1.1.0/transactions/pendingtxtracker_test.go (about) 1 package transactions 2 3 import ( 4 "context" 5 "database/sql" 6 "encoding/json" 7 "math/big" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/golang/mock/gomock" 13 "github.com/stretchr/testify/mock" 14 "github.com/stretchr/testify/require" 15 16 eth "github.com/ethereum/go-ethereum/common" 17 "github.com/ethereum/go-ethereum/core/types" 18 "github.com/ethereum/go-ethereum/event" 19 "github.com/ethereum/go-ethereum/rpc" 20 "github.com/status-im/status-go/rpc/chain" 21 mock_rpcclient "github.com/status-im/status-go/rpc/mock/client" 22 23 "github.com/status-im/status-go/services/wallet/common" 24 "github.com/status-im/status-go/services/wallet/walletevent" 25 "github.com/status-im/status-go/t/helpers" 26 "github.com/status-im/status-go/walletdatabase" 27 ) 28 29 // setupTestTransactionDB will use the default pending check interval if checkInterval is nil 30 func setupTestTransactionDB(t *testing.T, checkInterval *time.Duration) (*PendingTxTracker, func(), *MockChainClient, *event.Feed) { 31 db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{}) 32 require.NoError(t, err) 33 34 chainClient := NewMockChainClient() 35 ctrl := gomock.NewController(t) 36 defer ctrl.Finish() 37 eventFeed := &event.Feed{} 38 pendingCheckInterval := PendingCheckInterval 39 if checkInterval != nil { 40 pendingCheckInterval = *checkInterval 41 } 42 rpcClient := mock_rpcclient.NewMockClientInterface(ctrl) 43 rpcClient.EXPECT().EthClient(common.EthereumMainnet).Return(chainClient, nil).AnyTimes() 44 45 // Delegate the call to the fake implementation 46 rpcClient.EXPECT().AbstractEthClient(gomock.Any()).DoAndReturn(func(chainID common.ChainID) (chain.BatchCallClient, error) { 47 return chainClient.AbstractEthClient(chainID) 48 }).AnyTimes() 49 return NewPendingTxTracker(db, rpcClient, nil, eventFeed, pendingCheckInterval), func() { 50 require.NoError(t, db.Close()) 51 }, chainClient, eventFeed 52 } 53 54 func waitForTaskToStop(pt *PendingTxTracker) { 55 for pt.taskRunner.IsRunning() { 56 time.Sleep(1 * time.Microsecond) 57 } 58 } 59 60 func TestPendingTxTracker_ValidateConfirmedWithSuccessStatus(t *testing.T) { 61 m, stop, chainClient, eventFeed := setupTestTransactionDB(t, nil) 62 defer stop() 63 64 txs := MockTestTransactions(t, chainClient, []TestTxSummary{{}}) 65 66 eventChan := make(chan walletevent.Event, 3) 67 sub := eventFeed.Subscribe(eventChan) 68 69 err := m.StoreAndTrackPendingTx(&txs[0]) 70 require.NoError(t, err) 71 72 for i := 0; i < 3; i++ { 73 select { 74 case we := <-eventChan: 75 if i == 0 || i == 1 { 76 // Check add and delete 77 require.Equal(t, EventPendingTransactionUpdate, we.Type) 78 } else { 79 require.Equal(t, EventPendingTransactionStatusChanged, we.Type) 80 var p StatusChangedPayload 81 err = json.Unmarshal([]byte(we.Message), &p) 82 require.NoError(t, err) 83 require.Equal(t, txs[0].Hash, p.Hash) 84 require.Equal(t, Success, p.Status) 85 } 86 case <-time.After(1 * time.Second): 87 t.Fatal("timeout waiting for event") 88 } 89 } 90 91 // Wait for the answer to be processed 92 err = m.Stop() 93 require.NoError(t, err) 94 95 waitForTaskToStop(m) 96 97 res, err := m.GetAllPending() 98 require.NoError(t, err) 99 require.Equal(t, 0, len(res)) 100 101 sub.Unsubscribe() 102 } 103 104 func TestPendingTxTracker_ValidateConfirmedWithFailedStatus(t *testing.T) { 105 m, stop, chainClient, eventFeed := setupTestTransactionDB(t, nil) 106 defer stop() 107 108 txs := MockTestTransactions(t, chainClient, []TestTxSummary{{failStatus: true}}) 109 110 eventChan := make(chan walletevent.Event, 3) 111 sub := eventFeed.Subscribe(eventChan) 112 113 err := m.StoreAndTrackPendingTx(&txs[0]) 114 require.NoError(t, err) 115 116 for i := 0; i < 3; i++ { 117 select { 118 case we := <-eventChan: 119 if i == 0 || i == 1 { 120 // Check add and delete 121 require.Equal(t, EventPendingTransactionUpdate, we.Type) 122 } else { 123 require.Equal(t, EventPendingTransactionStatusChanged, we.Type) 124 var p StatusChangedPayload 125 err = json.Unmarshal([]byte(we.Message), &p) 126 require.NoError(t, err) 127 require.Equal(t, txs[0].Hash, p.Hash) 128 require.Equal(t, Failed, p.Status) 129 } 130 case <-time.After(1 * time.Second): 131 t.Fatal("timeout waiting for event") 132 } 133 } 134 135 // Wait for the answer to be processed 136 err = m.Stop() 137 require.NoError(t, err) 138 139 waitForTaskToStop(m) 140 141 res, err := m.GetAllPending() 142 require.NoError(t, err) 143 require.Equal(t, 0, len(res)) 144 145 sub.Unsubscribe() 146 } 147 148 func TestPendingTxTracker_InterruptWatching(t *testing.T) { 149 m, stop, chainClient, eventFeed := setupTestTransactionDB(t, nil) 150 defer stop() 151 152 txs := GenerateTestPendingTransactions(0, 2) 153 154 // Mock the first call to getTransactionByHash 155 chainClient.SetAvailableClients([]common.ChainID{txs[0].ChainID}) 156 cl := chainClient.Clients[txs[0].ChainID] 157 cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool { 158 return (len(b) == 2 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[0].Hash && b[1].Method == GetTransactionReceiptRPCName && b[1].Args[0] == txs[1].Hash) 159 })).Return(nil).Once().Run(func(args mock.Arguments) { 160 elems := args.Get(1).([]rpc.BatchElem) 161 162 // Simulate still pending due to "null" return from eth_getTransactionReceipt 163 elems[0].Result.(*nullableReceipt).Receipt = nil 164 165 // Simulate parsing of eth_getTransactionReceipt response 166 elems[1].Result.(*nullableReceipt).Receipt = &types.Receipt{ 167 BlockNumber: new(big.Int).SetUint64(1), 168 Status: 1, 169 } 170 }) 171 172 eventChan := make(chan walletevent.Event, 2) 173 sub := eventFeed.Subscribe(eventChan) 174 175 for i := range txs { 176 err := m.addPending(&txs[i]) 177 require.NoError(t, err) 178 } 179 180 // Check add 181 for i := 0; i < 2; i++ { 182 select { 183 case we := <-eventChan: 184 require.Equal(t, EventPendingTransactionUpdate, we.Type) 185 case <-time.After(1 * time.Second): 186 t.Fatal("timeout waiting for event") 187 } 188 } 189 190 err := m.Start() 191 require.NoError(t, err) 192 193 for i := 0; i < 2; i++ { 194 select { 195 case we := <-eventChan: 196 if i == 0 { 197 require.Equal(t, EventPendingTransactionUpdate, we.Type) 198 } else { 199 require.Equal(t, EventPendingTransactionStatusChanged, we.Type) 200 var p StatusChangedPayload 201 err := json.Unmarshal([]byte(we.Message), &p) 202 require.NoError(t, err) 203 require.Equal(t, txs[1].Hash, p.Hash) 204 require.Equal(t, txs[1].ChainID, p.ChainID) 205 require.Equal(t, Success, p.Status) 206 } 207 case <-time.After(1 * time.Second): 208 t.Fatal("timeout waiting for event") 209 } 210 } 211 212 // Stop the next timed call 213 err = m.Stop() 214 require.NoError(t, err) 215 216 waitForTaskToStop(m) 217 218 res, err := m.GetAllPending() 219 require.NoError(t, err) 220 require.Equal(t, 1, len(res), "should have only one pending tx") 221 222 // Restart the tracker to process leftovers 223 // 224 cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool { 225 return (len(b) == 1 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[0].Hash) 226 })).Return(nil).Once().Run(func(args mock.Arguments) { 227 elems := args.Get(1).([]rpc.BatchElem) 228 // Simulate parsing of eth_getTransactionReceipt response 229 elems[0].Result.(*nullableReceipt).Receipt = &types.Receipt{ 230 BlockNumber: new(big.Int).SetUint64(1), 231 Status: 1, 232 } 233 }) 234 235 err = m.Start() 236 require.NoError(t, err) 237 238 for i := 0; i < 2; i++ { 239 select { 240 case we := <-eventChan: 241 if i == 0 { 242 require.Equal(t, EventPendingTransactionUpdate, we.Type) 243 } else { 244 require.Equal(t, EventPendingTransactionStatusChanged, we.Type) 245 var p StatusChangedPayload 246 err := json.Unmarshal([]byte(we.Message), &p) 247 require.NoError(t, err) 248 require.Equal(t, txs[0].ChainID, p.ChainID) 249 require.Equal(t, txs[0].Hash, p.Hash) 250 require.Equal(t, Success, p.Status) 251 } 252 case <-time.After(1 * time.Second): 253 t.Fatal("timeout waiting for event") 254 } 255 } 256 257 err = m.Stop() 258 require.NoError(t, err) 259 260 waitForTaskToStop(m) 261 262 res, err = m.GetAllPending() 263 require.NoError(t, err) 264 require.Equal(t, 0, len(res)) 265 266 sub.Unsubscribe() 267 } 268 269 func TestPendingTxTracker_MultipleClients(t *testing.T) { 270 m, stop, chainClient, eventFeed := setupTestTransactionDB(t, nil) 271 defer stop() 272 273 txs := GenerateTestPendingTransactions(0, 2) 274 txs[1].ChainID++ 275 276 // Mock the both clients to be available 277 chainClient.SetAvailableClients([]common.ChainID{txs[0].ChainID, txs[1].ChainID}) 278 cl := chainClient.Clients[txs[0].ChainID] 279 cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool { 280 return (len(b) == 1 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[0].Hash) 281 })).Return(nil).Once().Run(func(args mock.Arguments) { 282 elems := args.Get(1).([]rpc.BatchElem) 283 // Simulate parsing of eth_getTransactionReceipt response 284 elems[0].Result.(*nullableReceipt).Receipt = &types.Receipt{ 285 BlockNumber: new(big.Int).SetUint64(1), 286 Status: 1, 287 } 288 }) 289 cl = chainClient.Clients[txs[1].ChainID] 290 cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool { 291 return (len(b) == 1 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[1].Hash) 292 })).Return(nil).Once().Run(func(args mock.Arguments) { 293 elems := args.Get(1).([]rpc.BatchElem) 294 // Simulate parsing of eth_getTransactionReceipt response 295 elems[0].Result.(*nullableReceipt).Receipt = &types.Receipt{ 296 BlockNumber: new(big.Int).SetUint64(1), 297 Status: 1, 298 } 299 }) 300 301 eventChan := make(chan walletevent.Event, 6) 302 sub := eventFeed.Subscribe(eventChan) 303 304 for i := range txs { 305 err := m.TrackPendingTransaction(txs[i].ChainID, txs[i].Hash, txs[i].From, txs[i].To, txs[i].Type, AutoDelete, "") 306 require.NoError(t, err) 307 } 308 309 err := m.Start() 310 require.NoError(t, err) 311 312 storeEventCount := 0 313 statusEventCount := 0 314 315 validateStatusChange := func(we *walletevent.Event) { 316 if we.Type == EventPendingTransactionUpdate { 317 storeEventCount++ 318 } else if we.Type == EventPendingTransactionStatusChanged { 319 statusEventCount++ 320 require.Equal(t, EventPendingTransactionStatusChanged, we.Type) 321 var p StatusChangedPayload 322 err := json.Unmarshal([]byte(we.Message), &p) 323 require.NoError(t, err) 324 require.Equal(t, Success, p.Status) 325 } 326 } 327 328 for i := 0; i < 2; i++ { 329 for j := 0; j < 3; j++ { 330 select { 331 case we := <-eventChan: 332 validateStatusChange(&we) 333 case <-time.After(1 * time.Second): 334 t.Fatal("timeout waiting for event", i, j, storeEventCount, statusEventCount) 335 } 336 } 337 } 338 339 require.Equal(t, 4, storeEventCount) 340 require.Equal(t, 2, statusEventCount) 341 342 err = m.Stop() 343 require.NoError(t, err) 344 345 waitForTaskToStop(m) 346 347 res, err := m.GetAllPending() 348 require.NoError(t, err) 349 require.Equal(t, 0, len(res)) 350 351 sub.Unsubscribe() 352 } 353 354 func TestPendingTxTracker_Watch(t *testing.T) { 355 m, stop, chainClient, eventFeed := setupTestTransactionDB(t, nil) 356 defer stop() 357 358 txs := GenerateTestPendingTransactions(0, 2) 359 // Make the second already confirmed 360 *txs[0].Status = Success 361 362 // Mock the first call to getTransactionByHash 363 chainClient.SetAvailableClients([]common.ChainID{txs[0].ChainID}) 364 cl := chainClient.Clients[txs[0].ChainID] 365 cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool { 366 return len(b) == 1 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[1].Hash 367 })).Return(nil).Once().Run(func(args mock.Arguments) { 368 elems := args.Get(1).([]rpc.BatchElem) 369 // Simulate parsing of eth_getTransactionReceipt response 370 elems[0].Result.(*nullableReceipt).Receipt = &types.Receipt{ 371 BlockNumber: new(big.Int).SetUint64(1), 372 Status: 1, 373 } 374 }) 375 376 eventChan := make(chan walletevent.Event, 3) 377 sub := eventFeed.Subscribe(eventChan) 378 379 // Track the first transaction 380 err := m.TrackPendingTransaction(txs[1].ChainID, txs[1].Hash, txs[1].From, txs[1].To, txs[1].Type, Keep, "") 381 require.NoError(t, err) 382 383 // Store the confirmed already 384 err = m.StoreAndTrackPendingTx(&txs[0]) 385 require.NoError(t, err) 386 387 storeEventCount := 0 388 statusEventCount := 0 389 for j := 0; j < 3; j++ { 390 select { 391 case we := <-eventChan: 392 if EventPendingTransactionUpdate == we.Type { 393 storeEventCount++ 394 } else if EventPendingTransactionStatusChanged == we.Type { 395 statusEventCount++ 396 var p StatusChangedPayload 397 err := json.Unmarshal([]byte(we.Message), &p) 398 require.NoError(t, err) 399 require.Equal(t, txs[1].ChainID, p.ChainID) 400 require.Equal(t, txs[1].Hash, p.Hash) 401 require.Equal(t, Success, p.Status) 402 } 403 case <-time.After(1 * time.Second): 404 t.Fatal("timeout waiting for the status update event") 405 } 406 } 407 require.Equal(t, 2, storeEventCount) 408 require.Equal(t, 1, statusEventCount) 409 410 // Stop the next timed call 411 err = m.Stop() 412 require.NoError(t, err) 413 414 waitForTaskToStop(m) 415 416 res, err := m.GetAllPending() 417 require.NoError(t, err) 418 require.Equal(t, 0, len(res), "should have no pending tx") 419 420 status, err := m.Watch(context.Background(), txs[1].ChainID, txs[1].Hash) 421 require.NoError(t, err) 422 require.NotEqual(t, Pending, status) 423 424 err = m.Delete(context.Background(), txs[1].ChainID, txs[1].Hash) 425 require.NoError(t, err) 426 427 select { 428 case we := <-eventChan: 429 require.Equal(t, EventPendingTransactionUpdate, we.Type) 430 case <-time.After(1 * time.Second): 431 t.Fatal("timeout waiting for the delete event") 432 } 433 434 sub.Unsubscribe() 435 } 436 437 func TestPendingTxTracker_Watch_StatusChangeIncrementally(t *testing.T) { 438 m, stop, chainClient, eventFeed := setupTestTransactionDB(t, common.NewAndSet(1*time.Nanosecond)) 439 defer stop() 440 441 txs := GenerateTestPendingTransactions(0, 2) 442 443 var firsDoneWG sync.WaitGroup 444 firsDoneWG.Add(1) 445 446 // Mock the first call to getTransactionByHash 447 chainClient.SetAvailableClients([]common.ChainID{txs[0].ChainID}) 448 cl := chainClient.Clients[txs[0].ChainID] 449 450 cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool { 451 if len(cl.Calls) == 0 { 452 res := len(b) > 0 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[0].Hash 453 // If the first processing call picked up the second validate this case also 454 if len(b) == 2 { 455 res = res && b[1].Method == GetTransactionReceiptRPCName && b[1].Args[0] == txs[1].Hash 456 } 457 return res 458 } 459 // Second call we expect only one left 460 return len(b) == 1 && (b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[1].Hash) 461 })).Return(nil).Twice().Run(func(args mock.Arguments) { 462 elems := args.Get(1).([]rpc.BatchElem) 463 if len(cl.Calls) == 2 { 464 firsDoneWG.Wait() 465 } 466 // Only first item is processed, second is left pending 467 // Simulate parsing of eth_getTransactionReceipt response 468 elems[0].Result.(*nullableReceipt).Receipt = &types.Receipt{ 469 BlockNumber: new(big.Int).SetUint64(1), 470 Status: 1, 471 } 472 }) 473 474 eventChan := make(chan walletevent.Event, 6) 475 sub := eventFeed.Subscribe(eventChan) 476 477 for i := range txs { 478 // Track the first transaction 479 err := m.TrackPendingTransaction(txs[i].ChainID, txs[i].Hash, txs[i].From, txs[i].To, txs[i].Type, Keep, "") 480 require.NoError(t, err) 481 } 482 483 storeEventCount := 0 484 statusEventCount := 0 485 486 validateStatusChange := func(we *walletevent.Event) { 487 var p StatusChangedPayload 488 err := json.Unmarshal([]byte(we.Message), &p) 489 require.NoError(t, err) 490 491 if statusEventCount == 0 { 492 require.Equal(t, txs[0].ChainID, p.ChainID) 493 require.Equal(t, txs[0].Hash, p.Hash) 494 require.Equal(t, Success, p.Status) 495 496 status, err := m.Watch(context.Background(), txs[0].ChainID, txs[0].Hash) 497 require.NoError(t, err) 498 require.Equal(t, Success, *status) 499 err = m.Delete(context.Background(), txs[0].ChainID, txs[0].Hash) 500 require.NoError(t, err) 501 502 status, err = m.Watch(context.Background(), txs[1].ChainID, txs[1].Hash) 503 require.NoError(t, err) 504 require.Equal(t, Pending, *status) 505 firsDoneWG.Done() 506 } else { 507 _, err := m.Watch(context.Background(), txs[0].ChainID, txs[0].Hash) 508 require.Equal(t, err, sql.ErrNoRows) 509 510 status, err := m.Watch(context.Background(), txs[1].ChainID, txs[1].Hash) 511 require.NoError(t, err) 512 require.Equal(t, Success, *status) 513 err = m.Delete(context.Background(), txs[1].ChainID, txs[1].Hash) 514 require.NoError(t, err) 515 } 516 517 statusEventCount++ 518 } 519 520 for j := 0; j < 6; j++ { 521 select { 522 case we := <-eventChan: 523 if EventPendingTransactionUpdate == we.Type { 524 storeEventCount++ 525 } else if EventPendingTransactionStatusChanged == we.Type { 526 validateStatusChange(&we) 527 } 528 case <-time.After(1 * time.Second): 529 t.Fatal("timeout waiting for the status update event") 530 } 531 } 532 533 _, err := m.Watch(context.Background(), txs[1].ChainID, txs[1].Hash) 534 require.Equal(t, err, sql.ErrNoRows) 535 536 // One for add and one for delete 537 require.Equal(t, 4, storeEventCount) 538 require.Equal(t, 2, statusEventCount) 539 540 err = m.Stop() 541 require.NoError(t, err) 542 543 waitForTaskToStop(m) 544 545 res, err := m.GetAllPending() 546 require.NoError(t, err) 547 require.Equal(t, 0, len(res), "should have no pending tx") 548 549 sub.Unsubscribe() 550 } 551 552 func TestPendingTransactions(t *testing.T) { 553 manager, stop, _, _ := setupTestTransactionDB(t, nil) 554 defer stop() 555 556 tx := GenerateTestPendingTransactions(0, 1)[0] 557 558 rst, err := manager.GetAllPending() 559 require.NoError(t, err) 560 require.Nil(t, rst) 561 562 rst, err = manager.GetPendingByAddress([]uint64{777}, tx.From) 563 require.NoError(t, err) 564 require.Nil(t, rst) 565 566 err = manager.addPending(&tx) 567 require.NoError(t, err) 568 569 rst, err = manager.GetPendingByAddress([]uint64{777}, tx.From) 570 require.NoError(t, err) 571 require.Equal(t, 1, len(rst)) 572 require.Equal(t, tx, *rst[0]) 573 574 rst, err = manager.GetAllPending() 575 require.NoError(t, err) 576 require.Equal(t, 1, len(rst)) 577 require.Equal(t, tx, *rst[0]) 578 579 rst, err = manager.GetPendingByAddress([]uint64{777}, eth.Address{2}) 580 require.NoError(t, err) 581 require.Nil(t, rst) 582 583 err = manager.Delete(context.Background(), common.ChainID(777), tx.Hash) 584 require.Error(t, err, ErrStillPending) 585 586 rst, err = manager.GetPendingByAddress([]uint64{777}, tx.From) 587 require.NoError(t, err) 588 require.Equal(t, 0, len(rst)) 589 590 rst, err = manager.GetAllPending() 591 require.NoError(t, err) 592 require.Equal(t, 0, len(rst)) 593 }