diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 193a8285dc..e104720593 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -2514,6 +2514,8 @@ D0118E492B7C9A5200D55CE6 /* ResendBitcoinModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0118E472B7C9A5200D55CE6 /* ResendBitcoinModule.swift */; }; D0118E4B2B7CC63300D55CE6 /* ResendBitcoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0118E4A2B7CC63300D55CE6 /* ResendBitcoinViewController.swift */; }; D0118E4C2B7CC63300D55CE6 /* ResendBitcoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0118E4A2B7CC63300D55CE6 /* ResendBitcoinViewController.swift */; }; + D01513D12D1EA6A400FD9816 /* SpamScanState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01513D02D1EA6A400FD9816 /* SpamScanState.swift */; }; + D01513D22D1EA6A400FD9816 /* SpamScanState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01513D02D1EA6A400FD9816 /* SpamScanState.swift */; }; D01517EE2B10A69E00FECCBC /* WatchModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01517ED2B10A69E00FECCBC /* WatchModule.swift */; }; D01517EF2B10A69E00FECCBC /* WatchModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01517ED2B10A69E00FECCBC /* WatchModule.swift */; }; D023D2632A249E59004F65B0 /* TronKit in Frameworks */ = {isa = PBXBuildFile; productRef = D023D2622A249E59004F65B0 /* TronKit */; }; @@ -2528,6 +2530,12 @@ D023D2722A25CF61004F65B0 /* TronAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023D2702A25CF61004F65B0 /* TronAdapter.swift */; }; D02447D92C09FA5200A04BBC /* CoinTreasuriesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02447D82C09FA5200A04BBC /* CoinTreasuriesView.swift */; }; D02447DA2C09FA5200A04BBC /* CoinTreasuriesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02447D82C09FA5200A04BBC /* CoinTreasuriesView.swift */; }; + D02C85A82D1A93A60096A84D /* SpamAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02C85A72D1A93A60096A84D /* SpamAddress.swift */; }; + D02C85A92D1A93A60096A84D /* SpamAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02C85A72D1A93A60096A84D /* SpamAddress.swift */; }; + D02C85AB2D1A94410096A84D /* SpamAddressManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02C85AA2D1A94410096A84D /* SpamAddressManager.swift */; }; + D02C85AC2D1A94410096A84D /* SpamAddressManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02C85AA2D1A94410096A84D /* SpamAddressManager.swift */; }; + D02C85AE2D1A969C0096A84D /* SpamAddressStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02C85AD2D1A969C0096A84D /* SpamAddressStorage.swift */; }; + D02C85AF2D1A969C0096A84D /* SpamAddressStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02C85AD2D1A969C0096A84D /* SpamAddressStorage.swift */; }; D033289F2BF6199600BBB364 /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033289E2BF6199600BBB364 /* InfoView.swift */; }; D03328A02BF6199600BBB364 /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033289E2BF6199600BBB364 /* InfoView.swift */; }; D03F74822BF76D0A004FBCFA /* GasPriceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03F74812BF76D0A004FBCFA /* GasPriceData.swift */; }; @@ -2578,6 +2586,10 @@ D06A171C2BA1B1BC0081E312 /* FeeSettingsViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06A171A2BA1B1BC0081E312 /* FeeSettingsViewHelper.swift */; }; D06B302C2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06B302B2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift */; }; D06B302D2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06B302B2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift */; }; + D06F60032D195FBC0033A288 /* AddressSecurityCheckerChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F60012D195FBC0033A288 /* AddressSecurityCheckerChain.swift */; }; + D06F60052D195FBC0033A288 /* AddressSecurityCheckerChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F60012D195FBC0033A288 /* AddressSecurityCheckerChain.swift */; }; + D06F60082D195FE90033A288 /* AddressSecurityCheckerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F60072D195FE90033A288 /* AddressSecurityCheckerFactory.swift */; }; + D06F60092D195FE90033A288 /* AddressSecurityCheckerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F60072D195FE90033A288 /* AddressSecurityCheckerFactory.swift */; }; D07157DB2A2DD968006F141F /* SendTronModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07157DA2A2DD968006F141F /* SendTronModule.swift */; }; D07157DC2A2DD968006F141F /* SendTronModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07157DA2A2DD968006F141F /* SendTronModule.swift */; }; D07157DE2A2DDA09006F141F /* SendTronService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07157DD2A2DDA09006F141F /* SendTronService.swift */; }; @@ -2682,6 +2694,8 @@ D0F132A52B6B98F500C7310E /* RbfViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F132A32B6B98F500C7310E /* RbfViewModel.swift */; }; D0F132A72B6B990500C7310E /* RbfDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F132A62B6B990500C7310E /* RbfDataSource.swift */; }; D0F132A82B6B990500C7310E /* RbfDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F132A62B6B990500C7310E /* RbfDataSource.swift */; }; + D0F766CF2D1AD36200E409AD /* SpamAddressDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F766CE2D1AD36200E409AD /* SpamAddressDetector.swift */; }; + D0F766D02D1AD36200E409AD /* SpamAddressDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F766CE2D1AD36200E409AD /* SpamAddressDetector.swift */; }; D0F9F5172B99857700C3190A /* FeeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9F5162B99857700C3190A /* FeeSettings.swift */; }; D0F9F5182B99857700C3190A /* FeeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9F5162B99857700C3190A /* FeeSettings.swift */; }; D30D7E5F2CAAC89700B8CAA7 /* TonConnectEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30D7E5E2CAAC89700B8CAA7 /* TonConnectEventHandler.swift */; }; @@ -4504,12 +4518,16 @@ D00DAE442B626C2900F48E1D /* GasPrice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GasPrice.swift; sourceTree = ""; }; D0118E472B7C9A5200D55CE6 /* ResendBitcoinModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResendBitcoinModule.swift; sourceTree = ""; }; D0118E4A2B7CC63300D55CE6 /* ResendBitcoinViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResendBitcoinViewController.swift; sourceTree = ""; }; + D01513D02D1EA6A400FD9816 /* SpamScanState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpamScanState.swift; sourceTree = ""; }; D01517ED2B10A69E00FECCBC /* WatchModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchModule.swift; sourceTree = ""; }; D023D2662A24BBC6004F65B0 /* TronAddressParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronAddressParser.swift; sourceTree = ""; }; D023D2692A24CD16004F65B0 /* BaseTronAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTronAdapter.swift; sourceTree = ""; }; D023D26C2A24CD4F004F65B0 /* TronKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronKitManager.swift; sourceTree = ""; }; D023D2702A25CF61004F65B0 /* TronAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronAdapter.swift; sourceTree = ""; }; D02447D82C09FA5200A04BBC /* CoinTreasuriesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinTreasuriesView.swift; sourceTree = ""; }; + D02C85A72D1A93A60096A84D /* SpamAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpamAddress.swift; sourceTree = ""; }; + D02C85AA2D1A94410096A84D /* SpamAddressManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpamAddressManager.swift; sourceTree = ""; }; + D02C85AD2D1A969C0096A84D /* SpamAddressStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpamAddressStorage.swift; sourceTree = ""; }; D033289E2BF6199600BBB364 /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = ""; }; D03F74812BF76D0A004FBCFA /* GasPriceData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GasPriceData.swift; sourceTree = ""; }; D04D98E9268055A2001A3135 /* TransactionRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionRecord.swift; sourceTree = ""; }; @@ -4535,6 +4553,8 @@ D066A45E2C6CC7E200074E35 /* WelcomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenViewModel.swift; sourceTree = ""; }; D06A171A2BA1B1BC0081E312 /* FeeSettingsViewHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeSettingsViewHelper.swift; sourceTree = ""; }; D06B302B2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyFeeSettingsViewModel.swift; sourceTree = ""; }; + D06F60012D195FBC0033A288 /* AddressSecurityCheckerChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressSecurityCheckerChain.swift; sourceTree = ""; }; + D06F60072D195FE90033A288 /* AddressSecurityCheckerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressSecurityCheckerFactory.swift; sourceTree = ""; }; D07157DA2A2DD968006F141F /* SendTronModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTronModule.swift; sourceTree = ""; }; D07157DD2A2DDA09006F141F /* SendTronService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTronService.swift; sourceTree = ""; }; D07157E02A2DDA17006F141F /* SendTronViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTronViewModel.swift; sourceTree = ""; }; @@ -4585,6 +4605,7 @@ D0F132A02B6B98E100C7310E /* RbfService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RbfService.swift; sourceTree = ""; }; D0F132A32B6B98F500C7310E /* RbfViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RbfViewModel.swift; sourceTree = ""; }; D0F132A62B6B990500C7310E /* RbfDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RbfDataSource.swift; sourceTree = ""; }; + D0F766CE2D1AD36200E409AD /* SpamAddressDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpamAddressDetector.swift; sourceTree = ""; }; D0F9F5162B99857700C3190A /* FeeSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeSettings.swift; sourceTree = ""; }; D30D7E5E2CAAC89700B8CAA7 /* TonConnectEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectEventHandler.swift; sourceTree = ""; }; D30D7E612CAACCF300B8CAA7 /* TonConnectSendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectSendView.swift; sourceTree = ""; }; @@ -5404,6 +5425,7 @@ 11B35480153660045A6CF9AC /* Managers */ = { isa = PBXGroup; children = ( + D02C85AA2D1A94410096A84D /* SpamAddressManager.swift */, 6BE8A0802ADE2FAB0012DE7F /* CurrencyManager.swift */, 1A564BB88BF3ED779F8C21DC /* BlurManager.swift */, 11B35C4FFB99D10A8F343E9C /* LanguageManager.swift */, @@ -5756,6 +5778,8 @@ 11B356605394189E9B5EBEE0 /* Models */ = { isa = PBXGroup; children = ( + D01513D02D1EA6A400FD9816 /* SpamScanState.swift */, + D02C85A72D1A93A60096A84D /* SpamAddress.swift */, 6BE8A07D2ADE2F950012DE7F /* CurrencyValue.swift */, 6BE8A07A2ADE2F8D0012DE7F /* Currency.swift */, 11B3526E3BCED7FF55C3CAD2 /* Deprecated */, @@ -5928,6 +5952,7 @@ 11B35703146B7401E5D5170E /* Factories */ = { isa = PBXGroup; children = ( + D06F60072D195FE90033A288 /* AddressSecurityCheckerFactory.swift */, 11B35C4D6F474C2EB3687EB4 /* AccountFactory.swift */, 11B353450DED12F9F024BAD0 /* AddressParserFactory.swift */, 11B359D88585F2BBFA56CB77 /* FeeRateProviderFactory.swift */, @@ -5971,6 +5996,7 @@ 11B3573D2F4B70997D4BBAD3 /* Storage */ = { isa = PBXGroup; children = ( + D02C85AD2D1A969C0096A84D /* SpamAddressStorage.swift */, 11B352044BCE494491257933 /* LocalStorage.swift */, 11B35A1AE56A94BEB52AC4D1 /* StorageMigrator.swift */, 11B35E87C15A3AE82F471007 /* AccountStorage.swift */, @@ -7076,6 +7102,8 @@ 58AAA0C31F14444C22ACF393 /* Address */ = { isa = PBXGroup; children = ( + D0F766CE2D1AD36200E409AD /* SpamAddressDetector.swift */, + D06F60012D195FBC0033A288 /* AddressSecurityCheckerChain.swift */, 58AAA7305EF2A12D3DC82B32 /* AddressParserChain.swift */, 58AAAAC62C5B05463D339BCC /* EvmAddressParserItem.swift */, 58AAA9A4761030BE9F60C85E /* UdnAddressParserItem.swift */, @@ -9406,6 +9434,7 @@ D3833AF32BF20B8600ACECFB /* MarketPairsView.swift in Sources */, 11B350B22CCCFCD466BEB808 /* FeeRateProvider.swift in Sources */, 6BCD53172A161F4800993F20 /* BackupViewController.swift in Sources */, + D01513D12D1EA6A400FD9816 /* SpamScanState.swift in Sources */, 11B35B3F384758B223A7218C /* MainSettingsFooterCell.swift in Sources */, 58AAAA6AF87DE0EE337BB8AA /* GradientLayer.swift in Sources */, 58AAA82CE1738BCC5B426CB8 /* DebugModule.swift in Sources */, @@ -9459,6 +9488,7 @@ 6BBB92B12D10095300112409 /* SubscribePeriodSegmentView.swift in Sources */, D023D2682A24BBC6004F65B0 /* TronAddressParser.swift in Sources */, D36DE0C1272FD864000BC916 /* UniswapService.swift in Sources */, + D0F766D02D1AD36200E409AD /* SpamAddressDetector.swift in Sources */, 11B35CB5A90FCD0B53D59140 /* AlertViewController.swift in Sources */, 11B35C8A76BB5C69263E9757 /* AlertModule.swift in Sources */, 11B35383F24853BDDB27618A /* AlertPresenter.swift in Sources */, @@ -9736,6 +9766,7 @@ D36DE0B4272FD689000BC916 /* SwapProviderManager.swift in Sources */, 11B357118379A537844A83D0 /* EvmSyncSource.swift in Sources */, 11B354CAD4BC4FAB3889838D /* EvmSyncSourceManager.swift in Sources */, + D02C85A82D1A93A60096A84D /* SpamAddress.swift in Sources */, D36287B62CA2CC8600ADAF3B /* TonConnectConnectViewModel.swift in Sources */, 11B3573B8B8DA5C1DE332EB6 /* EvmNetworkViewController.swift in Sources */, 11B3558589D57B3EAD53919F /* EvmNetworkViewModel.swift in Sources */, @@ -9824,6 +9855,7 @@ 2FA5D095A83D18DE2E21B580 /* EvmTransactionsAdapter.swift in Sources */, 2FA5D4BE55E8AB193EA61411 /* BinanceChainIncomingTransactionRecord.swift in Sources */, 2FA5DC0EF03C92C2125D433E /* BinanceChainOutgoingTransactionRecord.swift in Sources */, + D06F60082D195FE90033A288 /* AddressSecurityCheckerFactory.swift in Sources */, 2FA5DC95B68FC21FD48CCB13 /* BinanceChainTransactionRecord.swift in Sources */, 2FA5D6BCACDEB0BF06310354 /* TransactionAdapterManager.swift in Sources */, 2FA5DB5E2D23E973286979E8 /* HistoricalRateService.swift in Sources */, @@ -9851,6 +9883,7 @@ D07157DC2A2DD968006F141F /* SendTronModule.swift in Sources */, 11B35A82532EC55909EFBAD8 /* LaunchScreen.swift in Sources */, 6BB14F762C01D04200E879B2 /* CheckBoxUiView.swift in Sources */, + D06F60052D195FBC0033A288 /* AddressSecurityCheckerChain.swift in Sources */, 6BCD53052A161F4100993F20 /* ICloudBackupTermsViewController.swift in Sources */, 11B354FA6F6BF59F64560590 /* CoinTreasuriesViewModel.swift in Sources */, 11B35F6092E0950714E277E4 /* PostCell.swift in Sources */, @@ -10663,6 +10696,7 @@ 11B35AAD64D68265B2128C25 /* TransactionBlockchainSelectView.swift in Sources */, 11B35A4F4A98537CE99B5221 /* TransactionTokenSelectView.swift in Sources */, 11B35ACE7B126DCF9F7F1A19 /* SearchBar.swift in Sources */, + D02C85AB2D1A94410096A84D /* SpamAddressManager.swift in Sources */, 11B3546CB3C043D22A5F7A88 /* TransactionsViewModel.swift in Sources */, 11B35071705455BC25C214D2 /* TonAdapter.swift in Sources */, 11B35C60FE9B94994FCCB0CB /* TonTransactionRecord.swift in Sources */, @@ -10743,6 +10777,7 @@ 11B35C943710774694282388 /* IMultiSwapQuote.swift in Sources */, 11B35772618CDA2F19CCDB2C /* IMultiSwapConfirmationQuote.swift in Sources */, 11B354B484C4785A27216BEC /* ISendData.swift in Sources */, + D02C85AE2D1A969C0096A84D /* SpamAddressStorage.swift in Sources */, 11B35A4428B94FF4E4F01AA9 /* SendData.swift in Sources */, 11B358C4D4C466ACCEF0E4C7 /* MultiSwapMainField.swift in Sources */, 11B353D4BC292465358979D3 /* ValueLevel.swift in Sources */, @@ -10888,6 +10923,7 @@ 11B3540F182F3EDE74245EC7 /* MainSettingsFooterCell.swift in Sources */, 6BCD53162A161F4800993F20 /* BackupViewController.swift in Sources */, D3833AF22BF20B8600ACECFB /* MarketPairsView.swift in Sources */, + D01513D22D1EA6A400FD9816 /* SpamScanState.swift in Sources */, 58AAAD33B32694AFA2E954D6 /* GradientLayer.swift in Sources */, 58AAAE430A2184D5A12202EA /* DebugModule.swift in Sources */, 58AAA54CF2169C04A4A38817 /* DebugRouter.swift in Sources */, @@ -10941,6 +10977,7 @@ 6BBB92B02D10095300112409 /* SubscribePeriodSegmentView.swift in Sources */, 11B3557D484786A0D41E16AF /* AlertViewController.swift in Sources */, D023D2672A24BBC6004F65B0 /* TronAddressParser.swift in Sources */, + D0F766CF2D1AD36200E409AD /* SpamAddressDetector.swift in Sources */, 11B35C16BE16B6AEEA0690EC /* AlertModule.swift in Sources */, 11B3557E460F3BA25ED9F6CC /* AlertPresenter.swift in Sources */, 11B35E81542268ACDC17502A /* AlertRouter.swift in Sources */, @@ -11218,6 +11255,7 @@ 1A564032D3C011BCAA7D44A8 /* DeepLinkService.swift in Sources */, 11B35804545D048C0EDB8089 /* EvmSyncSource.swift in Sources */, 11B352F14D96C26D946E3877 /* EvmSyncSourceManager.swift in Sources */, + D02C85A92D1A93A60096A84D /* SpamAddress.swift in Sources */, D36287B52CA2CC8600ADAF3B /* TonConnectConnectViewModel.swift in Sources */, 11B35F73153DEE805DD539CE /* EvmNetworkViewController.swift in Sources */, 11B35B507F2F843A5B3E4C7C /* EvmNetworkViewModel.swift in Sources */, @@ -11306,6 +11344,7 @@ D003297626CD2C67002EC21D /* TransactionLockInfo.swift in Sources */, 11B35989090E4AD4E3EB8D72 /* CoinAuditsViewModel.swift in Sources */, 11B350C265D84964DCB0B317 /* CoinAuditsView.swift in Sources */, + D06F60092D195FE90033A288 /* AddressSecurityCheckerFactory.swift in Sources */, 2FA5DB27EB552BF53BC3E829 /* EvmTransactionsAdapter.swift in Sources */, 2FA5D7283FA3C72FDFECF26D /* BinanceChainIncomingTransactionRecord.swift in Sources */, 2FA5D5A690F5473920996095 /* BinanceChainOutgoingTransactionRecord.swift in Sources */, @@ -11333,6 +11372,7 @@ D087627629815DAE00E6FFD4 /* ChooseWatchViewModel.swift in Sources */, 11B35ED9D5F95988E9335440 /* CoinAnalyticsModule.swift in Sources */, D389BC4C2C0DDCF500724504 /* MarketAdvancedSearchBlockchainsView.swift in Sources */, + D06F60032D195FBC0033A288 /* AddressSecurityCheckerChain.swift in Sources */, D0118E4B2B7CC63300D55CE6 /* ResendBitcoinViewController.swift in Sources */, 6BB14F752C01D04200E879B2 /* CheckBoxUiView.swift in Sources */, 11B3551E5E9A6D167F7BA078 /* LaunchScreenManager.swift in Sources */, @@ -12145,6 +12185,7 @@ 11B35B09AADB1FBF7DDE765C /* TransactionFilterView.swift in Sources */, 11B355A6A5AD88A48B9641CE /* TransactionFilterViewModel.swift in Sources */, 11B35E02FF7E8A48B9B69ABB /* TransactionBlockchainSelectView.swift in Sources */, + D02C85AC2D1A94410096A84D /* SpamAddressManager.swift in Sources */, 11B355FC0E3DE029EB3F95D5 /* TransactionTokenSelectView.swift in Sources */, 11B358781EBEFCE7CED000F0 /* SearchBar.swift in Sources */, 11B358D91C9D8102C46B97ED /* TransactionsViewModel.swift in Sources */, @@ -12225,6 +12266,7 @@ 11B3550846F2DD60D3778FE5 /* EvmSendHandler.swift in Sources */, 11B350B33F891A2720420697 /* ISendHandler.swift in Sources */, 11B35A3B506EDD1EFB7D913E /* BaseSendEvmData.swift in Sources */, + D02C85AF2D1A969C0096A84D /* SpamAddressStorage.swift in Sources */, 11B3546D0D2F891FC9AE372C /* IMultiSwapQuote.swift in Sources */, 11B3584A053BC5DAE6E434F8 /* IMultiSwapConfirmationQuote.swift in Sources */, 11B3557AF8D64E9897965526 /* ISendData.swift in Sources */, @@ -13127,7 +13169,7 @@ repositoryURL = "https://github.com/horizontalsystems/EvmKit.Swift"; requirement = { kind = exactVersion; - version = 2.1.2; + version = 2.2.0; }; }; D3604E4828F02A8B0066C366 /* XCRemoteSwiftPackageReference "Eip20Kit" */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/BackCompatibility/EvmKit.swift b/UnstoppableWallet/UnstoppableWallet/BackCompatibility/EvmKit.swift index aeead6d463..c1fda6638e 100644 --- a/UnstoppableWallet/UnstoppableWallet/BackCompatibility/EvmKit.swift +++ b/UnstoppableWallet/UnstoppableWallet/BackCompatibility/EvmKit.swift @@ -24,10 +24,6 @@ public extension Kit { accountStatePublisher.asObservable() } - var allTransactionsObservable: Observable<([FullTransaction], Bool)> { - allTransactionsPublisher.asObservable() - } - func transactionSingle(hash: Data) -> Single { Single.create { [weak self] observer in guard let strongSelf = self else { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/Eip20Adapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/Eip20Adapter.swift index 8dd4e448c5..2c54a6cd87 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/Eip20Adapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/Eip20Adapter.swift @@ -12,12 +12,15 @@ class Eip20Adapter: BaseEvmAdapter { private let contractAddress: EvmKit.Address private let transactionConverter: EvmTransactionConverter - init(evmKitWrapper: EvmKitWrapper, contractAddress: String, wallet: Wallet, baseToken: Token, coinManager: CoinManager, evmLabelManager: EvmLabelManager) throws { + init(evmKitWrapper: EvmKitWrapper, contractAddress: String, wallet: Wallet, baseToken: Token, coinManager: CoinManager, evmLabelManager: EvmLabelManager, spamAddressManager: SpamAddressManager) throws { let address = try EvmKit.Address(hex: contractAddress) eip20Kit = try Eip20Kit.Kit.instance(evmKit: evmKitWrapper.evmKit, contractAddress: address) self.contractAddress = address - transactionConverter = EvmTransactionConverter(source: wallet.transactionSource, baseToken: baseToken, coinManager: coinManager, evmKitWrapper: evmKitWrapper, evmLabelManager: evmLabelManager) + transactionConverter = EvmTransactionConverter( + source: wallet.transactionSource, baseToken: baseToken, coinManager: coinManager, blockchainType: evmKitWrapper.blockchainType, + userAddress: evmKitWrapper.evmKit.address, evmLabelManager: evmLabelManager, spamAddressManager: spamAddressManager + ) super.init(evmKitWrapper: evmKitWrapper, decimals: wallet.decimals) } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmTransactionConverter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmTransactionConverter.swift index 5d1665c2fb..9356a43a10 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmTransactionConverter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmTransactionConverter.swift @@ -9,23 +9,23 @@ import UniswapKit class EvmTransactionConverter { private let coinManager: CoinManager - private let evmKitWrapper: EvmKitWrapper + private let blockchainType: BlockchainType + private let userAddress: EvmKit.Address private let evmLabelManager: EvmLabelManager + private let spamAddressManager: SpamAddressManager private let source: TransactionSource private let baseToken: MarketKit.Token - init(source: TransactionSource, baseToken: MarketKit.Token, coinManager: CoinManager, evmKitWrapper: EvmKitWrapper, evmLabelManager: EvmLabelManager) { + init(source: TransactionSource, baseToken: MarketKit.Token, coinManager: CoinManager, blockchainType: BlockchainType, userAddress: EvmKit.Address, evmLabelManager: EvmLabelManager, spamAddressManager: SpamAddressManager) { self.coinManager = coinManager - self.evmKitWrapper = evmKitWrapper + self.blockchainType = blockchainType + self.userAddress = userAddress self.evmLabelManager = evmLabelManager + self.spamAddressManager = spamAddressManager self.source = source self.baseToken = baseToken } - private var evmKit: EvmKit.Kit { - evmKitWrapper.evmKit - } - private func convertAmount(amount: BigUInt, decimals: Int, sign: FloatingPointSign) -> Decimal { guard let significand = Decimal(string: amount.description), significand != 0 else { return 0 @@ -40,7 +40,7 @@ class EvmTransactionConverter { } private func eip20Value(tokenAddress: EvmKit.Address, value: BigUInt, sign: FloatingPointSign, tokenInfo: Eip20Kit.TokenInfo?) -> AppValue { - let query = TokenQuery(blockchainType: evmKitWrapper.blockchainType, tokenType: .eip20(address: tokenAddress.hex)) + let query = TokenQuery(blockchainType: blockchainType, tokenType: .eip20(address: tokenAddress.hex)) if let token = try? coinManager.token(query: query) { let value = convertAmount(amount: value, decimals: token.decimals, sign: sign) @@ -181,6 +181,7 @@ class EvmTransactionConverter { extension EvmTransactionConverter { func transactionRecord(fromTransaction fullTransaction: FullTransaction) -> EvmTransactionRecord { let transaction = fullTransaction.transaction + let spam = spamAddressManager.isSpam(transactionHash: transaction.hash) switch fullTransaction.decoration { case is ContractCreationDecoration: @@ -196,7 +197,8 @@ extension EvmTransactionConverter { transaction: transaction, baseToken: baseToken, from: decoration.from.eip55, - value: baseAppValue(value: decoration.value, sign: .plus) + value: baseAppValue(value: decoration.value, sign: .plus), + spam: spam ) case let decoration as OutgoingDecoration: @@ -302,23 +304,21 @@ extension EvmTransactionConverter { ) case let decoration as UnknownTransactionDecoration: - let address = evmKit.address - - let internalTransactions = decoration.internalTransactions.filter { $0.to == address } + let internalTransactions = decoration.internalTransactions.filter { $0.to == userAddress } let eip20Transfers = decoration.eventInstances.compactMap { $0 as? TransferEventInstance } - let incomingEip20Transfers = eip20Transfers.filter { $0.to == address && $0.from != address } - let outgoingEip20Transfers = eip20Transfers.filter { $0.from == address } + let incomingEip20Transfers = eip20Transfers.filter { $0.to == userAddress && $0.from != userAddress } + let outgoingEip20Transfers = eip20Transfers.filter { $0.from == userAddress } let eip721Transfers = decoration.eventInstances.compactMap { $0 as? Eip721TransferEventInstance } - let incomingEip721Transfers = eip721Transfers.filter { $0.to == address && $0.from != address } - let outgoingEip721Transfers = eip721Transfers.filter { $0.from == address } + let incomingEip721Transfers = eip721Transfers.filter { $0.to == userAddress && $0.from != userAddress } + let outgoingEip721Transfers = eip721Transfers.filter { $0.from == userAddress } let eip1155Transfers = decoration.eventInstances.compactMap { $0 as? Eip1155TransferEventInstance } - let incomingEip1155Transfers = eip1155Transfers.filter { $0.to == address && $0.from != address } - let outgoingEip1155Transfers = eip1155Transfers.filter { $0.from == address } + let incomingEip1155Transfers = eip1155Transfers.filter { $0.to == userAddress && $0.from != userAddress } + let outgoingEip1155Transfers = eip1155Transfers.filter { $0.from == userAddress } - if transaction.from == address, let contractAddress = transaction.to, let value = transaction.value { + if transaction.from == userAddress, let contractAddress = transaction.to, let value = transaction.value { return ContractCallTransactionRecord( source: source, transaction: transaction, @@ -330,7 +330,7 @@ extension EvmTransactionConverter { outgoingEvents: transferEvents(contractAddress: contractAddress, value: value) + transferEvents(outgoingEip20Transfers: outgoingEip20Transfers) + transferEvents(outgoingEip721Transfers: outgoingEip721Transfers) + transferEvents(outgoingEip1155Transfers: outgoingEip1155Transfers) ) - } else if transaction.from != address, transaction.to != address { + } else if transaction.from != userAddress, transaction.to != userAddress { return ExternalContractCallTransactionRecord( source: source, transaction: transaction, @@ -338,7 +338,8 @@ extension EvmTransactionConverter { incomingEvents: transferEvents(internalTransactions: internalTransactions) + transferEvents(incomingEip20Transfers: incomingEip20Transfers) + transferEvents(incomingEip721Transfers: incomingEip721Transfers) + transferEvents(incomingEip1155Transfers: incomingEip1155Transfers), outgoingEvents: transferEvents(outgoingEip20Transfers: outgoingEip20Transfers) + - transferEvents(outgoingEip721Transfers: outgoingEip721Transfers) + transferEvents(outgoingEip1155Transfers: outgoingEip1155Transfers) + transferEvents(outgoingEip721Transfers: outgoingEip721Transfers) + transferEvents(outgoingEip1155Transfers: outgoingEip1155Transfers), + spam: spam ) } @@ -349,7 +350,7 @@ extension EvmTransactionConverter { source: source, transaction: transaction, baseToken: baseToken, - ownTransaction: transaction.from == evmKit.address + ownTransaction: transaction.from == userAddress ) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmTransactionsAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmTransactionsAdapter.swift index 31db210b9a..6850dee581 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmTransactionsAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/Evm/EvmTransactionsAdapter.swift @@ -13,9 +13,9 @@ class EvmTransactionsAdapter: BaseEvmAdapter { private let evmTransactionSource: EvmKit.TransactionSource private let transactionConverter: EvmTransactionConverter - init(evmKitWrapper: EvmKitWrapper, source: TransactionSource, baseToken: MarketKit.Token, evmTransactionSource: EvmKit.TransactionSource, coinManager: CoinManager, evmLabelManager: EvmLabelManager) { + init(evmKitWrapper: EvmKitWrapper, source: TransactionSource, baseToken: MarketKit.Token, evmTransactionSource: EvmKit.TransactionSource, coinManager: CoinManager, evmLabelManager: EvmLabelManager, spamAddressManager: SpamAddressManager) { self.evmTransactionSource = evmTransactionSource - transactionConverter = EvmTransactionConverter(source: source, baseToken: baseToken, coinManager: coinManager, evmKitWrapper: evmKitWrapper, evmLabelManager: evmLabelManager) + transactionConverter = EvmTransactionConverter(source: source, baseToken: baseToken, coinManager: coinManager, blockchainType: evmKitWrapper.blockchainType, userAddress: evmKitWrapper.evmKit.address, evmLabelManager: evmLabelManager, spamAddressManager: spamAddressManager) super.init(evmKitWrapper: evmKitWrapper, decimals: EvmAdapter.decimals) } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressSecurityCheckerChain.swift b/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressSecurityCheckerChain.swift new file mode 100644 index 0000000000..39298cf995 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Address/AddressSecurityCheckerChain.swift @@ -0,0 +1,48 @@ +import Foundation +import MarketKit +import RxCocoa +import RxRelay +import RxSwift + +protocol IAddressSecurityCheckerItem: AnyObject { + func handle(address: Address) -> Single +} + +class AddressSecurityCheckerChain { + private let disposeBag = DisposeBag() + private var handlers = [IAddressSecurityCheckerItem]() +} + +extension AddressSecurityCheckerChain { + @discardableResult func append(handlers: [IAddressSecurityCheckerItem]) -> Self { + self.handlers.append(contentsOf: handlers) + return self + } + + @discardableResult func append(handler: IAddressSecurityCheckerItem) -> Self { + handlers.append(handler) + return self + } + + func handle(address: Address) -> Single<[SecurityCheckResult]> { + Single.zip(handlers.map { handler -> Single in + handler.handle(address: address) + }) + } +} + +extension AddressSecurityCheckerChain { + public enum SecurityCheckResult { + case valid + case spam(transactionHash: String) + case sanctioned(description: String) + + public var description: String? { + switch self { + case .valid: return nil + case let .spam(transactionHash): return "Possibly phishing address. Transaction hash: \(transactionHash)" + case let .sanctioned(description): return description + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Address/SpamAddressDetector.swift b/UnstoppableWallet/UnstoppableWallet/Core/Address/SpamAddressDetector.swift new file mode 100644 index 0000000000..ec90e60738 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Address/SpamAddressDetector.swift @@ -0,0 +1,24 @@ +import RxSwift + +class SpamAddressDetector { + private let spamAddressManager: SpamAddressManager + + init() { + spamAddressManager = App.shared.spamAddressManager + } +} + +extension SpamAddressDetector: IAddressSecurityCheckerItem { + func handle(address: Address) -> Single { + let result: AddressSecurityCheckerChain.SecurityCheckResult + + let spamAddress = spamAddressManager.find(address: address.raw.uppercased()) + if let spamAddress { + result = .spam(transactionHash: spamAddress.transactionHash.hs.hexString) + } else { + result = .valid + } + + return Single.just(result) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/App.swift b/UnstoppableWallet/UnstoppableWallet/Core/App.swift index 96b20a43b8..e31f0fb7be 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/App.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/App.swift @@ -100,6 +100,7 @@ class App { let statManager: StatManager let tonConnectManager: TonConnectManager + let spamAddressManager: SpamAddressManager let purchaseManager: PurchaseManager @@ -200,8 +201,11 @@ class App { let restoreStateStorage = RestoreStateStorage(dbPool: dbPool) restoreStateManager = RestoreStateManager(storage: restoreStateStorage) + let spamAddressStorage = try SpamAddressStorage(dbPool: dbPool) + spamAddressManager = SpamAddressManager(storage: spamAddressStorage, marketKit: marketKit, coinManager: coinManager) + let evmAccountManagerFactory = EvmAccountManagerFactory(accountManager: accountManager, walletManager: walletManager, restoreStateManager: restoreStateManager, marketKit: marketKit) - evmBlockchainManager = EvmBlockchainManager(syncSourceManager: evmSyncSourceManager, testNetManager: testNetManager, marketKit: marketKit, accountManagerFactory: evmAccountManagerFactory) + evmBlockchainManager = EvmBlockchainManager(syncSourceManager: evmSyncSourceManager, testNetManager: testNetManager, marketKit: marketKit, accountManagerFactory: evmAccountManagerFactory, spamAddressManager: spamAddressManager) let hsLabelProvider = HsLabelProvider(networkManager: networkManager) let evmLabelStorage = EvmLabelStorage(dbPool: dbPool) @@ -266,7 +270,8 @@ class App { tonKitManager: tonKitManager, restoreSettingsManager: restoreSettingsManager, coinManager: coinManager, - evmLabelManager: evmLabelManager + evmLabelManager: evmLabelManager, + spamAddressManager: spamAddressManager ) adapterManager = AdapterManager( adapterFactory: adapterFactory, @@ -342,11 +347,12 @@ class App { statManager: statManager, walletConnectSocketConnectionService: walletConnectSocketConnectionService, nftMetadataSyncer: nftMetadataSyncer, - tonKitManager: tonKitManager + tonKitManager: tonKitManager, + spamAddressManager: spamAddressManager ) } - func newSendEnabled(wallet: Wallet) -> Bool { + func newSendEnabled(wallet _: Wallet) -> Bool { true // switch wallet.token.blockchainType { // case .ton: return true diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift index 55d8318506..8739aef2aa 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AdapterFactory.swift @@ -14,10 +14,10 @@ class AdapterFactory { private let restoreSettingsManager: RestoreSettingsManager private let coinManager: CoinManager private let evmLabelManager: EvmLabelManager - + private let spamAddressManager: SpamAddressManager init(evmBlockchainManager: EvmBlockchainManager, evmSyncSourceManager: EvmSyncSourceManager, binanceKitManager: BinanceKitManager, btcBlockchainManager: BtcBlockchainManager, tronKitManager: TronKitManager, tonKitManager: TonKitManager, - restoreSettingsManager: RestoreSettingsManager, coinManager: CoinManager, evmLabelManager: EvmLabelManager) + restoreSettingsManager: RestoreSettingsManager, coinManager: CoinManager, evmLabelManager: EvmLabelManager, spamAddressManager: SpamAddressManager) { self.evmBlockchainManager = evmBlockchainManager self.evmSyncSourceManager = evmSyncSourceManager @@ -28,6 +28,7 @@ class AdapterFactory { self.restoreSettingsManager = restoreSettingsManager self.coinManager = coinManager self.evmLabelManager = evmLabelManager + self.spamAddressManager = spamAddressManager } private func evmAdapter(wallet: Wallet) -> IAdapter? { @@ -52,7 +53,7 @@ class AdapterFactory { return nil } - return try? Eip20Adapter(evmKitWrapper: evmKitWrapper, contractAddress: address, wallet: wallet, baseToken: baseToken, coinManager: coinManager, evmLabelManager: evmLabelManager) + return try? Eip20Adapter(evmKitWrapper: evmKitWrapper, contractAddress: address, wallet: wallet, baseToken: baseToken, coinManager: coinManager, evmLabelManager: evmLabelManager, spamAddressManager: spamAddressManager) } private func tronAdapter(wallet: Wallet) -> IAdapter? { @@ -80,7 +81,7 @@ extension AdapterFactory { let baseToken = evmBlockchainManager.baseToken(blockchainType: blockchainType) { let syncSource = evmSyncSourceManager.syncSource(blockchainType: blockchainType) - return EvmTransactionsAdapter(evmKitWrapper: evmKitWrapper, source: transactionSource, baseToken: baseToken, evmTransactionSource: syncSource.transactionSource, coinManager: coinManager, evmLabelManager: evmLabelManager) + return EvmTransactionsAdapter(evmKitWrapper: evmKitWrapper, source: transactionSource, baseToken: baseToken, evmTransactionSource: syncSource.transactionSource, coinManager: coinManager, evmLabelManager: evmLabelManager, spamAddressManager: spamAddressManager) } return nil @@ -154,11 +155,13 @@ extension AdapterFactory { if let tonKit = try? tonKitManager.tonKit(account: wallet.account) { return TonAdapter(tonKit: tonKit) } + case let (.jetton(address), .ton): do { let tonKit = try tonKitManager.tonKit(account: wallet.account) return try JettonAdapter(tonKit: tonKit, address: address) } catch {} + default: () } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Factories/AddressSecurityCheckerFactory.swift b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AddressSecurityCheckerFactory.swift new file mode 100644 index 0000000000..85ea2cc9bf --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Factories/AddressSecurityCheckerFactory.swift @@ -0,0 +1,30 @@ +import MarketKit + +enum AddressSecurityCheckerFactory { + static func securityCheckerChainHandlers(blockchainType: BlockchainType) -> [IAddressSecurityCheckerItem] { + switch blockchainType { + case .ethereum, .gnosis, .fantom, .polygon, .arbitrumOne, .avalanche, .optimism, .binanceSmartChain, .base: + let evmAddressSecurityCheckerItem = SpamAddressDetector() + + var handlers = [IAddressSecurityCheckerItem]() + handlers.append(evmAddressSecurityCheckerItem) + + return handlers + default: + return [] + } + } + + static func securityCheckerChain(blockchainType: BlockchainType?) -> AddressSecurityCheckerChain { + if let blockchainType { + return AddressSecurityCheckerChain().append(handlers: securityCheckerChainHandlers(blockchainType: blockchainType)) + } + + var handlers = [IAddressSecurityCheckerItem]() + for blockchainType in BlockchainType.supported { + handlers.append(contentsOf: securityCheckerChainHandlers(blockchainType: blockchainType)) + } + + return AddressSecurityCheckerChain().append(handlers: handlers) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift index 352ff056fb..3f2f3d98ed 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift @@ -24,6 +24,7 @@ class AppManager { private let walletConnectSocketConnectionService: WalletConnectSocketConnectionService private let nftMetadataSyncer: NftMetadataSyncer private let tonKitManager: TonKitManager + private let spamAddressManager: SpamAddressManager private let didBecomeActiveSubject = PublishSubject() private let willEnterForegroundSubjectOld = PublishSubject() @@ -36,7 +37,8 @@ class AppManager { appVersionManager: AppVersionManager, rateAppManager: RateAppManager, logRecordManager: LogRecordManager, deepLinkManager: DeepLinkManager, evmLabelManager: EvmLabelManager, balanceHiddenManager: BalanceHiddenManager, statManager: StatManager, - walletConnectSocketConnectionService: WalletConnectSocketConnectionService, nftMetadataSyncer: NftMetadataSyncer, tonKitManager: TonKitManager) + walletConnectSocketConnectionService: WalletConnectSocketConnectionService, nftMetadataSyncer: NftMetadataSyncer, tonKitManager: TonKitManager, + spamAddressManager: SpamAddressManager) { self.accountManager = accountManager self.walletManager = walletManager @@ -57,6 +59,7 @@ class AppManager { self.walletConnectSocketConnectionService = walletConnectSocketConnectionService self.nftMetadataSyncer = nftMetadataSyncer self.tonKitManager = tonKitManager + self.spamAddressManager = spamAddressManager } private func warmUp() { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift index 7adf4ce638..83a53c12f8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift @@ -35,9 +35,10 @@ extension DeepLinkManager { return true } - if ((scheme == DeepLinkManager.deepLinkScheme && (host == Self.tonDeepLinkHost || host == Self.tonUniversalHost)) || - (scheme == "https" && host == Self.deepLinkScheme && path == "/\(Self.tonUniversalHost)")), - let parameters = try? TonConnectManager.parseParameters(queryItems: queryItems) { + if (scheme == DeepLinkManager.deepLinkScheme && (host == Self.tonDeepLinkHost || host == Self.tonUniversalHost)) || + (scheme == "https" && host == Self.deepLinkScheme && path == "/\(Self.tonUniversalHost)"), + let parameters = try? TonConnectManager.parseParameters(queryItems: queryItems) + { newSchemeRelay.accept(.tonConnect(parameters: parameters)) return true } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmAccountManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmAccountManager.swift index 45b333037b..7e013e61bf 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmAccountManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmAccountManager.swift @@ -45,8 +45,6 @@ class EvmAccountManager { return } -// print("Subscribe: \(evmKitWrapper.evmKit.networkType)") - evmKitWrapper.evmKit.allTransactionsPublisher .receive(on: DispatchQueue.global(qos: .userInitiated)) .sink { [weak self] fullTransactions, initial in @@ -56,8 +54,6 @@ class EvmAccountManager { } private func handle(fullTransactions: [FullTransaction], initial: Bool) { -// print("Tx Sync: \(blockchainType): full transactions: \(fullTransactions.count); initial: \(initial)") - guard let account = accountManager.activeAccount else { return } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmBlockchainManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmBlockchainManager.swift index 6c6facd5a5..311a7089f8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmBlockchainManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmBlockchainManager.swift @@ -19,6 +19,7 @@ class EvmBlockchainManager { private let testNetManager: TestNetManager private let marketKit: MarketKit.Kit private let accountManagerFactory: EvmAccountManagerFactory + private let spamAddressManager: SpamAddressManager private var evmKitManagerMap = [BlockchainType: EvmKitManager]() private var evmAccountManagerMap = [BlockchainType: EvmAccountManager]() @@ -31,11 +32,12 @@ class EvmBlockchainManager { } } - init(syncSourceManager: EvmSyncSourceManager, testNetManager: TestNetManager, marketKit: MarketKit.Kit, accountManagerFactory: EvmAccountManagerFactory) { + init(syncSourceManager: EvmSyncSourceManager, testNetManager: TestNetManager, marketKit: MarketKit.Kit, accountManagerFactory: EvmAccountManagerFactory, spamAddressManager: SpamAddressManager) { self.syncSourceManager = syncSourceManager self.testNetManager = testNetManager self.marketKit = marketKit self.accountManagerFactory = accountManagerFactory + self.spamAddressManager = spamAddressManager } private func evmManagers(blockchainType: BlockchainType) -> (EvmKitManager, EvmAccountManager) { @@ -49,6 +51,8 @@ class EvmBlockchainManager { evmKitManagerMap[blockchainType] = evmKitManager evmAccountManagerMap[blockchainType] = evmAccountManager + spamAddressManager.subscribeToKitCreation(evmKitManager: evmKitManager, blockchainType: blockchainType) + return (evmKitManager, evmAccountManager) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmKitManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmKitManager.swift index f3350990f4..d331b48d9d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmKitManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/EvmKitManager.swift @@ -1,3 +1,4 @@ +import BigInt import Eip20Kit import EvmKit import Foundation @@ -18,7 +19,7 @@ class EvmKitManager { private let evmKitCreatedRelay = PublishRelay() private let evmKitUpdatedRelay = PublishRelay() - private var currentAccount: Account? + private(set) var currentAccount: Account? private let queue = DispatchQueue(label: "\(AppConfig.label).ethereum-kit-manager", qos: .userInitiated) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/SpamAddressManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/SpamAddressManager.swift new file mode 100644 index 0000000000..31798c2aba --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/SpamAddressManager.swift @@ -0,0 +1,216 @@ +import BigInt +import Combine +import Eip20Kit +import EvmKit +import Foundation +import MarketKit +import NftKit +import RxSwift + +class SpamAddressManager { + private let storage: SpamAddressStorage + private let coinManager: CoinManager + private let disposeBag = DisposeBag() + private var cancellables = Set() + private let coinValueLimits: [String: Decimal] = AppConfig.spamCoinValueLimits + private let coins: [FullCoin] + + init(storage: SpamAddressStorage, marketKit: MarketKit.Kit, coinManager: CoinManager) { + self.storage = storage + self.coinManager = coinManager + coins = (try? marketKit.fullCoins(coinUids: Array(coinValueLimits.keys))) ?? [] + } + + private func scanSpamAddresses(fullTransaction: FullTransaction, userAddress: EvmKit.Address, spamConfig: SpamConfig) -> [EvmKit.Address] { + let transaction = fullTransaction.transaction + let baseCoinValue = spamConfig.baseCoinValue + let coinsMap = spamConfig.coinsMap + let blockchainType = spamConfig.blockchainType + + switch fullTransaction.decoration { + case let decoration as IncomingDecoration: + if let from = transaction.from, decoration.value <= baseCoinValue { + return [from] + } + + case let decoration as UnknownTransactionDecoration: + if transaction.from == userAddress { + return [] + } else if transaction.to != userAddress { + var spamAddresses = [EvmKit.Address]() + + let internalTransactions = decoration.internalTransactions.filter { $0.to == userAddress } + let (totalIncomingValue, addresses) = internalTransactions.reduce(into: (value: BigUInt(0), addresses: [EvmKit.Address]())) { acc, internalTransaction in + if internalTransaction.to == userAddress { + acc.value += internalTransaction.value + acc.addresses.append(internalTransaction.from) + } + } + if totalIncomingValue <= baseCoinValue { + spamAddresses.append(contentsOf: addresses.map { $0 }) + } + + let eip20Transfers = decoration.eventInstances.compactMap { $0 as? TransferEventInstance } + for transfer in eip20Transfers { + let query = TokenQuery(blockchainType: blockchainType, tokenType: .eip20(address: transfer.contractAddress.hex)) + + var isSpam: Bool + if let minValue = coinsMap[transfer.contractAddress] { + isSpam = transfer.value <= minValue + } else if let _ = try? coinManager.token(query: query) { + isSpam = transfer.value == 0 + } else { + isSpam = true // Unknown token is considered spam + } + + if isSpam { + let counterpartyAddress = transfer.from == userAddress ? transfer.to : transfer.from + spamAddresses.append(counterpartyAddress) + } else { + return [] + } + } + + let eip721Transfers = decoration.eventInstances.compactMap { $0 as? Eip721TransferEventInstance } + let eip1155Transfers = decoration.eventInstances.compactMap { $0 as? Eip1155TransferEventInstance } + + if !eip721Transfers.isEmpty || !eip1155Transfers.isEmpty { + return [] + } + + return spamAddresses + } + + default: () + } + + return [] + } + + private func spamConfig(blockchainType: BlockchainType) -> SpamConfig { + let tokens = coins.reduce(into: []) { result, coin in + result.append(contentsOf: coin.tokens.filter { $0.blockchainType == blockchainType }) + } + var baseCoinValue = BigUInt.zero + let coinsMap = tokens.reduce(into: [EvmKit.Address: BigUInt]()) { result, token in + guard let value = coinValueLimits[token.coin.uid] else { + return + } + + if case let .eip20(addressString) = token.type, let address = try? EvmKit.Address(hex: addressString) { + result[address] = token.fractionalMonetaryValue(value: value) + } else if case .native = token.type { + baseCoinValue = token.fractionalMonetaryValue(value: value) + } + } + + return SpamConfig(baseCoinValue: baseCoinValue, coinsMap: coinsMap, blockchainType: blockchainType) + } + + private func handleEvmKitCreated(evmKitManager: EvmKitManager?, blockchainType: BlockchainType) { + guard let evmKitWrapper = evmKitManager?.evmKitWrapper, let currentAccount = evmKitManager?.currentAccount else { + return + } + + let spamConfig = spamConfig(blockchainType: blockchainType) + evmKitWrapper.evmKit.allTransactionsPublisher + .receive(on: DispatchQueue.global(qos: .userInitiated)) + .sink { [weak self] fullTransactions, _ in + self?.handle(fullTransactions: fullTransactions, userAddress: evmKitWrapper.evmKit.address, spamConfig: spamConfig) + } + .store(in: &cancellables) + + sync(evmKit: evmKitWrapper.evmKit, account: currentAccount, spamConfig: spamConfig) + } + + private func sync(evmKit: EvmKit.Kit, account: Account, spamConfig: SpamConfig) { + let spamScanState = try? storage.find(blockchainTypeUid: spamConfig.blockchainType.uid, accountUid: account.id) + let fullTransactions = evmKit.allTransactionsAfter(transactionHash: spamScanState?.lastTransactionHash) + let lastTransactionHash = handle(fullTransactions: fullTransactions, userAddress: evmKit.address, spamConfig: spamConfig) + + if let lastTransactionHash { + let spamScanState = SpamScanState(blockchainTypeUid: spamConfig.blockchainType.uid, accountUid: account.id, lastTransactionHash: lastTransactionHash) + try? storage.save(spamScanState: spamScanState) + } + } + + private func handle(fullTransactions: [FullTransaction], userAddress: EvmKit.Address, spamConfig: SpamConfig) -> Data? { + let spamAddresses = fullTransactions.reduce(into: [SpamAddress]()) { acc, fullTransaction in + let spamAddresses = scanSpamAddresses(fullTransaction: fullTransaction, userAddress: userAddress, spamConfig: spamConfig) + acc.append(contentsOf: spamAddresses.map { + SpamAddress(transactionHash: fullTransaction.transaction.hash, address: Address(raw: $0.eip55.uppercased(), blockchainType: spamConfig.blockchainType)) + }) + } + do { + try storage.save(spamAddresses: spamAddresses) + } catch {} + + let transactions = fullTransactions.map(\.transaction) + let sortedTransactions = transactions.sorted { tx1, tx2 in + if tx1.timestamp != tx2.timestamp { return tx1.timestamp > tx2.timestamp } + if let index1 = tx1.transactionIndex, let index2 = tx2.transactionIndex, index1 != index2 { + return index1 > index2 + } + return tx1.hash > tx2.hash + } + return sortedTransactions.first?.hash + } +} + +extension SpamAddressManager { + func subscribeToKitCreation(evmKitManager: EvmKitManager, blockchainType: BlockchainType) { + subscribe(ConcurrentDispatchQueueScheduler(qos: .userInitiated), disposeBag, evmKitManager.evmKitCreatedObservable) { [weak self, weak evmKitManager] in + self?.handleEvmKitCreated(evmKitManager: evmKitManager, blockchainType: blockchainType) + } + } + + func find(address: String) -> SpamAddress? { + try? storage.find(address: address) + } + + func isSpam(transactionHash: Data) -> Bool { + (try? storage.isSpam(transactionHash: transactionHash)) ?? false + } +} + +extension SpamAddressManager { + static func isSpam(appValues: [AppValue]) -> Bool { + let stableCoinUids = ["tether", "usd-coin", "dai", "binance-usd", "binance-peg-busd", "stasis-eurs"] + + for appValue in appValues { + let value = appValue.value + + switch appValue.kind { + case let .token(token): + if stableCoinUids.contains(token.coin.uid) { + if value > 0.01 { + return false + } + } else if value > 0 { + return false + } + case let .coin(coin, _): + if stableCoinUids.contains(coin.uid) { + if value > 0.01 { + return false + } + } else if value > 0 { + return false + } + case .nft: + if value > 0 { + return false + } + default: () + } + } + + return true + } +} + +struct SpamConfig { + let baseCoinValue: BigUInt + let coinsMap: [EvmKit.Address: BigUInt] + let blockchainType: BlockchainType +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift b/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift index 6043af4306..cf4fa7d58f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift @@ -41,6 +41,14 @@ enum AppConfig { .tron: "TQzANCd363w5CjRWDtswm8Y5nFPAdnwekF", .solana: "5gattKnvu5f1NDHBuZ6VfDXjRrJa9UcAArkZ3ys3e82F", ] + static var spamCoinValueLimits: [String: Decimal] = [ + "tether": 0.01, + "usd-coin": 0.01, + "dai": 0.01, + "binance-usd": 0.01, + "binance-peg-busd": 0.01, + "stasis-eurs": 0.01, + ] static var appVersion: String { Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/SpamAddressStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/SpamAddressStorage.swift new file mode 100644 index 0000000000..bc3b516224 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/SpamAddressStorage.swift @@ -0,0 +1,79 @@ +import Foundation +import GRDB + +class SpamAddressStorage { + private let dbPool: DatabasePool + + init(dbPool: DatabasePool) throws { + self.dbPool = dbPool + + try migrator.migrate(dbPool) + } + + var migrator: DatabaseMigrator { + var migrator = DatabaseMigrator() + + migrator.registerMigration("create SpamAddress") { db in + try db.create(table: SpamAddress.databaseTableName) { t in + t.column(SpamAddress.Columns.raw.name, .text).notNull() + t.column(SpamAddress.Columns.domain.name, .text) + t.column(SpamAddress.Columns.blockchainTypeUid.name, .text) + t.column(SpamAddress.Columns.transactionHash.name, .blob).notNull() + + t.primaryKey([SpamAddress.Columns.transactionHash.name, SpamAddress.Columns.raw.name], onConflict: .ignore) + } + } + + migrator.registerMigration("create SpamScanState") { db in + try db.create(table: SpamScanState.databaseTableName) { t in + t.column(SpamScanState.Columns.blockchainTypeUid.name, .text).notNull() + t.column(SpamScanState.Columns.accountUid.name, .text).notNull() + t.column(SpamScanState.Columns.lastTransactionHash.name, .blob).notNull() + + t.primaryKey([SpamScanState.Columns.blockchainTypeUid.name, SpamScanState.Columns.accountUid.name], onConflict: .ignore) + } + } + + return migrator + } +} + +extension SpamAddressStorage { + func save(spamAddresses: [SpamAddress]) throws { + try dbPool.write { db in + for spamAddress in spamAddresses { + try spamAddress.insert(db) + } + } + } + + func find(address: String) throws -> SpamAddress? { + try dbPool.read { db in + try SpamAddress.filter(SpamAddress.Columns.raw == address).fetchOne(db) + } + } + + func isSpam(transactionHash: Data) throws -> Bool { + try dbPool.read { db in + try SpamAddress.filter(SpamAddress.Columns.transactionHash == transactionHash).fetchOne(db) != nil + } + } + + func save(spamScanState: SpamScanState) throws { + try dbPool.write { db in + try spamScanState.save(db) + } + } + + func find(blockchainTypeUid: String, accountUid: String) throws -> SpamScanState? { + try dbPool.read { db in + try SpamScanState.filter(SpamScanState.Columns.blockchainTypeUid == blockchainTypeUid && SpamScanState.Columns.accountUid == accountUid).fetchOne(db) + } + } + + func clear() throws { + _ = try dbPool.write { db in + try SpamAddress.deleteAll(db) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Models/SpamAddress.swift b/UnstoppableWallet/UnstoppableWallet/Models/SpamAddress.swift new file mode 100644 index 0000000000..3d0e2667cd --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Models/SpamAddress.swift @@ -0,0 +1,40 @@ +import Foundation +import GRDB +import MarketKit + +public class SpamAddress: Record { + let transactionHash: Data + let address: Address + + init(transactionHash: Data, address: Address) { + self.transactionHash = transactionHash + self.address = address + + super.init() + } + + override public class var databaseTableName: String { + "spamAddresses" + } + + enum Columns: String, ColumnExpression, CaseIterable { + case transactionHash + case raw + case domain + case blockchainTypeUid + } + + required init(row: Row) throws { + transactionHash = row[Columns.transactionHash] + address = Address(raw: row[Columns.raw], domain: row[Columns.domain], blockchainType: BlockchainType(uid: row[Columns.blockchainTypeUid])) + + try super.init(row: row) + } + + override public func encode(to container: inout PersistenceContainer) throws { + container[Columns.transactionHash] = transactionHash + container[Columns.raw] = address.raw + container[Columns.domain] = address.domain + container[Columns.blockchainTypeUid] = address.blockchainType?.uid + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Models/SpamScanState.swift b/UnstoppableWallet/UnstoppableWallet/Models/SpamScanState.swift new file mode 100644 index 0000000000..99384e78e0 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Models/SpamScanState.swift @@ -0,0 +1,41 @@ +import Foundation +import GRDB +import MarketKit + +public class SpamScanState: Record { + let blockchainTypeUid: String + let accountUid: String + let lastTransactionHash: Data + + init(blockchainTypeUid: String, accountUid: String, lastTransactionHash: Data) { + self.blockchainTypeUid = blockchainTypeUid + self.accountUid = accountUid + self.lastTransactionHash = lastTransactionHash + + super.init() + } + + override public class var databaseTableName: String { + "spamScanStates" + } + + enum Columns: String, ColumnExpression, CaseIterable { + case blockchainTypeUid + case accountUid + case lastTransactionHash + } + + required init(row: Row) throws { + blockchainTypeUid = row[Columns.blockchainTypeUid] + accountUid = row[Columns.accountUid] + lastTransactionHash = row[Columns.lastTransactionHash] + + try super.init(row: row) + } + + override public func encode(to container: inout PersistenceContainer) throws { + container[Columns.blockchainTypeUid] = blockchainTypeUid + container[Columns.accountUid] = accountUid + container[Columns.lastTransactionHash] = lastTransactionHash + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/EvmIncomingTransactionRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/EvmIncomingTransactionRecord.swift index 30309f6c80..3163537c29 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/EvmIncomingTransactionRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/EvmIncomingTransactionRecord.swift @@ -6,19 +6,10 @@ class EvmIncomingTransactionRecord: EvmTransactionRecord { let from: String let value: AppValue - init(source: TransactionSource, transaction: Transaction, baseToken: Token, from: String, value: AppValue) { + init(source: TransactionSource, transaction: Transaction, baseToken: Token, from: String, value: AppValue, spam: Bool) { self.from = from self.value = value - let spam: Bool - - switch value.kind { - case .token: - spam = value.value == 0 - default: - spam = false - } - super.init(source: source, transaction: transaction, baseToken: baseToken, ownTransaction: false, spam: spam) } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/ExternalContractCallTransactionRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/ExternalContractCallTransactionRecord.swift index af22fd4972..610e05e7e2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/ExternalContractCallTransactionRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/ExternalContractCallTransactionRecord.swift @@ -6,12 +6,10 @@ class ExternalContractCallTransactionRecord: EvmTransactionRecord { let incomingEvents: [TransferEvent] let outgoingEvents: [TransferEvent] - init(source: TransactionSource, transaction: Transaction, baseToken: Token, incomingEvents: [TransferEvent], outgoingEvents: [TransferEvent]) { + init(source: TransactionSource, transaction: Transaction, baseToken: Token, incomingEvents: [TransferEvent], outgoingEvents: [TransferEvent], spam: Bool) { self.incomingEvents = incomingEvents self.outgoingEvents = outgoingEvents - let spam = TransactionRecord.isSpam(appValues: (incomingEvents + outgoingEvents).map(\.value)) - super.init(source: source, transaction: transaction, baseToken: baseToken, ownTransaction: false, spam: spam) } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/SwapTransactionRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/SwapTransactionRecord.swift index 21abd64b7e..3446607648 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/SwapTransactionRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Evm/SwapTransactionRecord.swift @@ -12,7 +12,6 @@ class SwapTransactionRecord: EvmTransactionRecord { self.exchangeAddress = exchangeAddress self.amountIn = amountIn self.amountOut = amountOut - self.recipient = recipient super.init(source: source, transaction: transaction, baseToken: baseToken, ownTransaction: true) diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/TransactionRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/TransactionRecord.swift index 6de5c7e408..5df9351770 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/TransactionRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/TransactionRecord.swift @@ -47,40 +47,6 @@ class TransactionRecord { open var mainValue: AppValue? { nil } - - static func isSpam(appValues: [AppValue]) -> Bool { - let stableCoinUids = ["tether", "usd-coin", "dai", "binance-usd", "binance-peg-busd", "stasis-eurs"] - - for appValue in appValues { - let value = appValue.value - - switch appValue.kind { - case let .token(token): - if stableCoinUids.contains(token.coin.uid) { - if value > 0.01 { - return false - } - } else if value > 0 { - return false - } - case let .coin(coin, _): - if stableCoinUids.contains(coin.uid) { - if value > 0.01 { - return false - } - } else if value > 0 { - return false - } - case .nft: - if value > 0 { - return false - } - default: () - } - } - - return true - } } extension TransactionRecord: Comparable { diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Tron/TronExternalContractCallTransactionRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Tron/TronExternalContractCallTransactionRecord.swift index c2ec4a3eff..ce6165491e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Tron/TronExternalContractCallTransactionRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionRecords/Tron/TronExternalContractCallTransactionRecord.swift @@ -10,7 +10,7 @@ class TronExternalContractCallTransactionRecord: TronTransactionRecord { self.incomingEvents = incomingEvents self.outgoingEvents = outgoingEvents - let spam = TransactionRecord.isSpam(appValues: (incomingEvents + outgoingEvents).map(\.value)) + let spam = SpamAddressManager.isSpam(appValues: (incomingEvents + outgoingEvents).map(\.value)) super.init(source: source, transaction: transaction, baseToken: baseToken, ownTransaction: false, spam: spam) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/AddressView/AddressViewModelNew.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/AddressView/AddressViewModelNew.swift index 3066651eb7..badc983dd5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/AddressView/AddressViewModelNew.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/AddressView/AddressViewModelNew.swift @@ -9,6 +9,7 @@ class AddressViewModelNew: ObservableObject { private var addressUriParser: AddressUriParser private let parserChain: AddressParserChain + private let securityCheckerChain: AddressSecurityCheckerChain let blockchainType: BlockchainType? private let contactBookManager: ContactBookManager = App.shared.contactManager @@ -38,6 +39,7 @@ class AddressViewModelNew: ObservableObject { init(initial: AddressInput.Initial) { addressUriParser = AddressParserFactory.parser(blockchainType: initial.blockchainType, tokenType: nil) parserChain = AddressParserFactory.parserChain(blockchainType: initial.blockchainType) + securityCheckerChain = AddressSecurityCheckerFactory.securityCheckerChain(blockchainType: initial.blockchainType) blockchainType = initial.blockchainType @@ -83,8 +85,18 @@ class AddressViewModelNew: ObservableObject { .handle(address: address) .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) .observeOn(MainScheduler.instance) + .flatMap { [weak self] parsedAddress -> Single<(Address?, [AddressSecurityCheckerChain.SecurityCheckResult])> in + guard let _address = parsedAddress, let securityCheckerChain = self?.securityCheckerChain else { + return .just((parsedAddress, [])) + } + + return securityCheckerChain.handle(address: _address).map { (_address, $0) } + } .subscribe( - onSuccess: { [weak self] in self?.sync($0, uri: uri) }, + onSuccess: { [weak self] parsedAddress, securityCheckResults in + print("securityCheckResults: \(securityCheckResults)") + self?.sync(parsedAddress, uri: uri, securityCheckResults: securityCheckResults) + }, onError: { [weak self] in self?.sync($0, text: text) } ) .disposed(by: addressParserDisposeBag) @@ -101,13 +113,13 @@ class AddressViewModelNew: ObservableObject { } } - private func sync(_ address: Address?, uri: AddressUri?) { + private func sync(_ address: Address?, uri: AddressUri?, securityCheckResults: [AddressSecurityCheckerChain.SecurityCheckResult]) { guard let address else { result = .idle return } - result = .valid(.init(address: address, uri: uri)) + result = .valid(.init(address: address, uri: uri, securityCheckResults: securityCheckResults)) } private func sync(_ error: Error, text: String) { @@ -179,6 +191,11 @@ enum AddressInput { struct Success: Equatable { let address: Address let uri: AddressUri? + let securityCheckResults: [AddressSecurityCheckerChain.SecurityCheckResult] + + static func == (lhs: AddressInput.Success, rhs: AddressInput.Success) -> Bool { + lhs.address == rhs.address && lhs.uri == rhs.uri + } } struct Failure: Equatable { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseMultiSwap/MultiSwapSettings/AddressMultiSwapSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseMultiSwap/MultiSwapSettings/AddressMultiSwapSettingsViewModel.swift index 88735e7e80..258136c2bc 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseMultiSwap/MultiSwapSettings/AddressMultiSwapSettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseMultiSwap/MultiSwapSettings/AddressMultiSwapSettingsViewModel.swift @@ -35,7 +35,7 @@ class AddressMultiSwapSettingsViewModel: ObservableObject, IMultiSwapSettingsFie if let initialAddress { address = initialAddress.title - addressResult = .valid(.init(address: initialAddress, uri: nil)) + addressResult = .valid(.init(address: initialAddress, uri: nil, securityCheckResults: [])) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ResendBitcoin/ResendBitcoinViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ResendBitcoin/ResendBitcoinViewModel.swift index dc6be638dc..89003e0c38 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ResendBitcoin/ResendBitcoinViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ResendBitcoin/ResendBitcoinViewModel.swift @@ -136,7 +136,6 @@ class ResendBitcoinViewModel { type: .regular ) ) - default: () } }