Below "Hidden Tokens" add another section called "Popular Tokens" #2845

pull/2861/head
Vladyslav shepitko 3 years ago
parent d4445040ae
commit e25872a67e
  1. 36
      AlphaWallet.xcodeproj/project.pbxproj
  2. 24
      AlphaWallet/Core/Coordinators/FilterTokensCoordinator.swift
  3. 130
      AlphaWallet/Core/PopularTokens/PopularTokensCollection.swift
  4. 111
      AlphaWallet/Core/PopularTokens/known_contract.json
  5. 38
      AlphaWallet/Core/Types/ThreadSafeDictionary.swift
  6. 2
      AlphaWallet/Extensions/UIView.swift
  7. 1
      AlphaWallet/Localization/en.lproj/Localizable.strings
  8. 1
      AlphaWallet/Localization/es.lproj/Localizable.strings
  9. 1
      AlphaWallet/Localization/ja.lproj/Localizable.strings
  10. 1
      AlphaWallet/Localization/ko.lproj/Localizable.strings
  11. 1
      AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings
  12. 14
      AlphaWallet/Tokens/Coordinators/AddHideTokensCoordinator.swift
  13. 41
      AlphaWallet/Tokens/Coordinators/SingleChainTokenCoordinator.swift
  14. 127
      AlphaWallet/Tokens/Types/TokenObject.swift
  15. 132
      AlphaWallet/Tokens/ViewControllers/AddHideTokensViewController.swift
  16. 95
      AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift
  17. 50
      AlphaWallet/Tokens/ViewModels/PopularTokenViewCellViewModel.swift
  18. 2
      AlphaWallet/Tokens/ViewModels/TokenViewControllerViewModel.swift
  19. 63
      AlphaWallet/Tokens/ViewModels/WalletTokenViewCellViewModel.swift
  20. 69
      AlphaWallet/Tokens/Views/PopularTokenViewCell.swift
  21. 73
      AlphaWallet/Tokens/Views/WalletTokenViewCell.swift
  22. 169
      AlphaWallet/UI/TokenObject+UI.swift

@ -717,6 +717,7 @@
873F8063246E8E3E00EEE5EF /* SelectCurrencyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873F8062246E8E3E00EEE5EF /* SelectCurrencyButton.swift */; }; 873F8063246E8E3E00EEE5EF /* SelectCurrencyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873F8062246E8E3E00EEE5EF /* SelectCurrencyButton.swift */; };
8743CB50255059780039E469 /* DomainResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8743CB4F255059780039E469 /* DomainResolver.swift */; }; 8743CB50255059780039E469 /* DomainResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8743CB4F255059780039E469 /* DomainResolver.swift */; };
874AF0832603405F00D613A5 /* LoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874AF0822603405F00D613A5 /* LoadingIndicatorView.swift */; }; 874AF0832603405F00D613A5 /* LoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874AF0822603405F00D613A5 /* LoadingIndicatorView.swift */; };
874BD2BB2669F65800E62E02 /* PopularTokensCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874BD2BA2669F65800E62E02 /* PopularTokensCollection.swift */; };
874C6D8125E3FF2300AD8380 /* ConfirmationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874C6D8025E3FF2300AD8380 /* ConfirmationHeaderView.swift */; }; 874C6D8125E3FF2300AD8380 /* ConfirmationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874C6D8025E3FF2300AD8380 /* ConfirmationHeaderView.swift */; };
874C6D8325E3FF3B00AD8380 /* ConfirmationHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874C6D8225E3FF3B00AD8380 /* ConfirmationHeaderViewModel.swift */; }; 874C6D8325E3FF3B00AD8380 /* ConfirmationHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874C6D8225E3FF3B00AD8380 /* ConfirmationHeaderViewModel.swift */; };
874D099025EE32EF00A58EF2 /* SignatureConfirmationDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D098F25EE32EF00A58EF2 /* SignatureConfirmationDetailsViewModel.swift */; }; 874D099025EE32EF00A58EF2 /* SignatureConfirmationDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D098F25EE32EF00A58EF2 /* SignatureConfirmationDetailsViewModel.swift */; };
@ -741,6 +742,12 @@
87584B4425EFAFEC0070063B /* BuyTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87584B4325EFAFEC0070063B /* BuyTokenService.swift */; }; 87584B4425EFAFEC0070063B /* BuyTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87584B4325EFAFEC0070063B /* BuyTokenService.swift */; };
87584B4725EFB0950070063B /* Ramp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87584B4625EFB0950070063B /* Ramp.swift */; }; 87584B4725EFB0950070063B /* Ramp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87584B4625EFB0950070063B /* Ramp.swift */; };
875B3C34250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 875B3C33250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift */; }; 875B3C34250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 875B3C33250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift */; };
87620024266E00F70059B05A /* known_contract.json in Resources */ = {isa = PBXBuildFile; fileRef = 87620023266E00F60059B05A /* known_contract.json */; };
87620026266E07490059B05A /* ThreadSafeDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87620025266E07490059B05A /* ThreadSafeDictionary.swift */; };
87620028266E14A80059B05A /* PopularTokenViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87620027266E14A80059B05A /* PopularTokenViewCell.swift */; };
8762002A266E14C10059B05A /* WalletTokenViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87620029266E14C10059B05A /* WalletTokenViewCell.swift */; };
8762002C266E150B0059B05A /* WalletTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8762002B266E150B0059B05A /* WalletTokenViewCellViewModel.swift */; };
8762002E266E15310059B05A /* PopularTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8762002D266E15310059B05A /* PopularTokenViewCellViewModel.swift */; };
8769888D24C6ED04002BF62B /* TransactionInProgressCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8769888C24C6ED04002BF62B /* TransactionInProgressCoordinator.swift */; }; 8769888D24C6ED04002BF62B /* TransactionInProgressCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8769888C24C6ED04002BF62B /* TransactionInProgressCoordinator.swift */; };
8769BCA8256D15BF0095EA5B /* BlockieImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */; }; 8769BCA8256D15BF0095EA5B /* BlockieImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */; };
87713EAE264ABF2800B1B9CB /* DecodedFunctionCall+Decode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87713EAD264ABF2800B1B9CB /* DecodedFunctionCall+Decode.swift */; }; 87713EAE264ABF2800B1B9CB /* DecodedFunctionCall+Decode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87713EAD264ABF2800B1B9CB /* DecodedFunctionCall+Decode.swift */; };
@ -1618,6 +1625,7 @@
873F8062246E8E3E00EEE5EF /* SelectCurrencyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrencyButton.swift; sourceTree = "<group>"; }; 873F8062246E8E3E00EEE5EF /* SelectCurrencyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCurrencyButton.swift; sourceTree = "<group>"; };
8743CB4F255059780039E469 /* DomainResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainResolver.swift; sourceTree = "<group>"; }; 8743CB4F255059780039E469 /* DomainResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainResolver.swift; sourceTree = "<group>"; };
874AF0822603405F00D613A5 /* LoadingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorView.swift; sourceTree = "<group>"; }; 874AF0822603405F00D613A5 /* LoadingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorView.swift; sourceTree = "<group>"; };
874BD2BA2669F65800E62E02 /* PopularTokensCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularTokensCollection.swift; sourceTree = "<group>"; };
874C6D8025E3FF2300AD8380 /* ConfirmationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationHeaderView.swift; sourceTree = "<group>"; }; 874C6D8025E3FF2300AD8380 /* ConfirmationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationHeaderView.swift; sourceTree = "<group>"; };
874C6D8225E3FF3B00AD8380 /* ConfirmationHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationHeaderViewModel.swift; sourceTree = "<group>"; }; 874C6D8225E3FF3B00AD8380 /* ConfirmationHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationHeaderViewModel.swift; sourceTree = "<group>"; };
874D098F25EE32EF00A58EF2 /* SignatureConfirmationDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignatureConfirmationDetailsViewModel.swift; sourceTree = "<group>"; }; 874D098F25EE32EF00A58EF2 /* SignatureConfirmationDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignatureConfirmationDetailsViewModel.swift; sourceTree = "<group>"; };
@ -1642,6 +1650,12 @@
87584B4325EFAFEC0070063B /* BuyTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyTokenService.swift; sourceTree = "<group>"; }; 87584B4325EFAFEC0070063B /* BuyTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyTokenService.swift; sourceTree = "<group>"; };
87584B4625EFB0950070063B /* Ramp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ramp.swift; sourceTree = "<group>"; }; 87584B4625EFB0950070063B /* Ramp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ramp.swift; sourceTree = "<group>"; };
875B3C33250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeResolutionCoordinator.swift; sourceTree = "<group>"; }; 875B3C33250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeResolutionCoordinator.swift; sourceTree = "<group>"; };
87620023266E00F60059B05A /* known_contract.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = known_contract.json; sourceTree = "<group>"; };
87620025266E07490059B05A /* ThreadSafeDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeDictionary.swift; sourceTree = "<group>"; };
87620027266E14A80059B05A /* PopularTokenViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularTokenViewCell.swift; sourceTree = "<group>"; };
87620029266E14C10059B05A /* WalletTokenViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTokenViewCell.swift; sourceTree = "<group>"; };
8762002B266E150B0059B05A /* WalletTokenViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTokenViewCellViewModel.swift; sourceTree = "<group>"; };
8762002D266E15310059B05A /* PopularTokenViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularTokenViewCellViewModel.swift; sourceTree = "<group>"; };
8769888C24C6ED04002BF62B /* TransactionInProgressCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionInProgressCoordinator.swift; sourceTree = "<group>"; }; 8769888C24C6ED04002BF62B /* TransactionInProgressCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionInProgressCoordinator.swift; sourceTree = "<group>"; };
8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockieImageView.swift; sourceTree = "<group>"; }; 8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockieImageView.swift; sourceTree = "<group>"; };
87713EAD264ABF2800B1B9CB /* DecodedFunctionCall+Decode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DecodedFunctionCall+Decode.swift"; sourceTree = "<group>"; }; 87713EAD264ABF2800B1B9CB /* DecodedFunctionCall+Decode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DecodedFunctionCall+Decode.swift"; sourceTree = "<group>"; };
@ -2380,6 +2394,8 @@
5E7C73BD3FD2C2844F5DEA45 /* SegmentedControl.swift */, 5E7C73BD3FD2C2844F5DEA45 /* SegmentedControl.swift */,
5E7C78FF8A5682C27E15B488 /* UITableViewCell+TokenCell.swift */, 5E7C78FF8A5682C27E15B488 /* UITableViewCell+TokenCell.swift */,
87D1757724ADAEEB002130D2 /* BlockchainTagLabel.swift */, 87D1757724ADAEEB002130D2 /* BlockchainTagLabel.swift */,
87620027266E14A80059B05A /* PopularTokenViewCell.swift */,
87620029266E14C10059B05A /* WalletTokenViewCell.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2430,6 +2446,8 @@
87C8018B24350174007648CF /* AddHideTokenSectionHeaderViewModel.swift */, 87C8018B24350174007648CF /* AddHideTokenSectionHeaderViewModel.swift */,
87D1757924ADAF07002130D2 /* BlockchainTagLabelViewModel.swift */, 87D1757924ADAF07002130D2 /* BlockchainTagLabelViewModel.swift */,
874DED1824C1BD2C006C8FCE /* SelectAssetViewModel.swift */, 874DED1824C1BD2C006C8FCE /* SelectAssetViewModel.swift */,
8762002B266E150B0059B05A /* WalletTokenViewCellViewModel.swift */,
8762002D266E15310059B05A /* PopularTokenViewCellViewModel.swift */,
); );
path = ViewModels; path = ViewModels;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2831,6 +2849,7 @@
5E7C7F8F3CB3847D0E4E977B /* AlphaWalletAddress.swift */, 5E7C7F8F3CB3847D0E4E977B /* AlphaWalletAddress.swift */,
5E7C7EE6BFC8BB79CD1C5565 /* AlphaWalletAddressExtension.swift */, 5E7C7EE6BFC8BB79CD1C5565 /* AlphaWalletAddressExtension.swift */,
5E7C7A92352937F37294F12C /* RateLimiter.swift */, 5E7C7A92352937F37294F12C /* RateLimiter.swift */,
87620025266E07490059B05A /* ThreadSafeDictionary.swift */,
); );
path = Types; path = Types;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2889,6 +2908,7 @@
29FC9BC31F830880000209CD /* Core */ = { 29FC9BC31F830880000209CD /* Core */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
874BD2B72669766C00E62E02 /* PopularTokens */,
87DB61BE264A94A6009FBDDE /* Ethereum */, 87DB61BE264A94A6009FBDDE /* Ethereum */,
879599482604A5A2006722B2 /* CoinTicker */, 879599482604A5A2006722B2 /* CoinTicker */,
87584B4225EFAFB60070063B /* BuyToken */, 87584B4225EFAFB60070063B /* BuyToken */,
@ -3977,6 +3997,15 @@
path = HoneySwap; path = HoneySwap;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
874BD2B72669766C00E62E02 /* PopularTokens */ = {
isa = PBXGroup;
children = (
87620023266E00F60059B05A /* known_contract.json */,
874BD2BA2669F65800E62E02 /* PopularTokensCollection.swift */,
);
path = PopularTokens;
sourceTree = "<group>";
};
87584B4225EFAFB60070063B /* BuyToken */ = { 87584B4225EFAFB60070063B /* BuyToken */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -4427,6 +4456,7 @@
C876FF82204A79D300B7D0EA /* SourceSansPro-Light.otf in Resources */, C876FF82204A79D300B7D0EA /* SourceSansPro-Light.otf in Resources */,
C876FF84204A79D300B7D0EA /* SourceSansPro-Regular.otf in Resources */, C876FF84204A79D300B7D0EA /* SourceSansPro-Regular.otf in Resources */,
C868C5292053BDE00059672B /* LaunchScreen.storyboard in Resources */, C868C5292053BDE00059672B /* LaunchScreen.storyboard in Resources */,
87620024266E00F70059B05A /* known_contract.json in Resources */,
C876FF85204A79D300B7D0EA /* SourceSansPro-Semibold.otf in Resources */, C876FF85204A79D300B7D0EA /* SourceSansPro-Semibold.otf in Resources */,
C880330E2054371500D73D6F /* non_asset_catalog_redemption_location@3x.png in Resources */, C880330E2054371500D73D6F /* non_asset_catalog_redemption_location@3x.png in Resources */,
C880332020551DF800D73D6F /* howDoIGetMyMoneyInfo.html in Resources */, C880332020551DF800D73D6F /* howDoIGetMyMoneyInfo.html in Resources */,
@ -4963,6 +4993,7 @@
29BB94931F6FC380009B09CC /* BalanceViewModel.swift in Sources */, 29BB94931F6FC380009B09CC /* BalanceViewModel.swift in Sources */,
29D03F1D1F712183006E548C /* Button.swift in Sources */, 29D03F1D1F712183006E548C /* Button.swift in Sources */,
CCCD74FD1FD2D38D004A087D /* CheckDeviceCoordinator.swift in Sources */, CCCD74FD1FD2D38D004A087D /* CheckDeviceCoordinator.swift in Sources */,
8762002E266E15310059B05A /* PopularTokenViewCellViewModel.swift in Sources */,
87D163A4242CD811002662D2 /* AddHideTokensCoordinator.swift in Sources */, 87D163A4242CD811002662D2 /* AddHideTokensCoordinator.swift in Sources */,
87ED8F95248535400005C69B /* SwitchTableViewCellViewModel.swift in Sources */, 87ED8F95248535400005C69B /* SwitchTableViewCellViewModel.swift in Sources */,
29E2E33E1F7A2423000CF94A /* TransactionHeaderView.swift in Sources */, 29E2E33E1F7A2423000CF94A /* TransactionHeaderView.swift in Sources */,
@ -5074,6 +5105,7 @@
5E7C71DAA5DAFF764F92587D /* SetTransferTokensCardExpiryDateViewController.swift in Sources */, 5E7C71DAA5DAFF764F92587D /* SetTransferTokensCardExpiryDateViewController.swift in Sources */,
5E7C745C725F3F34037DCC68 /* SetTransferTokensCardExpiryDateViewControllerViewModel.swift in Sources */, 5E7C745C725F3F34037DCC68 /* SetTransferTokensCardExpiryDateViewControllerViewModel.swift in Sources */,
5E7C74B5796FB59C8427C7A0 /* GenerateTransferMagicLinkViewController.swift in Sources */, 5E7C74B5796FB59C8427C7A0 /* GenerateTransferMagicLinkViewController.swift in Sources */,
87620026266E07490059B05A /* ThreadSafeDictionary.swift in Sources */,
5E7C76696EF7F27EC0788CDD /* GenerateTransferMagicLinkViewControllerViewModel.swift in Sources */, 5E7C76696EF7F27EC0788CDD /* GenerateTransferMagicLinkViewControllerViewModel.swift in Sources */,
5E7C75E5C64619ABFD246183 /* TransferTokensCardViaWalletAddressViewController.swift in Sources */, 5E7C75E5C64619ABFD246183 /* TransferTokensCardViaWalletAddressViewController.swift in Sources */,
5E7C7EAEBB435F3909DA36FB /* TransferTokensCardViaWalletAddressViewControllerViewModel.swift in Sources */, 5E7C7EAEBB435F3909DA36FB /* TransferTokensCardViaWalletAddressViewControllerViewModel.swift in Sources */,
@ -5141,6 +5173,7 @@
5E7C729D43F311810652B1D5 /* UIActivityViewController.swift in Sources */, 5E7C729D43F311810652B1D5 /* UIActivityViewController.swift in Sources */,
87E2555624F52EAA00F025F7 /* GasSpeedTableViewHeaderViewModel.swift in Sources */, 87E2555624F52EAA00F025F7 /* GasSpeedTableViewHeaderViewModel.swift in Sources */,
5E7C7BC8F2E31F4E2BA534D9 /* Ether.swift in Sources */, 5E7C7BC8F2E31F4E2BA534D9 /* Ether.swift in Sources */,
87620028266E14A80059B05A /* PopularTokenViewCell.swift in Sources */,
5E7C70CF1C732CE07D074A8B /* BookmarksStore.swift in Sources */, 5E7C70CF1C732CE07D074A8B /* BookmarksStore.swift in Sources */,
5E7C7E2E47ED7EDD5C127D1D /* HistoryStore.swift in Sources */, 5E7C7E2E47ED7EDD5C127D1D /* HistoryStore.swift in Sources */,
5E7C7FC76A025AD91D57B960 /* HistoriesViewModel.swift in Sources */, 5E7C7FC76A025AD91D57B960 /* HistoriesViewModel.swift in Sources */,
@ -5180,6 +5213,7 @@
87CA8495253DDFF200BF8443 /* TransitionButton.swift in Sources */, 87CA8495253DDFF200BF8443 /* TransitionButton.swift in Sources */,
5E7C7582387C12CE25D7FE78 /* TokenListFormatTableViewCellWithCheckbox.swift in Sources */, 5E7C7582387C12CE25D7FE78 /* TokenListFormatTableViewCellWithCheckbox.swift in Sources */,
5E7C7D5D3FBA199DB0449776 /* TokenListFormatRowView.swift in Sources */, 5E7C7D5D3FBA199DB0449776 /* TokenListFormatRowView.swift in Sources */,
8762002C266E150B0059B05A /* WalletTokenViewCellViewModel.swift in Sources */,
8795994526049EF8006722B2 /* ActivityStateView.swift in Sources */, 8795994526049EF8006722B2 /* ActivityStateView.swift in Sources */,
5E7C7BFE9C8CAA3E204B1FAA /* TokenRowView.swift in Sources */, 5E7C7BFE9C8CAA3E204B1FAA /* TokenRowView.swift in Sources */,
5E7C7ED4612686DAD9B9D093 /* TokensCardViewControllerTitleHeader.swift in Sources */, 5E7C7ED4612686DAD9B9D093 /* TokensCardViewControllerTitleHeader.swift in Sources */,
@ -5413,6 +5447,7 @@
5E7C775B971EFCCEBE9B10A4 /* UITableViewCell+TokenCell.swift in Sources */, 5E7C775B971EFCCEBE9B10A4 /* UITableViewCell+TokenCell.swift in Sources */,
5E7C710D1522AFA533B0C22B /* XMLHandler+XPaths.swift in Sources */, 5E7C710D1522AFA533B0C22B /* XMLHandler+XPaths.swift in Sources */,
5E7C782069BAD4A667543979 /* SettingsViewController.swift in Sources */, 5E7C782069BAD4A667543979 /* SettingsViewController.swift in Sources */,
8762002A266E14C10059B05A /* WalletTokenViewCell.swift in Sources */,
5E7C750721DA2745432B1857 /* TokenObject+UI.swift in Sources */, 5E7C750721DA2745432B1857 /* TokenObject+UI.swift in Sources */,
5E7C74B615A84B248D3BE76C /* TokenImageView.swift in Sources */, 5E7C74B615A84B248D3BE76C /* TokenImageView.swift in Sources */,
878EE954255BFFB9000210DE /* FeedbackGenerator.swift in Sources */, 878EE954255BFFB9000210DE /* FeedbackGenerator.swift in Sources */,
@ -5447,6 +5482,7 @@
5E7C79F491F80EDF6E6B9234 /* GasNowGasPriceEstimator.swift in Sources */, 5E7C79F491F80EDF6E6B9234 /* GasNowGasPriceEstimator.swift in Sources */,
5E7C7B7FDCF8EFC3083A147E /* GasNowPriceEstimates.swift in Sources */, 5E7C7B7FDCF8EFC3083A147E /* GasNowPriceEstimates.swift in Sources */,
5E7C7C16DFF078FE84AB4903 /* TransactionConfigurations.swift in Sources */, 5E7C7C16DFF078FE84AB4903 /* TransactionConfigurations.swift in Sources */,
874BD2BB2669F65800E62E02 /* PopularTokensCollection.swift in Sources */,
8757E5DE25DD231600812392 /* SignatureConfirmationConfirmationViewModel.swift in Sources */, 8757E5DE25DD231600812392 /* SignatureConfirmationConfirmationViewModel.swift in Sources */,
5E7C7CA66F3086000BA3E72C /* GasEstimates.swift in Sources */, 5E7C7CA66F3086000BA3E72C /* GasEstimates.swift in Sources */,
5E7C766B96C59C7893CF2CB4 /* EditedTransactionConfiguration.swift in Sources */, 5E7C766B96C59C7893CF2CB4 /* EditedTransactionConfiguration.swift in Sources */,

@ -65,6 +65,30 @@ class FilterTokensCoordinator {
return filteredTokens return filteredTokens
} }
func filterTokens(tokens: [PopularToken], walletTokens: [TokenObject], filter: WalletFilter) -> [PopularToken] {
var filteredTokens: [PopularToken] = tokens.filter { token in
!walletTokens.contains(where: { $0.contractAddress.sameContract(as: token.contractAddress) }) && !token.name.isEmpty
}
switch filter {
case .all:
break //no-op
case .type, .currencyOnly, .assetsOnly, .collectiblesOnly:
filteredTokens = []
case .keyword(let keyword):
let lowercasedKeyword = keyword.trimmed.lowercased()
if lowercasedKeyword.isEmpty {
break //no-op
} else {
filteredTokens = filteredTokens.filter {
return $0.name.trimmed.lowercased().contains(lowercasedKeyword)
}
}
}
return filteredTokens
}
func sortDisplayedTokens(tokens: [TokenObject]) -> [TokenObject] { func sortDisplayedTokens(tokens: [TokenObject]) -> [TokenObject] {
let nativeCryptoAddressInDatabase = Constants.nativeCryptoAddressInDatabase.eip55String let nativeCryptoAddressInDatabase = Constants.nativeCryptoAddressInDatabase.eip55String

@ -0,0 +1,130 @@
//
// PopularTokensCollection.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 04.06.2021.
//
import UIKit
import PromiseKit
struct JSONCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
struct PopularToken: Decodable {
private enum AnyError: Error {
case invalid
}
var contractAddress: AlphaWallet.Address
var server: RPCServer
var name: String
var iconImage: Subscribable<TokenImage> {
return TokenImageFetcher.instance.image(contractAddress: contractAddress, server: server, name: name)
}
enum CodingKeys: String, CodingKey {
case address
case server = "network"
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let address = AlphaWallet.Address(uncheckedAgainstNullAddress: try container.decode(String.self, forKey: .address)) {
contractAddress = address
} else {
throw AnyError.invalid
}
name = container.decode(String.self, forKey: .name, defaultValue: "")
server = RPCServer(chainID: try container.decode(Int.self, forKey: .server))
}
}
enum WalletOrPopularToken {
case walletToken(TokenObject)
case popularToken(PopularToken)
}
protocol PopularTokensCollectionType: class {
func fetchTokens() -> Promise<[PopularToken]>
}
class PopularTokensCollection: NSObject, PopularTokensCollectionType {
private let tokensURL: URL = URL(string: "https://raw.githubusercontent.com/AlphaWallet/alpha-wallet-android/fa86b477586929f61e7fefefc6a9c70de91de1f0/app/src/main/assets/known_contract.json")!
private let queue = DispatchQueue.global()
private static var cache: [PopularToken]? = .none
func fetchTokens() -> Promise<[PopularToken]> {
if let cache = Self.cache {
return .value(cache)
} else {
return Promise { seal in
queue.async {
do {
let data = try Data(contentsOf: self.tokensURL, options: .alwaysMapped)
let respose = try JSONDecoder().decode(PopularTokenList.self, from: data)
Self.cache = respose.tokens
seal.fulfill(respose.tokens)
} catch {
seal.reject(error)
}
}
}
}
}
}
class LocalPopularTokensCollection: NSObject, PopularTokensCollectionType {
private let tokensURL: URL = URL(fileURLWithPath: Bundle.main.path(forResource: "known_contract", ofType: "json")!)
private let queue = DispatchQueue.global()
private static var cache: [PopularToken]? = .none
func fetchTokens() -> Promise<[PopularToken]> {
if let cache = Self.cache {
return .value(cache)
} else {
return Promise { seal in
queue.async {
do {
let data = try Data(contentsOf: self.tokensURL, options: .alwaysMapped)
let respose = try JSONDecoder().decode(PopularTokenList.self, from: data)
Self.cache = respose.tokens
seal.fulfill(respose.tokens)
} catch {
seal.reject(error)
}
}
}
}
}
}
private struct PopularTokenList: Decodable {
var tokens: [PopularToken] = []
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: JSONCodingKeys.self)
for key in container.allKeys {
tokens.append(contentsOf: try container.decode([PopularToken].self, forKey: key))
}
}
}

@ -0,0 +1,111 @@
{
"MainNet":
[
{"address": "0x056fd409e1d7a124bd7017459dfea2f387b6d5cd", "name": "Gemini dollar", "isPopular": true, "network": 1},
{"address": "0x8e870d67f660d95d5be530380d0ec0bd388289e1", "name": "Paxos Standard", "isPopular": true, "network": 1},
{"address": "0x0000000000085d4780B73119b644AE5ecd22b376", "name": "TrueUSD", "isPopular": true, "network": 1},
{"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "name": "USD Coin", "isPopular": true, "network": 1},
{"address": "0xa74476443119A942dE498590Fe1f2454d7D4aC0d", "name": "Golem Network Token", "isPopular": true, "network": 1},
{"address": "0x98f2ab72198f2e64527bdb28931f60c0f77ac2fc", "name": "USDO", "isPopular": true, "network": 1},
{"address": "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359", "name": "Dai Stablecoin v1.0", "isPopular": true, "network": 1},
{"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "name": "USDT", "isPopular": true, "network": 1},
{"address": "0x514910771AF9Ca656af840dff83E8264EcF986CA", "name": "LINK", "isPopular": true, "network": 1},
{"address": "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", "name": "BNB", "isPopular": true, "network": 1},
{"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "name": "WETH", "isPopular": true, "network": 1},
{"address": "0xB6eD7644C69416d67B522e20bC294A9a9B405B31", "name": "0xBTC", "isPopular": true, "network": 1},
{"address": "0xc11d0D5b9e8d7741289e78a52b9D2eFBCEC14478", "name": "aDAI", "isPopular": true, "network": 1},
{"address": "0x737F98AC8cA59f2C68aD658E3C3d8C8963E40a4c", "name": "AMN", "isPopular": true, "network": 1},
{"address": "0xD46bA6D942050d489DBd938a2C909A5d5039A161", "name": "AMPL", "isPopular": true, "network": 1},
{"address": "0xcD62b1C403fa761BAadFC74C525ce2B51780b184", "name": "ANJ", "isPopular": true, "network": 1},
{"address": "0x960b236A07cf122663c4303350609A66A7B288C0", "name": "ANT", "isPopular": true, "network": 1},
{"address": "0x27054b13b1B798B345b591a4d22e6562d47eA75a", "name": "AST", "isPopular": true, "network": 1},
{"address": "0xba100000625a3754423978a60c9317c58a424e3D", "name": "BAL", "isPopular": true, "network": 1},
{"address": "0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55", "name": "BAND", "isPopular": true, "network": 1},
{"address": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", "name": "BAT", "isPopular": true, "network": 1},
{"address": "0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e", "name": "BLT", "isPopular": true, "network": 1},
{"address": "0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C", "name": "BNT", "isPopular": true, "network": 1},
{"address": "0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd", "name": "BTC++", "isPopular": true, "network": 1},
{"address": "0x56d811088235F11C8920698a204A5010a788f4b3", "name": "BZRX", "isPopular": true, "network": 1},
{"address": "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", "name": "cDAI", "isPopular": true, "network": 1},
{"address": "0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d", "name": "CEL", "isPopular": true, "network": 1},
{"address": "0x4F9254C83EB525f9FCf346490bbb3ed28a81C667", "name": "CELR", "isPopular": true, "network": 1},
{"address": "0x06AF07097C9Eeb7fD685c692751D5C66dB49c215", "name": "CHAI", "isPopular": true, "network": 1},
{"address": "0x39AA39c021dfbaE8faC545936693aC917d5E7563", "name": "cUSDC", "isPopular": true, "network": 1},
{"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", "name": "DAI", "isPopular": true, "network": 1},
{"address": "0x1B5f21ee98eed48d292e8e2d3Ed82b40a9728A22", "name": "DATA", "isPopular": true, "network": 1},
{"address": "0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A", "name": "DGD", "isPopular": true, "network": 1},
{"address": "0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF", "name": "DGX", "isPopular": true, "network": 1},
{"address": "0x97af10D3fc7C70F67711Bf715d8397C6Da79C1Ab", "name": "DIP", "isPopular": true, "network": 1},
{"address": "0xC03238A3cb7CA6580f3a89B32AFaC1bcFF87CaE4", "name": "DONUT", "isPopular": true, "network": 1},
{"address": "0xa689DCEA8f7ad59fb213be4bc624ba5500458dC6", "name": "EBASE", "isPopular": true, "network": 1},
{"address": "0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c", "name": "ENJ", "isPopular": true, "network": 1},
{"address": "0x493C57C4763932315A328269E1ADaD09653B9081", "name": "iDAI", "isPopular": true, "network": 1},
{"address": "0x6fB3e0A217407EFFf7Ca062D46c26E5d60a14d69", "name": "IOTX", "isPopular": true, "network": 1},
{"address": "0x14094949152EDDBFcd073717200DA82fEd8dC960", "name": "iSAI", "isPopular": true, "network": 1},
{"address": "0x4CC19356f2D37338b9802aa8E8fc58B0373296E7", "name": "KEY", "isPopular": true, "network": 1},
{"address": "0xdd974D5C2e2928deA5F71b9825b8b646686BD200", "name": "KNC", "isPopular": true, "network": 1},
{"address": "0x80fB784B7eD66730e8b1DBd9820aFD29931aab03", "name": "LEND", "isPopular": true, "network": 1},
{"address": "0x514910771AF9Ca656af840dff83E8264EcF986CA", "name": "LINK", "isPopular": true, "network": 1},
{"address": "0xa3d58c4E56fedCae3a7c43A725aeE9A71F0ece4e", "name": "MET", "isPopular": true, "network": 1},
{"address": "0x1B941DEd58267a06f4Ab028b446933e578389DAF", "name": "MGN", "isPopular": true, "network": 1},
{"address": "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", "name": "MKR", "isPopular": true, "network": 1},
{"address": "0xec67005c4E498Ec7f55E092bd1d35cbC47C91892", "name": "MLN", "isPopular": true, "network": 1},
{"address": "0x957c30aB0426e0C93CD8241E2c60392d08c6aC8e", "name": "MOD", "isPopular": true, "network": 1},
{"address": "0xa3BeD4E1c75D00fa6f4E5E6922DB7261B5E9AcD2", "name": "MTA", "isPopular": true, "network": 1},
{"address": "0xe2f2a5C287993345a840Db3B0845fbC70f5935a5", "name": "mUSD", "isPopular": true, "network": 1},
{"address": "0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206", "name": "NEXO", "isPopular": true, "network": 1},
{"address": "0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671", "name": "NMR", "isPopular": true, "network": 1},
{"address": "0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6", "name": "RCN", "isPopular": true, "network": 1},
{"address": "0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6", "name": "RDN", "isPopular": true, "network": 1},
{"address": "0x408e41876cCCDC0F92210600ef50372656052a38", "name": "REN", "isPopular": true, "network": 1},
{"address": "0x459086F2376525BdCebA5bDDA135e4E9d3FeF5bf", "name": "renBCH", "isPopular": true, "network": 1},
{"address": "0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D", "name": "renBTC", "isPopular": true, "network": 1},
{"address": "0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2", "name": "renZEC", "isPopular": true, "network": 1},
{"address": "0xE94327D07Fc17907b4DB788E5aDf2ed424adDff6", "name": "REP", "isPopular": true, "network": 1},
{"address": "0x221657776846890989a759BA2973e427DfF5C9bB", "name": "REPv2", "isPopular": true, "network": 1},
{"address": "0x9469D013805bFfB7D3DEBe5E7839237e535ec483", "name": "RING", "isPopular": true, "network": 1},
{"address": "0xeEAE80e1790c63E390cFB176536D734c28828192", "name": "SOCKS", "isPopular": true, "network": 1},
{"address": "0x42d6622deCe394b54999Fbd73D108123806f6a18", "name": "SPANK", "isPopular": true, "network": 1},
{"address": "0x476c5E26a75bd202a9683ffD34359C0CC15be0fF", "name": "SRM", "isPopular": true, "network": 1},
{"address": "0x0Ae055097C6d159879521C384F1D2123D1f195e6", "name": "STAKE", "isPopular": true, "network": 1},
{"address": "0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC", "name": "STORJ", "isPopular": true, "network": 1},
{"address": "0x57Ab1ec28D129707052df4dF418D58a2D46d5f51", "name": "sUSD", "isPopular": true, "network": 1},
{"address": "0x261EfCdD24CeA98652B9700800a13DfBca4103fF", "name": "sXAU", "isPopular": true, "network": 1},
{"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "name": "USDC", "isPopular": true, "network": 1},
{"address": "0x098fEEd90F28493e02f6e745a2767120E7B79A1B", "name": "USDS", "isPopular": true, "network": 1},
{"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "name": "USDT", "isPopular": true, "network": 1},
{"address": "0xeb269732ab75A6fD61Ea60b06fE994cD32a83549", "name": "USDx", "isPopular": true, "network": 1},
{"address": "0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374", "name": "VERI", "isPopular": true, "network": 1},
{"address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", "name": "WBTC", "isPopular": true, "network": 1},
{"address": "0xb69EfF754380AC7C68ffeE174b881A39dae2f58C", "name": "WCK", "isPopular": true, "network": 1},
{"address": "0xB4272071eCAdd69d933AdcD19cA99fe80664fc08", "name": "XCHF", "isPopular": true, "network": 1},
{"address": "0xa45Eaf6d2Ce4d1a67381d5588B865457023c23A0", "name": "XIO", "isPopular": true, "network": 1},
{"address": "0xE41d2489571d322189246DaFA5ebDe1F4699F498", "name": "ZRX", "isPopular": true, "network": 1},
{"address": "0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0", "name": "", "isPopular": true, "network": 1},
{"address": "0xf230b790e05390fc8295f4d3f60332c93bed42e2", "name": "Tronix", "isPopular": true, "network": 1},
{"address": "0xd850942ef8811f2a866692a623011bde52a462c1", "name": "VeChain Token", "isPopular": true, "network": 1},
{"address": "0xb5a5f22694352c15b00323844ad545abb2b11028", "name": "", "isPopular": true, "network": 1},
{"address": "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07", "name": "OMGToken", "isPopular": true, "network": 1},
{"address": "0x05f4a42e251f2d52b8ed15e9fedaacfcef1fad27", "name": "Zilliqa", "isPopular": true, "network": 1},
{"address": "0xcb97e65f07da24d46bcdd078ebebd7c6e6e3d750", "name": "Bytom", "isPopular": true, "network": 1},
{"address": "0xd4fa1460f537bb9085d22c7bccb5dd450ef28e3a", "name": "Populous Platform", "isPopular": true, "network": 1},
{"address": "0x168296bb09e24a88805cb9c33356536b980d3fc5", "name": "RHOC", "isPopular": true, "network": 1},
{"address": "0x744d70fdbe2ba4cf95131626614a1763df805b9e", "name": "Status Network Token", "isPopular": true, "network": 1},
{"address": "0x4CEdA7906a5Ed2179785Cd3A40A69ee8bc99C466", "name": "AION", "isPopular": true, "network": 1},
{"address": "0xEF68e7C694F40c8202821eDF525dE3782458639f", "name": "", "isPopular": true, "network": 1},
{"address": "0x5ca9a71b1d01849c0a95490cc00559717fcf0d1d", "name": "Aeternity", "isPopular": true, "network": 1}
],
"xDAI":
[
{"address": "0x6a814843de5967cf94d7720ce15cba8b0da81967", "name": "BuffiDai", "isPopular": false, "network": 100},
{"address": "0x94819805310cf736198df0de856b0ff5584f0903", "name": "Burner", "isPopular": false, "network": 100},
{"address": "0xdec31651bec1fbbff392aa7de956d6ee4559498b", "name": "Burner", "isPopular": false, "network": 100},
{"address": "0xa95d505e6933cb790ed3431805871efe4e6bbafd", "name": "Burner", "isPopular": false, "network": 100},
{"address": "0xbdc3df563a3959a373916b724c683d69ba4097f7", "name": "DenDai", "isPopular": false, "network": 100},
{"address": "0x6e251ee9cadf0145babfd3b64664a9d7f941fcc3", "name": "DenDai", "isPopular": false, "network": 100},
{"address": "0x3e50bf6703fc132a94e4baff068db2055655f11b", "name": "buffiDai", "isPopular": false, "network": 100},
{"address": "0xa16b70e8fad839e62abba2d962e4ca5a28af9e76", "name": "ETHDenver 2019", "isPopular": false, "network": 100}
]
}

@ -0,0 +1,38 @@
//
// ThreadSafeDictionary.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 27.05.2021.
//
import UIKit
class ThreadSafeDictionary<Key: Hashable, Value> {
fileprivate var cache = [Key: Value]()
private let queue = DispatchQueue(label: "SynchronizedArrayAccess", attributes: .concurrent)
subscript(server: Key) -> Value? {
get {
var element: Value?
queue.sync {
element = cache[server]
}
return element
}
set {
queue.async(flags: .barrier) {
self.cache[server] = newValue
}
}
}
func removeAll() {
queue.async(flags: .barrier) {
self.cache.removeAll()
}
}
var value: [Key: Value] {
return cache
}
}

@ -8,7 +8,7 @@ extension UIView {
return .init() return .init()
} }
static var tokenSymbolBackgroundImageCache: [UIColor: UIImage] = .init() static var tokenSymbolBackgroundImageCache: ThreadSafeDictionary<UIColor, UIImage> = .init()
static func tokenSymbolBackgroundImage(backgroundColor: UIColor) -> UIImage { static func tokenSymbolBackgroundImage(backgroundColor: UIColor) -> UIImage {
if let cachedValue = tokenSymbolBackgroundImageCache[backgroundColor] { if let cachedValue = tokenSymbolBackgroundImageCache[backgroundColor] {
return cachedValue return cachedValue

@ -139,6 +139,7 @@
"addHideTokens.section.newTokens" = "New Token Found"; "addHideTokens.section.newTokens" = "New Token Found";
"addHideTokens.section.displayedTokens" = "Displayed Tokens"; "addHideTokens.section.displayedTokens" = "Displayed Tokens";
"addHideTokens.section.hiddenTokens" = "Hidden Tokens"; "addHideTokens.section.hiddenTokens" = "Hidden Tokens";
"addHideTokens.section.popularTokens" = "Popular Tokens";
"transactions.tabbar.item.title" = "Transactions"; "transactions.tabbar.item.title" = "Transactions";
"transactions.received.ether" = "You have received %@ %@"; "transactions.received.ether" = "You have received %@ %@";
"transactions.received.ether.notification.prompt" = "Allow Notifications When You Receive %@?"; "transactions.received.ether.notification.prompt" = "Allow Notifications When You Receive %@?";

@ -139,6 +139,7 @@
"addHideTokens.section.newTokens" = "Nuevo token encontrado"; "addHideTokens.section.newTokens" = "Nuevo token encontrado";
"addHideTokens.section.displayedTokens" = "Tokens mostrados"; "addHideTokens.section.displayedTokens" = "Tokens mostrados";
"addHideTokens.section.hiddenTokens" = "Tokens ocultos"; "addHideTokens.section.hiddenTokens" = "Tokens ocultos";
"addHideTokens.section.popularTokens" = "Popular Tokens";
"transactions.tabbar.item.title" = "Transacciones"; "transactions.tabbar.item.title" = "Transacciones";
"transactions.received.ether" = "Has recibido %@ %@"; "transactions.received.ether" = "Has recibido %@ %@";
"transactions.received.ether.notification.prompt" = "¿Permitir notificaciones al recibir %@?"; "transactions.received.ether.notification.prompt" = "¿Permitir notificaciones al recibir %@?";

@ -139,6 +139,7 @@
"addHideTokens.section.newTokens" = "New Token Found"; "addHideTokens.section.newTokens" = "New Token Found";
"addHideTokens.section.displayedTokens" = "Displayed Tokens"; "addHideTokens.section.displayedTokens" = "Displayed Tokens";
"addHideTokens.section.hiddenTokens" = "Hidden Tokens"; "addHideTokens.section.hiddenTokens" = "Hidden Tokens";
"addHideTokens.section.popularTokens" = "Popular Tokens";
"transactions.tabbar.item.title" = "私のトランザクション"; "transactions.tabbar.item.title" = "私のトランザクション";
"transactions.received.ether" = "You have received %@ %@"; "transactions.received.ether" = "You have received %@ %@";
"transactions.received.ether.notification.prompt" = "Allow Notifications When You Receive %@?"; "transactions.received.ether.notification.prompt" = "Allow Notifications When You Receive %@?";

@ -138,6 +138,7 @@
"addHideTokens.section.newTokens" = "New Token Found"; "addHideTokens.section.newTokens" = "New Token Found";
"addHideTokens.section.displayedTokens" = "Displayed Tokens"; "addHideTokens.section.displayedTokens" = "Displayed Tokens";
"addHideTokens.section.hiddenTokens" = "Hidden Tokens"; "addHideTokens.section.hiddenTokens" = "Hidden Tokens";
"addHideTokens.section.popularTokens" = "Popular Tokens";
"transactions.tabbar.item.title" = "내 거래"; "transactions.tabbar.item.title" = "내 거래";
"transactions.received.ether" = "You have received %@ %@"; "transactions.received.ether" = "You have received %@ %@";
"transactions.received.ether.notification.prompt" = "Allow Notifications When You Receive %@?"; "transactions.received.ether.notification.prompt" = "Allow Notifications When You Receive %@?";

@ -139,6 +139,7 @@
"addHideTokens.section.newTokens" = "找到新通证"; "addHideTokens.section.newTokens" = "找到新通证";
"addHideTokens.section.displayedTokens" = "显示的通证"; "addHideTokens.section.displayedTokens" = "显示的通证";
"addHideTokens.section.hiddenTokens" = "隐藏的通证"; "addHideTokens.section.hiddenTokens" = "隐藏的通证";
"addHideTokens.section.popularTokens" = "Popular Tokens";
"transactions.tabbar.item.title" = "我的账单"; "transactions.tabbar.item.title" = "我的账单";
"transactions.received.ether" = "您已收到%@ %@"; "transactions.received.ether" = "您已收到%@ %@";
"transactions.received.ether.notification.prompt" = "是否在收到%@时允许通知?"; "transactions.received.ether.notification.prompt" = "是否在收到%@时允许通知?";

@ -17,7 +17,6 @@ class AddHideTokensCoordinator: Coordinator {
private var viewModel: AddHideTokensViewModel private var viewModel: AddHideTokensViewModel
private lazy var viewController: AddHideTokensViewController = .init( private lazy var viewController: AddHideTokensViewController = .init(
viewModel: viewModel, viewModel: viewModel,
sessions: sessions,
assetDefinitionStore: assetDefinitionStore assetDefinitionStore: assetDefinitionStore
) )
private let tokenCollection: TokenCollection private let tokenCollection: TokenCollection
@ -26,15 +25,16 @@ class AddHideTokensCoordinator: Coordinator {
private let assetDefinitionStore: AssetDefinitionStore private let assetDefinitionStore: AssetDefinitionStore
private let singleChainTokenCoordinators: [SingleChainTokenCoordinator] private let singleChainTokenCoordinators: [SingleChainTokenCoordinator]
private let config: Config private let config: Config
private let popularTokensCollection: PopularTokensCollectionType = LocalPopularTokensCollection()
var coordinators: [Coordinator] = [] var coordinators: [Coordinator] = []
weak var delegate: AddHideTokensCoordinatorDelegate? weak var delegate: AddHideTokensCoordinatorDelegate?
private var tokens: [TokenObject]
init(tokens: [TokenObject], assetDefinitionStore: AssetDefinitionStore, filterTokensCoordinator: FilterTokensCoordinator, tickers: [AddressAndRPCServer: CoinTicker], sessions: ServerDictionary<WalletSession>, analyticsCoordinator: AnalyticsCoordinator, navigationController: UINavigationController, tokenCollection: TokenCollection, config: Config, singleChainTokenCoordinators: [SingleChainTokenCoordinator]) { init(tokens: [TokenObject], assetDefinitionStore: AssetDefinitionStore, filterTokensCoordinator: FilterTokensCoordinator, tickers: [AddressAndRPCServer: CoinTicker], sessions: ServerDictionary<WalletSession>, analyticsCoordinator: AnalyticsCoordinator, navigationController: UINavigationController, tokenCollection: TokenCollection, config: Config, singleChainTokenCoordinators: [SingleChainTokenCoordinator]) {
self.config = config self.config = config
self.filterTokensCoordinator = filterTokensCoordinator self.filterTokensCoordinator = filterTokensCoordinator
self.sessions = sessions self.sessions = sessions
self.tokens = tokens
self.analyticsCoordinator = analyticsCoordinator self.analyticsCoordinator = analyticsCoordinator
self.navigationController = navigationController self.navigationController = navigationController
self.tokenCollection = tokenCollection self.tokenCollection = tokenCollection
@ -42,14 +42,18 @@ class AddHideTokensCoordinator: Coordinator {
self.singleChainTokenCoordinators = singleChainTokenCoordinators self.singleChainTokenCoordinators = singleChainTokenCoordinators
self.viewModel = AddHideTokensViewModel( self.viewModel = AddHideTokensViewModel(
tokens: tokens, tokens: tokens,
tickers: tickers, filterTokensCoordinator: filterTokensCoordinator,
filterTokensCoordinator: filterTokensCoordinator singleChainTokenCoordinators: singleChainTokenCoordinators
) )
} }
func start() { func start() {
viewController.delegate = self viewController.delegate = self
navigationController.pushViewController(viewController, animated: true) navigationController.pushViewController(viewController, animated: true)
popularTokensCollection.fetchTokens().done { [weak self] tokens in
self?.viewController.add(popularTokens: tokens)
}.cauterize()
} }
@objc func dismiss() { @objc func dismiss() {

@ -147,7 +147,7 @@ class SingleChainTokenCoordinator: Coordinator {
DispatchQueue.global().async { [weak self] in DispatchQueue.global().async { [weak self] in
guard let strongSelf = self else { return } guard let strongSelf = self else { return }
for eachContract in contractsToAdd { for eachContract in contractsToAdd {
strongSelf.addToken(for: eachContract) { strongSelf.addToken(for: eachContract) { _ in
contractsPulled += 1 contractsPulled += 1
if contractsPulled == contractsToAdd.count { if contractsPulled == contractsToAdd.count {
hasRefreshedAfterAddingAllContracts = true hasRefreshedAfterAddingAllContracts = true
@ -231,7 +231,7 @@ class SingleChainTokenCoordinator: Coordinator {
switch result { switch result {
case .success(let balance): case .success(let balance):
if !balance.isEmpty { if !balance.isEmpty {
strongSelf.addToken(for: each) { strongSelf.addToken(for: each) { _ in
DispatchQueue.main.async { DispatchQueue.main.async {
strongSelf.delegate?.tokensDidChange(inCoordinator: strongSelf) strongSelf.delegate?.tokensDidChange(inCoordinator: strongSelf)
} }
@ -258,7 +258,7 @@ class SingleChainTokenCoordinator: Coordinator {
switch result { switch result {
case .success(let balance): case .success(let balance):
if balance > 0 { if balance > 0 {
strongSelf.addToken(for: each) { strongSelf.addToken(for: each) { _ in
DispatchQueue.main.async { DispatchQueue.main.async {
strongSelf.delegate?.tokensDidChange(inCoordinator: strongSelf) strongSelf.delegate?.tokensDidChange(inCoordinator: strongSelf)
} }
@ -286,7 +286,7 @@ class SingleChainTokenCoordinator: Coordinator {
} }
} }
private func addToken(for contract: AlphaWallet.Address, onlyIfThereIsABalance: Bool = false, completion: @escaping () -> Void) { private func addToken(for contract: AlphaWallet.Address, onlyIfThereIsABalance: Bool = false, completion: @escaping (TokenObject?) -> Void) {
fetchContractData(for: contract) { [weak self] data in fetchContractData(for: contract) { [weak self] data in
guard let strongSelf = self else { return } guard let strongSelf = self else { return }
switch data { switch data {
@ -303,8 +303,8 @@ class SingleChainTokenCoordinator: Coordinator {
type: tokenType, type: tokenType,
balance: balance balance: balance
) )
strongSelf.storage.addCustom(token: token) let value = strongSelf.storage.addCustom(token: token)
completion() completion(value)
case .fungibleTokenComplete(let name, let symbol, let decimals): case .fungibleTokenComplete(let name, let symbol, let decimals):
//We re-use the existing balance value to avoid the Wallets tab showing that token (if it already exist) as balance = 0 momentarily //We re-use the existing balance value to avoid the Wallets tab showing that token (if it already exist) as balance = 0 momentarily
let value = strongSelf.storage.enabledObject.first(where: { $0.contractAddress == contract })?.value ?? "0" let value = strongSelf.storage.enabledObject.first(where: { $0.contractAddress == contract })?.value ?? "0"
@ -318,16 +318,16 @@ class SingleChainTokenCoordinator: Coordinator {
value: value, value: value,
type: .erc20 type: .erc20
) )
strongSelf.storage.add(tokens: [token]) let value2 = strongSelf.storage.add(tokens: [token])[0]
completion() completion(value2)
case .delegateTokenComplete: case .delegateTokenComplete:
strongSelf.storage.add(delegateContracts: [DelegateContract(contractAddress: contract, server: strongSelf.server)]) strongSelf.storage.add(delegateContracts: [DelegateContract(contractAddress: contract, server: strongSelf.server)])
completion() completion(.none)
case .failed(let networkReachable): case .failed(let networkReachable):
if let networkReachable = networkReachable, networkReachable { if let networkReachable = networkReachable, networkReachable {
strongSelf.storage.add(deadContracts: [DeletedContract(contractAddress: contract, server: strongSelf.server)]) strongSelf.storage.add(deadContracts: [DeletedContract(contractAddress: contract, server: strongSelf.server)])
} }
completion() completion(.none)
} }
} }
} }
@ -335,12 +335,31 @@ class SingleChainTokenCoordinator: Coordinator {
//Adding a token may fail if we lose connectivity while fetching the contract details (e.g. name and balance). So we remove the contract from the hidden list (if it was there) so that the app has the chance to add it automatically upon auto detection at startup //Adding a token may fail if we lose connectivity while fetching the contract details (e.g. name and balance). So we remove the contract from the hidden list (if it was there) so that the app has the chance to add it automatically upon auto detection at startup
func addImportedToken(forContract contract: AlphaWallet.Address, onlyIfThereIsABalance: Bool = false) { func addImportedToken(forContract contract: AlphaWallet.Address, onlyIfThereIsABalance: Bool = false) {
delete(hiddenContract: contract) delete(hiddenContract: contract)
addToken(for: contract, onlyIfThereIsABalance: onlyIfThereIsABalance) { [weak self] in addToken(for: contract, onlyIfThereIsABalance: onlyIfThereIsABalance) { [weak self] _ in
guard let strongSelf = self else { return } guard let strongSelf = self else { return }
strongSelf.delegate?.tokensDidChange(inCoordinator: strongSelf) strongSelf.delegate?.tokensDidChange(inCoordinator: strongSelf)
} }
} }
//Adding a token may fail if we lose connectivity while fetching the contract details (e.g. name and balance). So we remove the contract from the hidden list (if it was there) so that the app has the chance to add it automatically upon auto detection at startup
func addImportedTokenPromise(forContract contract: AlphaWallet.Address, onlyIfThereIsABalance: Bool = false) -> Promise<TokenObject> {
struct ImportTokenError: Error { }
return Promise<TokenObject> { seal in
delete(hiddenContract: contract)
addToken(for: contract, onlyIfThereIsABalance: onlyIfThereIsABalance) { [weak self] tokenObject in
guard let strongSelf = self else { return }
strongSelf.delegate?.tokensDidChange(inCoordinator: strongSelf)
if let tokenObject = tokenObject {
seal.fulfill(tokenObject)
} else {
seal.reject(ImportTokenError())
}
}
}
}
private func delete(hiddenContract contract: AlphaWallet.Address) { private func delete(hiddenContract contract: AlphaWallet.Address) {
guard let hiddenContract = storage.hiddenContracts.first(where: { contract.sameContract(as: $0.contract) }) else { return } guard let hiddenContract = storage.hiddenContracts.first(where: { contract.sameContract(as: $0.contract) }) else { return }
//TODO we need to make sure it's all uppercase? //TODO we need to make sure it's all uppercase?

@ -6,7 +6,30 @@ import BigInt
extension Activity { extension Activity {
struct AssignedToken { struct AssignedToken: Equatable {
struct TokenBalance {
var balance = "0"
var json: String = "{}"
}
enum Balance {
case value(BigInt)
case balance([TokenBalance])
case none
var isEmpty: Bool {
switch self {
case .balance(let values):
return values.isEmpty
case .value(let value):
return value.isZero
case .none:
return true
}
}
}
var primaryKey: String var primaryKey: String
var contractAddress: AlphaWallet.Address var contractAddress: AlphaWallet.Address
var symbol: String var symbol: String
@ -14,7 +37,13 @@ extension Activity {
var server: RPCServer var server: RPCServer
var icon: Subscribable<TokenImage> var icon: Subscribable<TokenImage>
var type: TokenType var type: TokenType
var name: String
var balance: Balance
var shouldDisplay: Bool
var sortIndex: Int?
init(tokenObject: TokenObject) { init(tokenObject: TokenObject) {
name = tokenObject.name
primaryKey = tokenObject.primaryKey primaryKey = tokenObject.primaryKey
server = tokenObject.server server = tokenObject.server
contractAddress = tokenObject.contractAddress contractAddress = tokenObject.contractAddress
@ -22,6 +51,102 @@ extension Activity {
decimals = tokenObject.decimals decimals = tokenObject.decimals
icon = tokenObject.icon icon = tokenObject.icon
type = tokenObject.type type = tokenObject.type
shouldDisplay = tokenObject.shouldDisplay
sortIndex = tokenObject.sortIndex.value
switch type {
case .erc20, .nativeCryptocurrency:
self.balance = .value(tokenObject.valueBigInt)
case .erc721, .erc721ForTickets, .erc875:
let balance = tokenObject.balance.map { TokenBalance(balance: $0.balance, json: $0.json) }
self.balance = .balance(Array(balance))
}
}
// init(contractAddress: AlphaWallet.Address, symbol: String, decimals: Int, server: RPCServer, type: TokenType, name: String) {
// self.name = name
// self.primaryKey = TokenObject.generatePrimaryKey(fromContract: contractAddress, server: server)
// self.server = server
// self.contractAddress = contractAddress
// self.symbol = symbol
// self.decimals = decimals
//// self.icon = tokenObject.icon
// self.type = type
// self.shouldDisplay = true
// self.sortIndex = nil
//
//// switch type {
//// case .erc20, .nativeCryptocurrency:
//// self.balance = .value(tokenObject.valueBigInt)
//// case .erc721, .erc721ForTickets, .erc875:
//// let balance = tokenObject.balance.map { TokenBalance(balance: $0.balance, json: $0.json) }
//// self.balance = .balance(Array(balance))
//// }
// }
static func == (lhs: Activity.AssignedToken, rhs: Activity.AssignedToken) -> Bool {
return lhs.primaryKey == rhs.primaryKey
}
}
}
extension Activity.AssignedToken {
func title(withAssetDefinitionStore assetDefinitionStore: AssetDefinitionStore) -> String {
let handler = XMLHandler(contract: contractAddress, tokenType: type, assetDefinitionStore: assetDefinitionStore)
let localizedNameFromAssetDefinition = handler.getLabel(fallback: name)
return title(withAssetDefinitionStore: assetDefinitionStore, localizedNameFromAssetDefinition: localizedNameFromAssetDefinition)
}
func titleInPluralForm(withAssetDefinitionStore assetDefinitionStore: AssetDefinitionStore) -> String {
let handler = XMLHandler(contract: contractAddress, tokenType: type, assetDefinitionStore: assetDefinitionStore)
let localizedNameFromAssetDefinition = handler.getNameInPluralForm(fallback: name)
return title(withAssetDefinitionStore: assetDefinitionStore, localizedNameFromAssetDefinition: localizedNameFromAssetDefinition)
}
private func title(withAssetDefinitionStore assetDefinitionStore: AssetDefinitionStore, localizedNameFromAssetDefinition: String) -> String {
let compositeName = compositeTokenName(forContract: contractAddress, fromContractName: name, localizedNameFromAssetDefinition: localizedNameFromAssetDefinition)
if compositeName.isEmpty {
return symbol
} else {
let daiSymbol = "DAI\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}"
//We could have just trimmed away all trailing \0, but this is faster and safer since only DAI seems to have this problem
if daiSymbol == symbol {
return "\(compositeName) (DAI)"
} else {
return "\(compositeName) (\(symbol))"
}
}
}
func symbolInPluralForm(withAssetDefinitionStore assetDefinitionStore: AssetDefinitionStore) -> String {
let handler = XMLHandler(contract: contractAddress, tokenType: type, assetDefinitionStore: assetDefinitionStore)
let localizedNameFromAssetDefinition = handler.getNameInPluralForm(fallback: name)
return symbol(withAssetDefinitionStore: assetDefinitionStore, localizedNameFromAssetDefinition: localizedNameFromAssetDefinition)
}
private func symbol(withAssetDefinitionStore assetDefinitionStore: AssetDefinitionStore, localizedNameFromAssetDefinition: String) -> String {
let compositeName = compositeTokenName(forContract: contractAddress, fromContractName: name, localizedNameFromAssetDefinition: localizedNameFromAssetDefinition)
if compositeName.isEmpty {
return symbol
} else {
let daiSymbol = "DAI\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}"
//We could have just trimmed away all trailing \0, but this is faster and safer since only DAI seems to have this problem
if daiSymbol == symbol {
return "DAI"
} else {
return symbol
}
}
}
var isERC721AndNotForTickets: Bool {
switch type {
case .erc721:
return true
case .nativeCryptocurrency, .erc20, .erc875, .erc721ForTickets:
return false
} }
} }
} }

@ -2,6 +2,7 @@
import UIKit import UIKit
import StatefulViewController import StatefulViewController
import PromiseKit
protocol AddHideTokensViewControllerDelegate: class { protocol AddHideTokensViewControllerDelegate: class {
func didPressAddToken( in viewController: UIViewController) func didPressAddToken( in viewController: UIViewController)
@ -12,15 +13,15 @@ protocol AddHideTokensViewControllerDelegate: class {
class AddHideTokensViewController: UIViewController { class AddHideTokensViewController: UIViewController {
private let assetDefinitionStore: AssetDefinitionStore private let assetDefinitionStore: AssetDefinitionStore
private let sessions: ServerDictionary<WalletSession> // private let sessions: ServerDictionary<WalletSession>
private var viewModel: AddHideTokensViewModel private var viewModel: AddHideTokensViewModel
private let searchController: UISearchController private let searchController: UISearchController
private var isSearchBarConfigured = false private var isSearchBarConfigured = false
private lazy var tableView: UITableView = { private lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped) let tableView = UITableView(frame: .zero, style: .grouped)
tableView.register(FungibleTokenViewCell.self) tableView.register(WalletTokenViewCell.self)
tableView.register(NonFungibleTokenViewCell.self) tableView.register(PopularTokenViewCell.self)
tableView.register(EthTokenViewCell.self) // tableView.register(EthTokenViewCell.self)
tableView.registerHeaderFooterView(AddHideTokenSectionHeaderView.self) tableView.registerHeaderFooterView(AddHideTokenSectionHeaderView.self)
tableView.isEditing = true tableView.isEditing = true
tableView.estimatedRowHeight = 100 tableView.estimatedRowHeight = 100
@ -39,9 +40,8 @@ class AddHideTokensViewController: UIViewController {
private let notificationCenter = NotificationCenter.default private let notificationCenter = NotificationCenter.default
weak var delegate: AddHideTokensViewControllerDelegate? weak var delegate: AddHideTokensViewControllerDelegate?
init(viewModel: AddHideTokensViewModel, sessions: ServerDictionary<WalletSession>, assetDefinitionStore: AssetDefinitionStore) { init(viewModel: AddHideTokensViewModel, assetDefinitionStore: AssetDefinitionStore) {
self.assetDefinitionStore = assetDefinitionStore self.assetDefinitionStore = assetDefinitionStore
self.sessions = sessions
self.viewModel = viewModel self.viewModel = viewModel
searchController = UISearchController(searchResultsController: nil) searchController = UISearchController(searchResultsController: nil)
@ -147,6 +147,14 @@ class AddHideTokensViewController: UIViewController {
viewModel.add(token: token) viewModel.add(token: token)
reload() reload()
} }
func add(popularTokens: [PopularToken]) {
viewModel.set(allPopularTokens: popularTokens)
DispatchQueue.main.async {
self.reload()
}
}
} }
extension AddHideTokensViewController: StatefulViewController { extension AddHideTokensViewController: StatefulViewController {
@ -169,43 +177,15 @@ extension AddHideTokensViewController: UITableViewDataSource {
guard let token = viewModel.item(atIndexPath: indexPath) else { return UITableViewCell() } guard let token = viewModel.item(atIndexPath: indexPath) else { return UITableViewCell() }
let isVisible = viewModel.displayedToken(indexPath: indexPath) let isVisible = viewModel.displayedToken(indexPath: indexPath)
let session = sessions[token.server] switch token {
switch token.type { case .walletToken(let tokenObject):
case .nativeCryptocurrency: let cell: WalletTokenViewCell = tableView.dequeueReusableCell(for: indexPath)
let cell: EthTokenViewCell = tableView.dequeueReusableCell(for: indexPath) cell.configure(viewModel: .init(token: tokenObject, assetDefinitionStore: assetDefinitionStore, isVisible: isVisible))
cell.configure(viewModel: .init(
token: token,
ticker: viewModel.ticker(for: token),
currencyAmount: session.balanceCoordinator.viewModel.currencyAmount,
assetDefinitionStore: assetDefinitionStore,
isVisible: isVisible
))
return cell
case .erc20:
let cell: FungibleTokenViewCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(viewModel: .init(token: token,
assetDefinitionStore: assetDefinitionStore,
isVisible: isVisible,
ticker: viewModel.ticker(for: token)
))
return cell
case .erc721, .erc721ForTickets:
let cell: NonFungibleTokenViewCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(viewModel: .init(token: token,
server: token.server,
assetDefinitionStore: assetDefinitionStore,
isVisible: isVisible
))
return cell return cell
case .erc875: case .popularToken(let value):
let cell: NonFungibleTokenViewCell = tableView.dequeueReusableCell(for: indexPath) let cell: PopularTokenViewCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(viewModel: .init( cell.configure(viewModel: .init(token: value, isVisible: isVisible))
token: token,
server: token.server,
assetDefinitionStore: assetDefinitionStore,
isVisible: isVisible
))
return cell return cell
} }
@ -223,28 +203,45 @@ extension AddHideTokensViewController: UITableViewDataSource {
} }
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
let result: (token: TokenObject, indexPathToInsert: IndexPath)? let promise: Promise<(token: TokenObject, indexPathToInsert: IndexPath, withTokenCreation: Bool)?>
let isTokenHidden: Bool let isTokenHidden: Bool
switch editingStyle { switch editingStyle {
case .insert: case .insert:
result = viewModel.addDisplayed(indexPath: indexPath) promise = viewModel.addDisplayed(indexPath: indexPath)
isTokenHidden = false isTokenHidden = false
case .delete: case .delete:
result = viewModel.deleteToken(indexPath: indexPath) promise = viewModel.deleteToken(indexPath: indexPath)
isTokenHidden = true isTokenHidden = true
case .none: case .none:
result = nil promise = .value(nil)
isTokenHidden = false isTokenHidden = false
} }
if let result = result { self.displayLoading()
delegate?.didMark(token: result.token, in: self, isHidden: isTokenHidden)
tableView.performBatchUpdates({ promise.done { [weak self] result in
tableView.deleteRows(at: [indexPath], with: .automatic) guard let strongSelf = self else { return }
tableView.insertRows(at: [result.indexPathToInsert], with: .automatic)
}, completion: nil) if let result = result, let delegate = strongSelf.delegate {
} else { delegate.didMark(token: result.token, in: strongSelf, isHidden: isTokenHidden)
//NOTE: due to a table view BatchUpdates the table view can cracs we apply flag `withTokenCreation` to determine whether we create a new token, an if we do, reload table view
if result.withTokenCreation {
tableView.reloadData()
} else {
tableView.performBatchUpdates({
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.insertRows(at: [result.indexPathToInsert], with: .automatic)
}, completion: nil)
}
} else {
tableView.reloadData()
}
}.catch { _ in
tableView.reloadData() tableView.reloadData()
self.displayError(message: "Add token failure")
}.finally {
self.hideLoading()
} }
} }
@ -252,21 +249,28 @@ extension AddHideTokensViewController: UITableViewDataSource {
let title = R.string.localizable.walletsHideTokenTitle() let title = R.string.localizable.walletsHideTokenTitle()
let hideAction = UIContextualAction(style: .destructive, title: title) { [weak self] _, _, completionHandler in let hideAction = UIContextualAction(style: .destructive, title: title) { [weak self] _, _, completionHandler in
guard let strongSelf = self else { return } guard let strongSelf = self else { return }
if let result = strongSelf.viewModel.deleteToken(indexPath: indexPath) {
strongSelf.delegate?.didMark(token: result.token, in: strongSelf, isHidden: true) strongSelf.viewModel.deleteToken(indexPath: indexPath).done { result in
tableView.performBatchUpdates({ if let result = result, let delegate = strongSelf.delegate {
tableView.deleteRows(at: [indexPath], with: .automatic) delegate.didMark(token: result.token, in: strongSelf, isHidden: true)
tableView.insertRows(at: [result.indexPathToInsert], with: .automatic)
}, completion: nil) tableView.performBatchUpdates({
completionHandler(true) tableView.deleteRows(at: [indexPath], with: .automatic)
} else { tableView.insertRows(at: [result.indexPathToInsert], with: .automatic)
tableView.reloadData() }, completion: nil)
completionHandler(false)
} completionHandler(true)
} else {
tableView.reloadData()
completionHandler(false)
}
}.cauterize()
} }
hideAction.backgroundColor = R.color.danger() hideAction.backgroundColor = R.color.danger()
hideAction.image = R.image.hideToken() hideAction.image = R.image.hideToken()
let configuration = UISwipeActionsConfiguration(actions: [hideAction]) let configuration = UISwipeActionsConfiguration(actions: [hideAction])
configuration.performsFirstActionWithFullSwipe = true configuration.performsFirstActionWithFullSwipe = true

@ -1,11 +1,13 @@
// Copyright © 2020 Stormbird PTE. LTD. // Copyright © 2020 Stormbird PTE. LTD.
import UIKit import UIKit
import PromiseKit
private enum AddHideTokenSections: Int { private enum AddHideTokenSections: Int {
case availableNewTokens case availableNewTokens
case displayedTokens case displayedTokens
case hiddenTokens case hiddenTokens
case popularTokens
var description: String { var description: String {
switch self { switch self {
@ -15,29 +17,40 @@ private enum AddHideTokenSections: Int {
return R.string.localizable.addHideTokensSectionDisplayedTokens() return R.string.localizable.addHideTokensSectionDisplayedTokens()
case .hiddenTokens: case .hiddenTokens:
return R.string.localizable.addHideTokensSectionHiddenTokens() return R.string.localizable.addHideTokensSectionHiddenTokens()
case .popularTokens:
return R.string.localizable.addHideTokensSectionPopularTokens()
} }
} }
} }
//NOTE: Changed to class to prevent update all ViewModel copies and apply updates only in one place. //NOTE: Changed to class to prevent update all ViewModel copies and apply updates only in one place.
class AddHideTokensViewModel { class AddHideTokensViewModel {
private let sections: [AddHideTokenSections] = [.displayedTokens, .hiddenTokens] private let sections: [AddHideTokenSections] = [.displayedTokens, .hiddenTokens, .popularTokens]
private let filterTokensCoordinator: FilterTokensCoordinator private let filterTokensCoordinator: FilterTokensCoordinator
private var tokens: [TokenObject] private var tokens: [TokenObject]
private var tickers: [AddressAndRPCServer: CoinTicker] private var allPopularTokens: [PopularToken] = []
private var displayedTokens: [TokenObject] = [] private var displayedTokens: [TokenObject] = []
private var hiddenTokens: [TokenObject] = [] private var hiddenTokens: [TokenObject] = []
private var popularTokens: [PopularToken] = []
var searchText: String? { var searchText: String? {
didSet { didSet {
filter(tokens: tokens) filter(tokens: tokens)
} }
} }
private let singleChainTokenCoordinators: [SingleChainTokenCoordinator]
init(tokens: [TokenObject], tickers: [AddressAndRPCServer: CoinTicker], filterTokensCoordinator: FilterTokensCoordinator) { init(tokens: [TokenObject], filterTokensCoordinator: FilterTokensCoordinator, singleChainTokenCoordinators: [SingleChainTokenCoordinator]) {
self.tickers = tickers
self.tokens = tokens self.tokens = tokens
self.filterTokensCoordinator = filterTokensCoordinator self.filterTokensCoordinator = filterTokensCoordinator
self.singleChainTokenCoordinators = singleChainTokenCoordinators
filter(tokens: tokens)
}
func set(allPopularTokens: [PopularToken]) {
self.allPopularTokens = allPopularTokens
filter(tokens: tokens) filter(tokens: tokens)
} }
@ -66,6 +79,8 @@ class AddHideTokensViewModel {
return hiddenTokens.count return hiddenTokens.count
case .availableNewTokens: case .availableNewTokens:
return 0 return 0
case .popularTokens:
return popularTokens.count
} }
} }
@ -73,9 +88,7 @@ class AddHideTokensViewModel {
switch sections[indexPath.section] { switch sections[indexPath.section] {
case .displayedTokens: case .displayedTokens:
return true return true
case .availableNewTokens: case .availableNewTokens, .popularTokens, .hiddenTokens:
return false
case .hiddenTokens:
return false return false
} }
} }
@ -88,7 +101,11 @@ class AddHideTokensViewModel {
filter(tokens: tokens) filter(tokens: tokens)
} }
func addDisplayed(indexPath: IndexPath) -> (token: TokenObject, indexPathToInsert: IndexPath)? { private func singleChainTokenCoordinator(forServer server: RPCServer) -> SingleChainTokenCoordinator? {
singleChainTokenCoordinators.first { $0.isServer(server) }
}
func addDisplayed(indexPath: IndexPath) -> Promise<(token: TokenObject, indexPathToInsert: IndexPath, withTokenCreation: Bool)?> {
switch sections[indexPath.section] { switch sections[indexPath.section] {
case .displayedTokens: case .displayedTokens:
break break
@ -97,40 +114,49 @@ class AddHideTokensViewModel {
displayedTokens.append(token) displayedTokens.append(token)
if let sectionIndex = sections.index(of: .displayedTokens) { if let sectionIndex = sections.index(of: .displayedTokens) {
return (token, IndexPath(row: max(0, displayedTokens.count - 1), section: Int(sectionIndex))) return .value((token, IndexPath(row: max(0, displayedTokens.count - 1), section: Int(sectionIndex)), withTokenCreation: false))
} }
case .availableNewTokens: case .availableNewTokens:
break break
case .popularTokens:
let token = popularTokens[indexPath.row]
return fetchContractDataPromise(forServer: token.server, address: token.contractAddress).then { token -> Promise<(token: TokenObject, indexPathToInsert: IndexPath, withTokenCreation: Bool)?> in
self.popularTokens.remove(at: indexPath.row)
self.displayedTokens.append(token)
if let sectionIndex = self.sections.index(of: .displayedTokens) {
return .value((token, IndexPath(row: max(0, self.displayedTokens.count - 1), section: Int(sectionIndex)), withTokenCreation: true))
}
return .value(nil)
}
} }
return nil return .value(nil)
} }
func deleteToken(indexPath: IndexPath) -> (token: TokenObject, indexPathToInsert: IndexPath)? { func deleteToken(indexPath: IndexPath) -> Promise<(token: TokenObject, indexPathToInsert: IndexPath, withTokenCreation: Bool)?> {
switch sections[indexPath.section] { switch sections[indexPath.section] {
case .displayedTokens: case .displayedTokens:
let token = displayedTokens.remove(at: indexPath.row) let token = displayedTokens.remove(at: indexPath.row)
hiddenTokens.insert(token, at: 0) hiddenTokens.insert(token, at: 0)
if let sectionIndex = sections.index(of: .hiddenTokens) { if let sectionIndex = sections.index(of: .hiddenTokens) {
return (token, IndexPath(row: 0, section: Int(sectionIndex))) return .value((token, IndexPath(row: 0, section: Int(sectionIndex)), withTokenCreation: false))
} }
case .hiddenTokens: case .hiddenTokens, .availableNewTokens, .popularTokens:
break
case .availableNewTokens:
break break
} }
return nil return .value(nil)
} }
func editingStyle(indexPath: IndexPath) -> UITableViewCell.EditingStyle { func editingStyle(indexPath: IndexPath) -> UITableViewCell.EditingStyle {
switch sections[indexPath.section] { switch sections[indexPath.section] {
case .displayedTokens: case .displayedTokens:
return .delete return .delete
case .availableNewTokens: case .availableNewTokens, .popularTokens, .hiddenTokens:
return .insert
case .hiddenTokens:
return .insert return .insert
} }
} }
@ -139,21 +165,21 @@ class AddHideTokensViewModel {
switch sections[indexPath.section] { switch sections[indexPath.section] {
case .displayedTokens: case .displayedTokens:
return true return true
case .availableNewTokens: case .availableNewTokens, .popularTokens, .hiddenTokens:
return false
case .hiddenTokens:
return false return false
} }
} }
func item(atIndexPath indexPath: IndexPath) -> TokenObject? { func item(atIndexPath indexPath: IndexPath) -> WalletOrPopularToken? {
switch sections[indexPath.section] { switch sections[indexPath.section] {
case .displayedTokens: case .displayedTokens:
return displayedTokens[indexPath.row] return .walletToken(displayedTokens[indexPath.row])
case .hiddenTokens: case .hiddenTokens:
return hiddenTokens[indexPath.row] return .walletToken(hiddenTokens[indexPath.row])
case .availableNewTokens: case .availableNewTokens:
return nil return nil
case .popularTokens:
return .popularToken(popularTokens[indexPath.row])
} }
} }
@ -164,17 +190,11 @@ class AddHideTokensViewModel {
displayedTokens.insert(token, at: to.row) displayedTokens.insert(token, at: to.row)
return displayedTokens return displayedTokens
case .hiddenTokens: case .hiddenTokens, .availableNewTokens, .popularTokens:
return nil
case .availableNewTokens:
return nil return nil
} }
} }
func ticker(for token: TokenObject) -> CoinTicker? {
return tickers[token.addressAndRPCServer]
}
private func filter(tokens: [TokenObject]) { private func filter(tokens: [TokenObject]) {
displayedTokens.removeAll() displayedTokens.removeAll()
hiddenTokens.removeAll() hiddenTokens.removeAll()
@ -187,6 +207,17 @@ class AddHideTokensViewModel {
hiddenTokens.append(token) hiddenTokens.append(token)
} }
} }
popularTokens = filterTokensCoordinator.filterTokens(tokens: allPopularTokens, walletTokens: tokens, filter: .keyword(searchText ?? ""))
displayedTokens = filterTokensCoordinator.sortDisplayedTokens(tokens: displayedTokens) displayedTokens = filterTokensCoordinator.sortDisplayedTokens(tokens: displayedTokens)
} }
private func fetchContractDataPromise(forServer server: RPCServer, address: AlphaWallet.Address) -> Promise<TokenObject> {
guard let coordinator = singleChainTokenCoordinator(forServer: server) else {
return .init(error: RetrieveSingleChainTokenCoordinator())
}
return coordinator.addImportedTokenPromise(forContract: address, onlyIfThereIsABalance: false)
}
private struct RetrieveSingleChainTokenCoordinator: Error { }
} }

@ -0,0 +1,50 @@
//
// FungibleTokenViewCellViewModel3.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 07.06.2021.
//
import UIKit
struct PopularTokenViewCellViewModel {
private let token: PopularToken
private let isVisible: Bool
init(token: PopularToken, isVisible: Bool = true) {
self.token = token
self.isVisible = isVisible
}
private var title: String {
return token.name
}
var backgroundColor: UIColor {
return Screen.TokenCard.Color.background
}
var contentsBackgroundColor: UIColor {
return Screen.TokenCard.Color.background
}
var titleAttributedString: NSAttributedString {
return NSAttributedString(string: title, attributes: [
.foregroundColor: Screen.TokenCard.Color.title,
.font: Screen.TokenCard.Font.title
])
}
var alpha: CGFloat {
return isVisible ? 1.0 : 0.4
}
var iconImage: Subscribable<TokenImage> {
token.iconImage
}
var blockChainTagViewModel: BlockchainTagLabelViewModel {
return .init(server: token.server)
}
}

@ -57,7 +57,7 @@ struct TokenViewControllerViewModel {
case .xDai: case .xDai:
return [.init(type: .erc20Send), .init(type: .xDaiBridge), .init(type: .erc20Receive)] + tokenActionsProvider.actions(token: key) return [.init(type: .erc20Send), .init(type: .xDaiBridge), .init(type: .erc20Receive)] + tokenActionsProvider.actions(token: key)
case .main, .kovan, .ropsten, .rinkeby, .poa, .sokol, .classic, .callisto, .goerli, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .heco, .heco_testnet, .custom, .fantom, .fantom_testnet, .avalanche, .avalanche_testnet, .polygon, .mumbai_testnet, .optimistic, .optimisticKovan: case .main, .kovan, .ropsten, .rinkeby, .poa, .sokol, .classic, .callisto, .goerli, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .heco, .heco_testnet, .custom, .fantom, .fantom_testnet, .avalanche, .avalanche_testnet, .polygon, .mumbai_testnet, .optimistic, .optimisticKovan:
return actions + tokenActionsProvider.actions(token: token) return actions + tokenActionsProvider.actions(token: key)
} }
} }
} else { } else {

@ -0,0 +1,63 @@
//
// WalletTokenViewCellViewModel.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 07.06.2021.
//
import UIKit
import BigInt
struct WalletTokenViewCellViewModel {
private let shortFormatter = EtherNumberFormatter.short
private let token: TokenObject
private let assetDefinitionStore: AssetDefinitionStore
private let isVisible: Bool
init(token: TokenObject, assetDefinitionStore: AssetDefinitionStore, isVisible: Bool = true) {
self.token = token
self.assetDefinitionStore = assetDefinitionStore
self.isVisible = isVisible
}
private var title: String {
return token.titleInPluralForm(withAssetDefinitionStore: assetDefinitionStore)
}
private var amount: String {
return shortFormatter.string(from: token.valueBigInt, decimals: token.decimals)
}
var cryptoValueAttributedString: NSAttributedString {
return NSAttributedString(string: isVisible ? amount : String(), attributes: [
.foregroundColor: Screen.TokenCard.Color.title,
.font: Screen.TokenCard.Font.title
])
}
var backgroundColor: UIColor {
return Screen.TokenCard.Color.background
}
var contentsBackgroundColor: UIColor {
return Screen.TokenCard.Color.background
}
var titleAttributedString: NSAttributedString {
return NSAttributedString(string: title, attributes: [
.foregroundColor: Screen.TokenCard.Color.title,
.font: Screen.TokenCard.Font.title
])
}
var alpha: CGFloat {
return isVisible ? 1.0 : 0.4
}
var iconImage: Subscribable<TokenImage> {
token.icon
}
var blockChainTagViewModel: BlockchainTagLabelViewModel {
return .init(server: token.server)
}
}

@ -0,0 +1,69 @@
//
// PopularTokenViewCell.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 07.06.2021.
//
import UIKit
class PopularTokenViewCell: UITableViewCell {
private let background = UIView()
private let titleLabel = UILabel()
private var viewsWithContent: [UIView] {
[titleLabel]
}
private var tokenIconImageView: TokenImageView = {
let imageView = TokenImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private var blockChainTagLabel = BlockchainTagLabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(background)
background.translatesAutoresizingMaskIntoConstraints = false
let col0 = tokenIconImageView
let col1 = [
[titleLabel, UIView.spacerWidth(flexible: true)].asStackView(spacing: 5)
].asStackView(axis: .vertical, spacing: 2)
let stackView = [col0, col1].asStackView(spacing: 12, alignment: .center)
stackView.translatesAutoresizingMaskIntoConstraints = false
background.addSubview(stackView)
NSLayoutConstraint.activate([
tokenIconImageView.heightAnchor.constraint(equalToConstant: 40),
tokenIconImageView.widthAnchor.constraint(equalToConstant: 40),
stackView.anchorsConstraint(to: background, edgeInsets: .init(top: 16, left: 20, bottom: 16, right: 16)),
background.anchorsConstraint(to: contentView)
])
}
required init?(coder aDecoder: NSCoder) {
return nil
}
func configure(viewModel: PopularTokenViewCellViewModel) {
selectionStyle = .none
backgroundColor = viewModel.backgroundColor
background.backgroundColor = viewModel.contentsBackgroundColor
contentView.backgroundColor = GroupedTable.Color.background
titleLabel.attributedText = viewModel.titleAttributedString
titleLabel.baselineAdjustment = .alignCenters
viewsWithContent.forEach {
$0.alpha = viewModel.alpha
}
tokenIconImageView.subscribable = viewModel.iconImage
blockChainTagLabel.configure(viewModel: viewModel.blockChainTagViewModel)
}
}

@ -0,0 +1,73 @@
//
// WalletTokenViewCell.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 07.06.2021.
//
import UIKit
class WalletTokenViewCell: UITableViewCell {
private let background = UIView()
private let titleLabel = UILabel()
private let cryptoValueLabel = UILabel()
private var viewsWithContent: [UIView] {
[titleLabel, cryptoValueLabel]
}
private var tokenIconImageView: TokenImageView = {
let imageView = TokenImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private var blockChainTagLabel = BlockchainTagLabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(background)
background.translatesAutoresizingMaskIntoConstraints = false
cryptoValueLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
cryptoValueLabel.setContentHuggingPriority(.required, for: .horizontal)
let col0 = tokenIconImageView
let col1 = [
[cryptoValueLabel, titleLabel, UIView.spacerWidth(flexible: true)].asStackView(spacing: 5)
].asStackView(axis: .vertical, spacing: 2)
let stackView = [col0, col1].asStackView(spacing: 12, alignment: .center)
stackView.translatesAutoresizingMaskIntoConstraints = false
background.addSubview(stackView)
NSLayoutConstraint.activate([
tokenIconImageView.heightAnchor.constraint(equalToConstant: 40),
tokenIconImageView.widthAnchor.constraint(equalToConstant: 40),
stackView.anchorsConstraint(to: background, edgeInsets: .init(top: 16, left: 20, bottom: 16, right: 16)),
background.anchorsConstraint(to: contentView)
])
}
required init?(coder aDecoder: NSCoder) {
return nil
}
func configure(viewModel: WalletTokenViewCellViewModel) {
selectionStyle = .none
backgroundColor = viewModel.backgroundColor
background.backgroundColor = viewModel.contentsBackgroundColor
contentView.backgroundColor = GroupedTable.Color.background
titleLabel.attributedText = viewModel.titleAttributedString
titleLabel.baselineAdjustment = .alignCenters
cryptoValueLabel.attributedText = viewModel.cryptoValueAttributedString
viewsWithContent.forEach {
$0.alpha = viewModel.alpha
}
tokenIconImageView.subscribable = viewModel.iconImage
blockChainTagLabel.configure(viewModel: viewModel.blockChainTagViewModel)
}
}

@ -5,28 +5,29 @@ import PromiseKit
typealias TokenImage = (image: UIImage, symbol: String) typealias TokenImage = (image: UIImage, symbol: String)
extension TokenObject { private func programmaticallyGeneratedIconImage(for contractAddress: AlphaWallet.Address, server: RPCServer) -> UIImage {
fileprivate static let numberOfCharactersOfSymbolToShowInIcon = 4 let backgroundColor = symbolBackgroundColor(for: contractAddress, server: server)
return UIView.tokenSymbolBackgroundImage(backgroundColor: backgroundColor)
fileprivate var programmaticallyGeneratedIconImage: UIImage { }
UIView.tokenSymbolBackgroundImage(backgroundColor: symbolBackgroundColor)
}
private var symbolBackgroundColor: UIColor { private func symbolBackgroundColor(for contractAddress: AlphaWallet.Address, server: RPCServer) -> UIColor {
if contractAddress.sameContract(as: Constants.nativeCryptoAddressInDatabase) { if contractAddress.sameContract(as: Constants.nativeCryptoAddressInDatabase) {
return server.blockChainNameColor return server.blockChainNameColor
} else {
let colors = [R.color.radical()!, R.color.cerulean()!, R.color.emerald()!, R.color.indigo()!, R.color.azure()!, R.color.pumpkin()!]
let index: Int
//We just need a random number from the contract. The LSBs are more random than the MSBs
if let i = Int(contractAddress.eip55String.substring(from: 37), radix: 16) {
index = i % colors.count
} else { } else {
let colors = [R.color.radical()!, R.color.cerulean()!, R.color.emerald()!, R.color.indigo()!, R.color.azure()!, R.color.pumpkin()!] index = 0
let index: Int
//We just need a random number from the contract. The LSBs are more random than the MSBs
if let i = Int(contractAddress.eip55String.substring(from: 37), radix: 16) {
index = i % colors.count
} else {
index = 0
}
return colors[index]
} }
return colors[index]
} }
}
extension TokenObject {
fileprivate static let numberOfCharactersOfSymbolToShowInIcon = 4
var icon: Subscribable<TokenImage> { var icon: Subscribable<TokenImage> {
switch type { switch type {
@ -43,19 +44,20 @@ extension TokenObject {
} }
} }
private class TokenImageFetcher { class TokenImageFetcher {
private enum ImageAvailabilityError: LocalizedError { private enum ImageAvailabilityError: LocalizedError {
case notAvailable case notAvailable
} }
static var instance = TokenImageFetcher() static var instance = TokenImageFetcher()
private var subscribables: [String: Subscribable<TokenImage>] = .init() private var subscribables: ThreadSafeDictionary<String, Subscribable<TokenImage>> = .init()
private let queue: DispatchQueue = .global()
private func programmaticallyGenerateIcon(forToken tokenObject: TokenObject) -> TokenImage { private func programmaticallyGenerateIcon(for contractAddress: AlphaWallet.Address, server: RPCServer, symbol: String) -> TokenImage {
let i = [TokenObject.numberOfCharactersOfSymbolToShowInIcon, tokenObject.symbol.count].min()! let i = [TokenObject.numberOfCharactersOfSymbolToShowInIcon, symbol.count].min()!
let symbol = tokenObject.symbol.substring(to: i) let symbol = symbol.substring(to: i)
return (image: tokenObject.programmaticallyGeneratedIconImage, symbol: symbol) return (image: programmaticallyGeneratedIconImage(for: contractAddress, server: server), symbol: symbol)
} }
//Relies on built-in HTTP/HTTPS caching in iOS for the images //Relies on built-in HTTP/HTTPS caching in iOS for the images
@ -70,45 +72,96 @@ private class TokenImageFetcher {
subscribable = sub subscribable = sub
} }
let contractAddress = tokenObject.contractAddress
let server = tokenObject.server
let symbol = tokenObject.symbol
let balance = tokenObject.balance.first?.balance
let type = tokenObject.type
if tokenObject.contractAddress.sameContract(as: Constants.nativeCryptoAddressInDatabase) { if tokenObject.contractAddress.sameContract(as: Constants.nativeCryptoAddressInDatabase) {
subscribable.value = programmaticallyGenerateIcon(forToken: tokenObject) queue.async {
let value = self.programmaticallyGenerateIcon(for: contractAddress, server: server, symbol: symbol)
DispatchQueue.main.async {
subscribable.value = value
}
}
return subscribable return subscribable
} }
let contractAddress = tokenObject.contractAddress queue.async {
let balance = tokenObject.balance.first?.balance let generatedImage = self.programmaticallyGenerateIcon(for: contractAddress, server: server, symbol: symbol)
let generatedImage = programmaticallyGenerateIcon(forToken: tokenObject)
fetchFromOpenSea(tokenObject.type, balance: balance).done { self.fetchFromOpenSea(type, balance: balance).done(on: .main, {
subscribable.value = (image: $0, symbol: "")
}.catch { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.fetchFromAssetGitHubRepo(contractAddress: contractAddress).done {
subscribable.value = (image: $0, symbol: "") subscribable.value = (image: $0, symbol: "")
}.catch { _ in }).catch(on: self.queue) { [weak self] _ in
subscribable.value = generatedImage guard let strongSelf = self else { return }
strongSelf.fetchFromAssetGitHubRepo(contractAddress: contractAddress).done(on: .main, {
subscribable.value = (image: $0, symbol: "")
}).catch(on: .main, { _ in
subscribable.value = generatedImage
})
} }
} }
return subscribable return subscribable
} }
func image(contractAddress: AlphaWallet.Address, server: RPCServer, name: String) -> Subscribable<TokenImage> {
let subscribable: Subscribable<TokenImage>
let key = "\(contractAddress.eip55String)-\(server.chainID)"
if let sub = subscribables[key] {
subscribable = sub
} else {
let sub = Subscribable<TokenImage>(nil)
subscribables[key] = sub
subscribable = sub
}
if contractAddress.sameContract(as: Constants.nativeCryptoAddressInDatabase) {
queue.async {
let generatedImage = self.programmaticallyGenerateIcon(for: contractAddress, server: server, symbol: name)
DispatchQueue.main.async {
subscribable.value = generatedImage
}
}
return subscribable
}
queue.async {
let generatedImage = self.programmaticallyGenerateIcon(for: contractAddress, server: server, symbol: name)
self.fetchFromAssetGitHubRepo(contractAddress: contractAddress).done(on: .main, {
subscribable.value = (image: $0, symbol: "")
}).catch(on: .main, { _ in
subscribable.value = generatedImage
})
}
return subscribable
}
private func fetchFromOpenSea(_ type: TokenType, balance: String?) -> Promise<UIImage> { private func fetchFromOpenSea(_ type: TokenType, balance: String?) -> Promise<UIImage> {
Promise { seal in Promise { seal in
switch type { queue.async {
case .erc721: switch type {
if let json = balance, let data = json.data(using: .utf8), let openSeaNonFungible = nonFungible(fromJsonData: data), !openSeaNonFungible.contractImageUrl.isEmpty { case .erc721:
let request = URLRequest(url: URL(string: openSeaNonFungible.contractImageUrl)!) if let json = balance, let data = json.data(using: .utf8), let openSeaNonFungible = nonFungible(fromJsonData: data), !openSeaNonFungible.contractImageUrl.isEmpty {
fetch(request: request).done { image in let request = URLRequest(url: URL(string: openSeaNonFungible.contractImageUrl)!)
seal.fulfill(image) self.fetch(request: request).done(on: self.queue, { image in
}.catch { _ in seal.fulfill(image)
}).catch(on: self.queue, { _ in
seal.reject(ImageAvailabilityError.notAvailable)
})
} else {
seal.reject(ImageAvailabilityError.notAvailable) seal.reject(ImageAvailabilityError.notAvailable)
} }
} else { case .nativeCryptocurrency, .erc20, .erc875, .erc721ForTickets:
seal.reject(ImageAvailabilityError.notAvailable) seal.reject(ImageAvailabilityError.notAvailable)
} }
case .nativeCryptocurrency, .erc20, .erc875, .erc721ForTickets:
seal.reject(ImageAvailabilityError.notAvailable)
} }
} }
} }
@ -116,34 +169,36 @@ private class TokenImageFetcher {
private func fetchFromAssetGitHubRepo(_ githubAssetsSource: GithubAssetsURLResolver.Source, contractAddress: AlphaWallet.Address) -> Promise<UIImage> { private func fetchFromAssetGitHubRepo(_ githubAssetsSource: GithubAssetsURLResolver.Source, contractAddress: AlphaWallet.Address) -> Promise<UIImage> {
firstly { firstly {
GithubAssetsURLResolver().resolve(for: githubAssetsSource, contractAddress: contractAddress) GithubAssetsURLResolver().resolve(for: githubAssetsSource, contractAddress: contractAddress)
}.then { request -> Promise<UIImage> in }.then(on: queue, { request -> Promise<UIImage> in
self.fetch(request: request) self.fetch(request: request)
} })
} }
private func fetchFromAssetGitHubRepo(contractAddress: AlphaWallet.Address) -> Promise<UIImage> { private func fetchFromAssetGitHubRepo(contractAddress: AlphaWallet.Address) -> Promise<UIImage> {
firstly { firstly {
fetchFromAssetGitHubRepo(.alphaWallet, contractAddress: contractAddress) fetchFromAssetGitHubRepo(.alphaWallet, contractAddress: contractAddress)
}.recover { _ -> Promise<UIImage> in }.recover(on: queue, { _ -> Promise<UIImage> in
self.fetchFromAssetGitHubRepo(.thirdParty, contractAddress: contractAddress) self.fetchFromAssetGitHubRepo(.thirdParty, contractAddress: contractAddress)
} })
} }
private func fetch(request: URLRequest) -> Promise<UIImage> { private func fetch(request: URLRequest) -> Promise<UIImage> {
Promise { seal in Promise { seal in
let task = URLSession.shared.dataTask(with: request) { data, _, _ in queue.async {
if let data = data { let task = URLSession.shared.dataTask(with: request) { data, _, _ in
let image = UIImage(data: data) if let data = data {
if let img = image { let image = UIImage(data: data)
seal.fulfill(img) if let img = image {
seal.fulfill(img)
} else {
seal.reject(ImageAvailabilityError.notAvailable)
}
} else { } else {
seal.reject(ImageAvailabilityError.notAvailable) seal.reject(ImageAvailabilityError.notAvailable)
} }
} else {
seal.reject(ImageAvailabilityError.notAvailable)
} }
task.resume()
} }
task.resume()
} }
} }
} }

Loading…
Cancel
Save