github.com/status-im/status-go@v1.1.0/services/wallet/reader_test.go (about) 1 package wallet 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "math/big" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/golang/mock/gomock" 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 16 "github.com/ethereum/go-ethereum/common" 17 "github.com/ethereum/go-ethereum/common/hexutil" 18 "github.com/ethereum/go-ethereum/event" 19 "github.com/status-im/status-go/rpc/chain" 20 mock_client "github.com/status-im/status-go/rpc/chain/mock/client" 21 "github.com/status-im/status-go/services/wallet/testutils" 22 "github.com/status-im/status-go/services/wallet/token" 23 mock_balance_persistence "github.com/status-im/status-go/services/wallet/token/mock/balance_persistence" 24 mock_token "github.com/status-im/status-go/services/wallet/token/mock/token" 25 ) 26 27 var ( 28 testTokenAddress1 = common.Address{0x34} 29 testTokenAddress2 = common.Address{0x56} 30 31 testAccAddress1 = common.Address{0x12} 32 testAccAddress2 = common.Address{0x45} 33 34 expectedTokens = map[common.Address][]token.StorageToken{ 35 testAccAddress1: []token.StorageToken{ 36 { 37 Token: token.Token{ 38 Address: testTokenAddress1, 39 Name: "Token 1", 40 Symbol: "T1", 41 Decimals: 18, 42 }, 43 BalancesPerChain: nil, 44 }, 45 }, 46 testAccAddress2: []token.StorageToken{ 47 { 48 Token: token.Token{ 49 Address: testTokenAddress2, 50 Name: "Token 2", 51 Symbol: "T2", 52 Decimals: 18, 53 }, 54 BalancesPerChain: map[uint64]token.ChainBalance{ 55 1: { 56 RawBalance: "1000000000000000000", 57 Balance: big.NewFloat(1), 58 Address: common.Address{0x12}, 59 ChainID: 1, 60 HasError: false, 61 }, 62 }, 63 }, 64 }, 65 } 66 ) 67 68 // This matcher is used to compare the expected and actual map[common.Address][]token.StorageToken in parameters to SaveTokens 69 type mapTokenWithBalanceMatcher struct { 70 expected []interface{} 71 } 72 73 func (m mapTokenWithBalanceMatcher) Matches(x interface{}) bool { 74 actual, ok := x.(map[common.Address][]token.StorageToken) 75 if !ok { 76 return false 77 } 78 79 if len(m.expected) != len(actual) { 80 return false 81 } 82 83 expected := m.expected[0].(map[common.Address][]token.StorageToken) 84 85 for address, expectedTokens := range expected { 86 actualTokens, ok := actual[address] 87 if !ok { 88 return false 89 } 90 91 if len(expectedTokens) != len(actualTokens) { 92 return false 93 } 94 95 for i, expectedToken := range expectedTokens { 96 actualToken := actualTokens[i] 97 if expectedToken.Token != actualToken.Token { 98 return false 99 } 100 101 if len(expectedToken.BalancesPerChain) != len(actualToken.BalancesPerChain) { 102 return false 103 } 104 105 // We can't compare the balances directly because the Balance field is a big.Float 106 for chainID, expectedBalance := range expectedToken.BalancesPerChain { 107 actualBalance, ok := actualToken.BalancesPerChain[chainID] 108 if !ok { 109 return false 110 } 111 112 if expectedBalance.Balance.Cmp(actualBalance.Balance) != 0 { 113 return false 114 } 115 116 if expectedBalance.RawBalance != actualBalance.RawBalance { 117 return false 118 } 119 120 if expectedBalance.Address != actualBalance.Address { 121 return false 122 } 123 124 if expectedBalance.ChainID != actualBalance.ChainID { 125 return false 126 } 127 128 if expectedBalance.HasError != actualBalance.HasError { 129 return false 130 } 131 } 132 } 133 } 134 135 return true 136 } 137 138 func (m *mapTokenWithBalanceMatcher) String() string { 139 return fmt.Sprintf("%v", m.expected) 140 } 141 142 func newMapTokenWithBalanceMatcher(expected []interface{}) gomock.Matcher { 143 return &mapTokenWithBalanceMatcher{ 144 expected: expected, 145 } 146 } 147 func testChainBalancesEqual(t *testing.T, expected, actual token.ChainBalance) { 148 assert.Equal(t, expected.RawBalance, actual.RawBalance) 149 assert.Equal(t, 0, expected.Balance.Cmp(actual.Balance)) 150 assert.Equal(t, expected.Address, actual.Address) 151 assert.Equal(t, expected.ChainID, actual.ChainID) 152 assert.Equal(t, expected.HasError, actual.HasError) 153 assert.Equal(t, expected.Balance1DayAgo, actual.Balance1DayAgo) 154 } 155 156 func testBalancePerChainEqual(t *testing.T, expected, actual map[uint64]token.ChainBalance) { 157 assert.Len(t, actual, len(expected)) 158 for chainID, expectedBalance := range expected { 159 actualBalance, ok := actual[chainID] 160 assert.True(t, ok) 161 testChainBalancesEqual(t, expectedBalance, actualBalance) 162 } 163 } 164 165 func setupReader(t *testing.T) (*Reader, *mock_token.MockManagerInterface, *mock_balance_persistence.MockTokenBalancesStorage, *gomock.Controller) { 166 mockCtrl := gomock.NewController(t) 167 mockTokenManager := mock_token.NewMockManagerInterface(mockCtrl) 168 tokenBalanceStorage := mock_balance_persistence.NewMockTokenBalancesStorage(mockCtrl) 169 eventsFeed := &event.Feed{} 170 171 return NewReader(mockTokenManager, nil, tokenBalanceStorage, eventsFeed), mockTokenManager, tokenBalanceStorage, mockCtrl 172 } 173 174 func TestGetCachedWalletTokensWithoutMarketData(t *testing.T) { 175 reader, _, tokenPersistence, mockCtrl := setupReader(t) 176 defer mockCtrl.Finish() 177 178 // Test when there is an error getting the tokens 179 tokenPersistence.EXPECT().GetTokens().Return(nil, errors.New("error")) 180 tokens, err := reader.getCachedWalletTokensWithoutMarketData() 181 require.Error(t, err) 182 assert.Nil(t, tokens) 183 184 // Test happy path 185 tokenPersistence.EXPECT().GetTokens().Return(expectedTokens, nil) 186 tokens, err = reader.getCachedWalletTokensWithoutMarketData() 187 require.NoError(t, err) 188 assert.Equal(t, expectedTokens, tokens) 189 } 190 191 func TestIsBalanceCacheValid(t *testing.T) { 192 reader, _, tokenPersistence, mockCtrl := setupReader(t) 193 defer mockCtrl.Finish() 194 195 // Mock the cache to be valid 196 addresses := []common.Address{testAccAddress1, testAccAddress2} 197 reader.balanceRefreshed() 198 reader.updateTokenUpdateTimestamp([]common.Address{testAccAddress1, testAccAddress2}) 199 tokenPersistence.EXPECT().GetTokens().Return(expectedTokens, nil) 200 valid := reader.isBalanceCacheValid(addresses) 201 assert.True(t, valid) 202 203 // Mock the cache to be invalid 204 reader.invalidateBalanceCache() 205 valid = reader.isBalanceCacheValid(addresses) 206 assert.False(t, valid) 207 208 // Make cached tokens not contain all the addresses 209 reader.balanceRefreshed() 210 cachedTokens := map[common.Address][]token.StorageToken{ 211 testAccAddress1: expectedTokens[testAccAddress1], 212 } 213 tokenPersistence.EXPECT().GetTokens().Return(cachedTokens, nil) 214 valid = reader.isBalanceCacheValid(addresses) 215 assert.False(t, valid) 216 217 // Some timestamps are not updated 218 reader.balanceRefreshed() 219 reader.lastWalletTokenUpdateTimestamp = sync.Map{} 220 reader.updateTokenUpdateTimestamp([]common.Address{testAccAddress1}) 221 cachedTokens = expectedTokens 222 tokenPersistence.EXPECT().GetTokens().AnyTimes().Return(cachedTokens, nil) 223 assert.False(t, valid) 224 } 225 226 func TestTokensCachedForAddresses(t *testing.T) { 227 reader, _, persistence, mockCtrl := setupReader(t) 228 defer mockCtrl.Finish() 229 230 addresses := []common.Address{testAccAddress1, testAccAddress2} 231 232 // Test when the cached tokens do not contain all the addresses 233 cachedTokens := map[common.Address][]token.StorageToken{ 234 testAccAddress1: expectedTokens[testAccAddress1], 235 } 236 persistence.EXPECT().GetTokens().Return(cachedTokens, nil) 237 238 result := reader.tokensCachedForAddresses(addresses) 239 assert.False(t, result) 240 241 // Test when the cached tokens contain all the addresses 242 cachedTokens = expectedTokens 243 persistence.EXPECT().GetTokens().Return(cachedTokens, nil) 244 245 result = reader.tokensCachedForAddresses(addresses) 246 assert.True(t, result) 247 } 248 249 func TestFetchBalancesInternal(t *testing.T) { 250 reader, tokenManager, _, mockCtrl := setupReader(t) 251 defer mockCtrl.Finish() 252 253 addresses := []common.Address{testAccAddress1, testAccAddress2} 254 tokenAddresses := []common.Address{testTokenAddress1, testTokenAddress2} 255 ctx := context.TODO() 256 clients := map[uint64]chain.ClientInterface{} 257 258 // Test when there is an error getting the tokens 259 tokenManager.EXPECT().GetBalancesByChain(ctx, clients, addresses, tokenAddresses).Return(nil, errors.New("error")) 260 balances, err := reader.fetchBalances(ctx, clients, addresses, tokenAddresses) 261 require.Error(t, err) 262 assert.Nil(t, balances) 263 264 // Test happy path 265 expectedBalances := map[uint64]map[common.Address]map[common.Address]*hexutil.Big{ 266 1: { 267 testAccAddress2: { 268 testTokenAddress2: (*hexutil.Big)(big.NewInt(1)), 269 }, 270 }, 271 } 272 tokenManager.EXPECT().GetBalancesByChain(ctx, clients, addresses, tokenAddresses).Return(expectedBalances, nil) 273 balances, err = reader.fetchBalances(ctx, clients, addresses, tokenAddresses) 274 require.NoError(t, err) 275 assert.Equal(t, balances, expectedBalances) 276 } 277 278 func TestTokensToBalancesPerChain(t *testing.T) { 279 cachedTokens := map[common.Address][]token.StorageToken{ 280 testAccAddress1: []token.StorageToken{ 281 { 282 Token: token.Token{ 283 Address: testTokenAddress1, 284 Name: "Token 1", 285 Symbol: "T1", 286 Decimals: 18, 287 }, 288 BalancesPerChain: map[uint64]token.ChainBalance{ 289 1: { 290 RawBalance: "1000000000000000000", 291 Balance: big.NewFloat(1), 292 Address: common.Address{0x12}, 293 ChainID: 1, 294 HasError: false, 295 }, 296 }, 297 }, 298 { 299 Token: token.Token{ 300 Address: testTokenAddress2, 301 Name: "Token 2", 302 Symbol: "T2", 303 Decimals: 18, 304 }, 305 BalancesPerChain: nil, // Skip this token 306 }, 307 }, 308 testAccAddress2: []token.StorageToken{ 309 { 310 Token: token.Token{ 311 Address: testTokenAddress2, 312 Name: "Token 2", 313 Symbol: "T2", 314 Decimals: 18, 315 }, 316 BalancesPerChain: map[uint64]token.ChainBalance{ 317 1: { 318 RawBalance: "2000000000000000000", 319 Balance: big.NewFloat(2), 320 Address: common.Address{0x34}, 321 ChainID: 1, 322 HasError: false, 323 }, 324 2: { 325 RawBalance: "3000000000000000000", 326 Balance: big.NewFloat(3), 327 Address: common.Address{0x56}, 328 ChainID: 2, 329 HasError: false, 330 }, 331 }, 332 }, 333 }, 334 } 335 336 expectedBalancesPerChain := map[uint64]map[common.Address]map[common.Address]*hexutil.Big{ 337 1: { 338 testAccAddress1: { 339 common.Address{0x12}: (*hexutil.Big)(big.NewInt(1000000000000000000)), 340 }, 341 testAccAddress2: { 342 common.Address{0x34}: (*hexutil.Big)(big.NewInt(2000000000000000000)), 343 }, 344 }, 345 2: { 346 testAccAddress2: { 347 common.Address{0x56}: (*hexutil.Big)(big.NewInt(3000000000000000000)), 348 }, 349 }, 350 } 351 352 result := tokensToBalancesPerChain(cachedTokens) 353 354 assert.Equal(t, expectedBalancesPerChain, result) 355 } 356 357 func TestGetBalance1DayAgo(t *testing.T) { 358 reader, tokenManager, _, mockCtrl := setupReader(t) 359 defer mockCtrl.Finish() 360 361 address := common.Address{0x12} 362 chainID := uint64(1) 363 symbol := "T1" 364 dayAgoTimestamp := time.Now().Add(-24 * time.Hour).Unix() 365 366 // Test happy path 367 expectedBalance := big.NewInt(1000000000000000000) 368 tokenManager.EXPECT().GetTokenHistoricalBalance(address, chainID, symbol, dayAgoTimestamp).Return(expectedBalance, nil) 369 370 balance1DayAgo, err := reader.getBalance1DayAgo(&token.ChainBalance{ 371 ChainID: chainID, 372 Address: address, 373 }, dayAgoTimestamp, symbol, address) 374 375 require.NoError(t, err) 376 assert.Equal(t, expectedBalance, balance1DayAgo) 377 378 // Test error 379 tokenManager.EXPECT().GetTokenHistoricalBalance(address, chainID, symbol, dayAgoTimestamp).Return(nil, errors.New("error")) 380 balance1DayAgo, err = reader.getBalance1DayAgo(&token.ChainBalance{ 381 ChainID: chainID, 382 Address: address, 383 }, dayAgoTimestamp, symbol, address) 384 385 require.Error(t, err) 386 assert.Nil(t, balance1DayAgo) 387 } 388 389 func TestToChainBalance(t *testing.T) { 390 balances := map[uint64]map[common.Address]map[common.Address]*hexutil.Big{ 391 1: { 392 common.Address{0x12}: { 393 common.Address{0x34}: (*hexutil.Big)(big.NewInt(1000000000000000000)), 394 }, 395 }, 396 } 397 tok := &token.Token{ 398 ChainID: 1, 399 Address: common.Address{0x34}, 400 Symbol: "T1", 401 Decimals: 18, 402 } 403 address := common.Address{0x12} 404 decimals := uint(18) 405 cachedTokens := map[common.Address][]token.StorageToken{ 406 common.Address{0x12}: { 407 { 408 Token: token.Token{ 409 Address: common.Address{0x34}, 410 Name: "Token 1", 411 Symbol: "T1", 412 Decimals: 18, 413 }, 414 BalancesPerChain: nil, 415 }, 416 }, 417 } 418 419 expectedBalance := big.NewFloat(1) 420 hasError := false 421 expectedChainBalance := &token.ChainBalance{ 422 RawBalance: "1000000000000000000", 423 Balance: expectedBalance, 424 Balance1DayAgo: "0", 425 Address: common.Address{0x34}, 426 ChainID: 1, 427 HasError: hasError, 428 } 429 430 chainBalance := toChainBalance(balances, tok, address, decimals, cachedTokens, hasError, false) 431 testChainBalancesEqual(t, *expectedChainBalance, *chainBalance) 432 433 // Test when the token is not visible 434 emptyCachedTokens := map[common.Address][]token.StorageToken{} 435 isMandatory := false 436 noBalances := map[uint64]map[common.Address]map[common.Address]*hexutil.Big{ 437 tok.ChainID: { 438 address: { 439 tok.Address: nil, // Idk why this can be nil 440 }, 441 }, 442 } 443 chainBalance = toChainBalance(noBalances, tok, address, decimals, emptyCachedTokens, hasError, isMandatory) 444 assert.Nil(t, chainBalance) 445 } 446 447 func TestIsCachedToken(t *testing.T) { 448 cachedTokens := map[common.Address][]token.StorageToken{ 449 common.Address{0x12}: { 450 { 451 Token: token.Token{ 452 Address: common.Address{0x34}, 453 Name: "Token 1", 454 Symbol: "T1", 455 Decimals: 18, 456 }, 457 BalancesPerChain: map[uint64]token.ChainBalance{ 458 1: { 459 RawBalance: "1000000000000000000", 460 Balance: big.NewFloat(1), 461 Address: common.Address{0x12}, 462 ChainID: 1, 463 HasError: false, 464 }, 465 }, 466 }, 467 }, 468 } 469 470 address := common.Address{0x12} 471 symbol := "T1" 472 chainID := uint64(1) 473 474 // Test when the token is cached 475 result := isCachedToken(cachedTokens, address, symbol, chainID) 476 assert.True(t, result) 477 478 // Test when the token is not cached 479 result = isCachedToken(cachedTokens, address, "T2", chainID) 480 assert.False(t, result) 481 482 // Test when BalancesPerChain for token have no such a chainID 483 wrongChainID := chainID + 1 484 result = isCachedToken(cachedTokens, address, symbol, wrongChainID) 485 assert.False(t, result) 486 487 } 488 489 func TestCreateBalancePerChainPerSymbol(t *testing.T) { 490 address := common.Address{0x12} 491 balances := map[uint64]map[common.Address]map[common.Address]*hexutil.Big{ 492 1: { 493 address: { 494 common.Address{0x34}: (*hexutil.Big)(big.NewInt(1000000000000000000)), 495 }, 496 }, 497 2: { 498 address: { 499 common.Address{0x56}: (*hexutil.Big)(big.NewInt(2000000000000000000)), 500 }, 501 }, 502 } 503 504 tokens := []*token.Token{ 505 { 506 Name: "Token 1 mainnet", 507 ChainID: 1, 508 Address: common.Address{0x34}, 509 Symbol: "T1", 510 Decimals: 18, 511 }, 512 { 513 Name: "Token 1 optimism", 514 ChainID: 2, 515 Address: common.Address{0x56}, 516 Symbol: "T1", 517 Decimals: 18, 518 }, 519 } 520 // Let cached tokens not have the token for chain 2, it still should be calculated because of positive balance 521 cachedTokens := map[common.Address][]token.StorageToken{ 522 address: { 523 { 524 Token: token.Token{ 525 Address: common.Address{0x34}, 526 Name: "Token 1", 527 Symbol: "T1", 528 Decimals: 18, 529 }, 530 BalancesPerChain: map[uint64]token.ChainBalance{ 531 1: { 532 RawBalance: "1000000000000000000", 533 Balance: big.NewFloat(1), 534 Address: common.Address{0x12}, 535 ChainID: 1, 536 HasError: false, 537 }, 538 }, 539 }, 540 }, 541 } 542 543 clientConnectionPerChain := map[uint64]bool{ 544 1: true, 545 2: false, 546 } 547 dayAgoTimestamp := time.Now().Add(-24 * time.Hour).Unix() 548 549 expectedBalancesPerChain := map[uint64]token.ChainBalance{ 550 1: { 551 RawBalance: "1000000000000000000", 552 Balance: big.NewFloat(1), 553 Balance1DayAgo: "0", 554 Address: common.Address{0x34}, 555 ChainID: 1, 556 HasError: false, 557 }, 558 2: { 559 RawBalance: "2000000000000000000", 560 Balance: big.NewFloat(2), 561 Balance1DayAgo: "0", 562 Address: common.Address{0x56}, 563 ChainID: 2, 564 HasError: true, 565 }, 566 } 567 568 reader, tokenManager, _, mockCtrl := setupReader(t) 569 defer mockCtrl.Finish() 570 571 tokenManager.EXPECT().GetTokenHistoricalBalance(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(nil, errors.New("error")) 572 result := reader.createBalancePerChainPerSymbol(address, balances, tokens, cachedTokens, clientConnectionPerChain, dayAgoTimestamp) 573 574 assert.Len(t, result, 2) 575 testBalancePerChainEqual(t, expectedBalancesPerChain, result) 576 } 577 578 func TestCreateBalancePerChainPerSymbolWithMissingBalance(t *testing.T) { 579 address := common.Address{0x12} 580 tokens := []*token.Token{ 581 { 582 Name: "Token 1 mainnet", 583 ChainID: 1, 584 Address: common.Address{0x34}, 585 Symbol: "T1", 586 Decimals: 18, 587 }, 588 { 589 Name: "Token 1 optimism", 590 ChainID: 2, 591 Address: common.Address{0x56}, 592 Symbol: "T1", 593 Decimals: 18, 594 }, 595 } 596 597 clientConnectionPerChain := map[uint64]bool{ 598 1: true, 599 2: false, 600 } 601 602 dayAgoTimestamp := time.Now().Add(-24 * time.Hour).Unix() 603 emptyCachedTokens := map[common.Address][]token.StorageToken{} 604 oneBalanceMissing := map[uint64]map[common.Address]map[common.Address]*hexutil.Big{ 605 1: { 606 address: { 607 common.Address{0x34}: nil, // Idk why this can be nil 608 }, 609 }, 610 611 2: { 612 address: { 613 common.Address{0x56}: (*hexutil.Big)(big.NewInt(1000000000000000000)), 614 }, 615 }, 616 } 617 618 expectedBalancesPerChain := map[uint64]token.ChainBalance{ 619 2: { 620 RawBalance: "1000000000000000000", 621 Balance: big.NewFloat(1), 622 Balance1DayAgo: "1", 623 Address: common.Address{0x56}, 624 ChainID: 2, 625 HasError: true, 626 }, 627 } 628 629 reader, tokenManager, _, mockCtrl := setupReader(t) 630 defer mockCtrl.Finish() 631 632 tokenManager.EXPECT().GetTokenHistoricalBalance(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(big.NewInt(1), nil) 633 result := reader.createBalancePerChainPerSymbol(address, oneBalanceMissing, tokens, emptyCachedTokens, clientConnectionPerChain, dayAgoTimestamp) 634 assert.Len(t, result, 1) 635 testBalancePerChainEqual(t, expectedBalancesPerChain, result) 636 } 637 638 func TestBalancesToTokensByAddress(t *testing.T) { 639 connectedPerChain := map[uint64]bool{ 640 1: true, 641 2: true, 642 } 643 644 addresses := []common.Address{ 645 common.HexToAddress("0x123"), 646 common.HexToAddress("0x456"), 647 } 648 649 allTokens := []*token.Token{ 650 { 651 Name: "Token 1", 652 Symbol: "T1", 653 Decimals: 18, 654 Verified: true, 655 ChainID: 1, 656 Address: common.HexToAddress("0x789"), 657 }, 658 { 659 Name: "Token 2", 660 Symbol: "T2", 661 Decimals: 18, 662 Verified: true, 663 ChainID: 1, 664 Address: common.HexToAddress("0xdef"), 665 }, 666 { 667 Name: "Token 2 optimism", 668 Symbol: "T2", 669 Decimals: 18, 670 Verified: true, 671 ChainID: 2, 672 Address: common.HexToAddress("0xabc"), 673 }, 674 } 675 676 balances := map[uint64]map[common.Address]map[common.Address]*hexutil.Big{ 677 1: { 678 addresses[0]: { 679 allTokens[0].Address: (*hexutil.Big)(big.NewInt(1000000000000000000)), 680 }, 681 addresses[1]: { 682 allTokens[1].Address: (*hexutil.Big)(big.NewInt(2000000000000000000)), 683 }, 684 }, 685 2: { 686 addresses[1]: { 687 allTokens[2].Address: (*hexutil.Big)(big.NewInt(3000000000000000000)), 688 }, 689 }, 690 } 691 692 cachedTokens := map[common.Address][]token.StorageToken{ 693 addresses[0]: { 694 { 695 Token: token.Token{ 696 Name: "Token 1", 697 Symbol: "T1", 698 Decimals: 18, 699 Verified: true, 700 Address: common.HexToAddress("0x789"), 701 ChainID: 1, 702 }, 703 BalancesPerChain: map[uint64]token.ChainBalance{ 704 1: { 705 RawBalance: "1000000000000000000", 706 Balance: big.NewFloat(1), 707 Address: common.HexToAddress("0x789"), 708 ChainID: 1, 709 HasError: false, 710 }, 711 }, 712 }, 713 }, 714 } 715 716 expectedTokensPerAddress := map[common.Address][]token.StorageToken{ 717 addresses[0]: { 718 { 719 Token: token.Token{ 720 Name: "Token 1", 721 Symbol: "T1", 722 Decimals: 18, 723 Verified: true, 724 }, 725 BalancesPerChain: map[uint64]token.ChainBalance{ 726 1: { 727 RawBalance: "1000000000000000000", 728 Balance: big.NewFloat(1), 729 Address: common.HexToAddress("0x789"), 730 ChainID: 1, 731 HasError: false, 732 Balance1DayAgo: "0", 733 }, 734 }, 735 }, 736 }, 737 addresses[1]: { 738 { 739 Token: token.Token{ 740 Name: "Token 2", 741 Symbol: "T2", 742 Decimals: 18, 743 Verified: true, 744 }, 745 BalancesPerChain: map[uint64]token.ChainBalance{ 746 1: { 747 RawBalance: "2000000000000000000", 748 Balance: big.NewFloat(2), 749 Address: common.HexToAddress("0xdef"), 750 ChainID: 1, 751 HasError: false, 752 Balance1DayAgo: "0", 753 }, 754 2: { 755 RawBalance: "3000000000000000000", 756 Balance: big.NewFloat(3), 757 Address: common.HexToAddress("0xabc"), 758 ChainID: 2, 759 HasError: false, 760 Balance1DayAgo: "0", 761 }, 762 }, 763 }, 764 }, 765 } 766 767 reader, tokenManager, _, mockCtrl := setupReader(t) 768 defer mockCtrl.Finish() 769 770 tokenManager.EXPECT().GetTokenHistoricalBalance(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil, nil) 771 tokens := reader.balancesToTokensByAddress(connectedPerChain, addresses, allTokens, balances, cachedTokens) 772 773 assert.Len(t, tokens, 2) 774 assert.Equal(t, 1, len(tokens[addresses[0]])) 775 assert.Equal(t, 1, len(tokens[addresses[1]])) 776 777 for _, address := range addresses { 778 for i, token := range tokens[address] { 779 assert.Equal(t, expectedTokensPerAddress[address][i].Token, token.Token) 780 testBalancePerChainEqual(t, expectedTokensPerAddress[address][i].BalancesPerChain, token.BalancesPerChain) 781 } 782 } 783 } 784 785 func TestGetCachedBalances(t *testing.T) { 786 reader, tokenManager, persistence, mockCtrl := setupReader(t) 787 defer mockCtrl.Finish() 788 789 addresses := []common.Address{ 790 common.HexToAddress("0x123"), 791 common.HexToAddress("0x456"), 792 } 793 794 mockClientIface1 := mock_client.NewMockClientInterface(mockCtrl) 795 mockClientIface2 := mock_client.NewMockClientInterface(mockCtrl) 796 clients := map[uint64]chain.ClientInterface{ 797 1: mockClientIface1, 798 2: mockClientIface2, 799 } 800 801 allTokens := []*token.Token{ 802 { 803 Address: common.HexToAddress("0xabc"), 804 Name: "Token 1", 805 Symbol: "T1", 806 Decimals: 18, 807 ChainID: 1, 808 }, 809 { 810 Address: common.HexToAddress("0xdef"), 811 Name: "Token 2", 812 Symbol: "T2", 813 Decimals: 18, 814 ChainID: 2, 815 }, 816 { 817 Address: common.HexToAddress("0x789"), 818 Name: "Token 3", 819 Symbol: "T3", 820 Decimals: 10, 821 ChainID: 1, 822 }, 823 } 824 825 cachedTokens := map[common.Address][]token.StorageToken{ 826 addresses[0]: { 827 { 828 Token: token.Token{ 829 Address: common.HexToAddress("0xabc"), 830 Name: "Token 1", 831 Symbol: "T1", 832 Decimals: 18, 833 ChainID: 1, 834 }, 835 BalancesPerChain: nil, 836 }, 837 }, 838 addresses[1]: { 839 { 840 Token: token.Token{ 841 Address: common.HexToAddress("0xdef"), 842 Name: "Token 2", 843 Symbol: "T2", 844 Decimals: 18, 845 ChainID: 2, 846 }, 847 BalancesPerChain: map[uint64]token.ChainBalance{ 848 2: { 849 RawBalance: "1000000000000000000", 850 Balance: big.NewFloat(1), 851 Balance1DayAgo: "0", 852 Address: common.HexToAddress("0xdef"), 853 ChainID: 2, 854 HasError: false, 855 }, 856 }, 857 }, 858 }, 859 } 860 861 expectedTokens := map[common.Address][]token.StorageToken{ 862 addresses[1]: { 863 { 864 Token: token.Token{ 865 Name: "Token 2", 866 Symbol: "T2", 867 Decimals: 18, 868 }, 869 BalancesPerChain: map[uint64]token.ChainBalance{ 870 2: { 871 RawBalance: "1000000000000000000", 872 Balance: big.NewFloat(1), 873 Balance1DayAgo: "0", 874 Address: common.HexToAddress("0xdef"), 875 ChainID: 2, 876 HasError: false, 877 }, 878 }, 879 }, 880 }, 881 } 882 883 persistence.EXPECT().GetTokens().Return(cachedTokens, nil) 884 expectedChains := []uint64{1, 2} 885 tokenManager.EXPECT().GetTokensByChainIDs(testutils.NewUint64SliceMatcher(expectedChains)).Return(allTokens, nil) 886 tokenManager.EXPECT().GetTokenHistoricalBalance(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil) 887 mockClientIface1.EXPECT().IsConnected().Return(true) 888 mockClientIface2.EXPECT().IsConnected().Return(true) 889 tokens, err := reader.GetCachedBalances(clients, addresses) 890 require.NoError(t, err) 891 892 for _, address := range addresses { 893 for i, token := range tokens[address] { 894 assert.Equal(t, expectedTokens[address][i].Token, token.Token) 895 testBalancePerChainEqual(t, expectedTokens[address][i].BalancesPerChain, token.BalancesPerChain) 896 } 897 } 898 } 899 900 func TestFetchBalances(t *testing.T) { 901 reader, tokenManager, persistence, mockCtrl := setupReader(t) 902 defer mockCtrl.Finish() 903 904 addresses := []common.Address{ 905 common.HexToAddress("0x123"), 906 common.HexToAddress("0x456"), 907 } 908 909 mockClientIface1 := mock_client.NewMockClientInterface(mockCtrl) 910 mockClientIface2 := mock_client.NewMockClientInterface(mockCtrl) 911 clients := map[uint64]chain.ClientInterface{ 912 1: mockClientIface1, 913 2: mockClientIface2, 914 } 915 916 allTokens := []*token.Token{ 917 { 918 Address: common.HexToAddress("0xabc"), 919 Name: "Token 1", 920 Symbol: "T1", 921 Decimals: 18, 922 ChainID: 1, 923 }, 924 { 925 Address: common.HexToAddress("0xdef"), 926 Name: "Token 2", 927 Symbol: "T2", 928 Decimals: 18, 929 ChainID: 2, 930 }, 931 { 932 Address: common.HexToAddress("0x789"), 933 Name: "Token 3", 934 Symbol: "T3", 935 Decimals: 10, 936 ChainID: 1, 937 }, 938 } 939 940 cachedTokens := map[common.Address][]token.StorageToken{ 941 addresses[0]: { 942 { 943 Token: token.Token{ 944 Address: common.HexToAddress("0xabc"), 945 Name: "Token 1", 946 Symbol: "T1", 947 Decimals: 18, 948 ChainID: 1, 949 }, 950 BalancesPerChain: nil, 951 }, 952 }, 953 addresses[1]: { 954 { 955 Token: token.Token{ 956 Address: common.HexToAddress("0xdef"), 957 Name: "Token 2", 958 Symbol: "T2", 959 Decimals: 18, 960 ChainID: 2, 961 }, 962 BalancesPerChain: map[uint64]token.ChainBalance{ 963 2: { 964 RawBalance: "1000000000000000000", 965 Balance: big.NewFloat(1), 966 Balance1DayAgo: "0", 967 Address: common.HexToAddress("0xdef"), 968 ChainID: 2, 969 HasError: false, 970 }, 971 }, 972 }, 973 }, 974 } 975 976 expectedTokens := map[common.Address][]token.StorageToken{ 977 addresses[1]: { 978 { 979 Token: token.Token{ 980 Name: "Token 2", 981 Symbol: "T2", 982 Decimals: 18, 983 }, 984 BalancesPerChain: map[uint64]token.ChainBalance{ 985 2: { 986 RawBalance: "2000000000000000000", 987 Balance: big.NewFloat(2), 988 Balance1DayAgo: "0", 989 Address: common.HexToAddress("0xdef"), 990 ChainID: 2, 991 HasError: false, 992 }, 993 }, 994 }, 995 }, 996 } 997 998 persistence.EXPECT().GetTokens().Times(2).Return(cachedTokens, nil) 999 // Verify that proper tokens are saved 1000 persistence.EXPECT().SaveTokens(newMapTokenWithBalanceMatcher([]interface{}{expectedTokens})).Return(nil) 1001 expectedChains := []uint64{1, 2} 1002 tokenManager.EXPECT().GetTokensByChainIDs(testutils.NewUint64SliceMatcher(expectedChains)).Return(allTokens, nil) 1003 tokenManager.EXPECT().GetTokenHistoricalBalance(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil) 1004 expectedBalances := map[uint64]map[common.Address]map[common.Address]*hexutil.Big{ 1005 2: { 1006 addresses[1]: { 1007 allTokens[1].Address: (*hexutil.Big)(big.NewInt(2000000000000000000)), 1008 }, 1009 }, 1010 } 1011 1012 tokenAddresses := getTokenAddresses(allTokens) 1013 tokenManager.EXPECT().GetBalancesByChain(context.TODO(), clients, addresses, testutils.NewAddressSliceMatcher(tokenAddresses)).Return(expectedBalances, nil) 1014 mockClientIface1.EXPECT().IsConnected().Return(true) 1015 mockClientIface2.EXPECT().IsConnected().Return(true) 1016 tokens, err := reader.FetchBalances(context.TODO(), clients, addresses) 1017 require.NoError(t, err) 1018 1019 for address, tokenList := range tokens { 1020 for i, token := range tokenList { 1021 assert.Equal(t, expectedTokens[address][i].Token, token.Token) 1022 testBalancePerChainEqual(t, expectedTokens[address][i].BalancesPerChain, token.BalancesPerChain) 1023 } 1024 } 1025 1026 require.True(t, reader.isBalanceCacheValid(addresses)) 1027 } 1028 1029 func TestReaderRestart(t *testing.T) { 1030 reader, _, _, mockCtrl := setupReader(t) 1031 defer mockCtrl.Finish() 1032 1033 err := reader.Start() 1034 require.NoError(t, err) 1035 require.NotNil(t, reader.walletEventsWatcher) 1036 previousWalletEventsWatcher := reader.walletEventsWatcher 1037 1038 err = reader.Restart() 1039 require.NoError(t, err) 1040 require.NotNil(t, reader.walletEventsWatcher) 1041 require.NotEqual(t, previousWalletEventsWatcher, reader.walletEventsWatcher) 1042 } 1043 1044 func TestFetchOrGetCachedWalletBalances(t *testing.T) { 1045 // Test the behavior of FetchOrGetCachedWalletBalances when fetching new balances fails. 1046 // This focuses on the error handling path where the function should return the cached balances as a fallback. 1047 // We don't explicitly test the contents of fetched or cached balances here, as those 1048 // are covered in other dedicated tests. The main goal is to ensure the correct flow of 1049 // execution and data retrieval in this specific failure scenario. 1050 1051 reader, _, tokenPersistence, mockCtrl := setupReader(t) 1052 defer mockCtrl.Finish() 1053 1054 reader.invalidateBalanceCache() 1055 1056 tokenPersistence.EXPECT().GetTokens().Return(nil, errors.New("error")).AnyTimes() 1057 1058 clients := map[uint64]chain.ClientInterface{} 1059 addresses := []common.Address{} 1060 1061 _, err := reader.FetchOrGetCachedWalletBalances(context.TODO(), clients, addresses, false) 1062 require.Error(t, err) 1063 }