diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 3a93cdfa1..6ff5739da 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -7,16 +7,31 @@ objects = { /* Begin PBXBuildFile section */ + 3A20D93B2AE7F316005475A6 /* AdamantTransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A20D93A2AE7F316005475A6 /* AdamantTransactionDetails.swift */; }; + 3A2F55F92AC6F308000A3F26 /* CoinTransaction+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2F55F72AC6F308000A3F26 /* CoinTransaction+CoreDataClass.swift */; }; + 3A2F55FA2AC6F308000A3F26 /* CoinTransaction+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2F55F82AC6F308000A3F26 /* CoinTransaction+CoreDataProperties.swift */; }; + 3A2F55FC2AC6F885000A3F26 /* CoinStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2F55FB2AC6F885000A3F26 /* CoinStorage.swift */; }; + 3A2F55FE2AC6F90E000A3F26 /* AdamantCoinStorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2F55FD2AC6F90E000A3F26 /* AdamantCoinStorageService.swift */; }; 3A33F9FA2A7A53DA002B8003 /* EmojiUpdateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A33F9F92A7A53DA002B8003 /* EmojiUpdateType.swift */; }; + 3A4068342ACD7C18007E87BD /* CoinTransaction+TransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4068332ACD7C18007E87BD /* CoinTransaction+TransactionDetails.swift */; }; 3A41938F2A580C57006A6B22 /* AdamantRichTransactionReactService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A41938E2A580C57006A6B22 /* AdamantRichTransactionReactService.swift */; }; 3A4193912A580C85006A6B22 /* RichTransactionReactService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4193902A580C85006A6B22 /* RichTransactionReactService.swift */; }; 3A41939A2A5D554A006A6B22 /* Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4193992A5D554A006A6B22 /* Reaction.swift */; }; + 3A770E4C2AE14F130008D98F /* SimpleTransactionDetails+Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A770E4B2AE14F130008D98F /* SimpleTransactionDetails+Hashable.swift */; }; + 3A7BD00E2AA9BCE80045AAB0 /* VibroService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7BD00D2AA9BCE80045AAB0 /* VibroService.swift */; }; + 3A7BD0102AA9BD030045AAB0 /* AdamantVibroService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7BD00F2AA9BD030045AAB0 /* AdamantVibroService.swift */; }; + 3A7BD0122AA9BD5A0045AAB0 /* AdamantVibroType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7BD0112AA9BD5A0045AAB0 /* AdamantVibroType.swift */; }; 3A8875EF27BBF38D00436195 /* Parchment in Frameworks */ = {isa = PBXBuildFile; productRef = 3A8875EE27BBF38D00436195 /* Parchment */; }; 3A9015A52A614A18002A2464 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9015A42A614A18002A2464 /* EmojiService.swift */; }; 3A9015A72A614A62002A2464 /* AdamantEmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9015A62A614A62002A2464 /* AdamantEmojiService.swift */; }; 3A9015A92A615893002A2464 /* ChatMessagesListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9015A82A615893002A2464 /* ChatMessagesListViewModel.swift */; }; + 3A96E37A2AED27D7001F5A52 /* AdamantPartnerQRService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96E3792AED27D7001F5A52 /* AdamantPartnerQRService.swift */; }; + 3A96E37C2AED27F8001F5A52 /* PartnerQRService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96E37B2AED27F8001F5A52 /* PartnerQRService.swift */; }; 3AA2D5F7280EADE3000ED971 /* SocketService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA2D5F6280EADE3000ED971 /* SocketService.swift */; }; 3AA2D5FA280EAF5D000ED971 /* AdamantSocketService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA2D5F9280EAF5D000ED971 /* AdamantSocketService.swift */; }; + 3AA50DEF2AEBE65D00C58FC8 /* PartnerQRView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA50DEE2AEBE65D00C58FC8 /* PartnerQRView.swift */; }; + 3AA50DF12AEBE66A00C58FC8 /* PartnerQRViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA50DF02AEBE66A00C58FC8 /* PartnerQRViewModel.swift */; }; + 3AA50DF32AEBE67C00C58FC8 /* PartnerQRFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA50DF22AEBE67C00C58FC8 /* PartnerQRFactory.swift */; }; 3AC76E3D2AB09118008042C4 /* ElegantEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3AC76E3C2AB09118008042C4 /* ElegantEmojiPicker */; }; 3C06931576393125C61FB8F6 /* Pods_Adamant.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33975C0D891698AA7E74EBCC /* Pods_Adamant.framework */; }; 41047B70294B5EE10039E956 /* VisibleWalletsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B6F294B5EE10039E956 /* VisibleWalletsViewController.swift */; }; @@ -75,12 +90,9 @@ 41CE153A297FF98200CC9254 /* Web3Swift+Adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */; }; 41E3C9CC2A0E20F500AF0985 /* AdamantCoinTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41E3C9CB2A0E20F500AF0985 /* AdamantCoinTools.swift */; }; 4E9EE86F28CE793D008359F7 /* SafeDecimalRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9EE86E28CE793D008359F7 /* SafeDecimalRow.swift */; }; - 550066C5284D65DB0044C0B1 /* HealthCheckService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550066C4284D65DB0044C0B1 /* HealthCheckService.swift */; }; - 550066C7284D682D0044C0B1 /* AdamantHealthCheckService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550066C6284D682D0044C0B1 /* AdamantHealthCheckService.swift */; }; 551F66E628959A5300DE5D69 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551F66E528959A5200DE5D69 /* LoadingView.swift */; }; 551F66E82895B3DA00DE5D69 /* AdamantHealthCheckServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551F66E72895B3DA00DE5D69 /* AdamantHealthCheckServiceTests.swift */; }; 5551CC8F28A8B75300B52AD0 /* ApiServiceStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5551CC8E28A8B75300B52AD0 /* ApiServiceStub.swift */; }; - 5558A436282AAFCC0024DDD6 /* AdamantApi+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5558A435282AAFCC0024DDD6 /* AdamantApi+Status.swift */; }; 5558A438282AB9390024DDD6 /* NodeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5558A437282AB9390024DDD6 /* NodeStatus.swift */; }; 557AC306287B10D8004699D7 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 557AC305287B10D8004699D7 /* SnapKit */; }; 557AC308287B1365004699D7 /* CheckmarkRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557AC307287B1365004699D7 /* CheckmarkRowView.swift */; }; @@ -91,17 +103,16 @@ 55FBAAFB28C550920066E629 /* NodesAllowanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FBAAFA28C550920066E629 /* NodesAllowanceTests.swift */; }; 6403F5DB2272389800D58779 /* (null) in Sources */ = {isa = PBXBuildFile; }; 6403F5DE22723C6800D58779 /* DashMainnet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5DD22723C6800D58779 /* DashMainnet.swift */; }; - 6403F5E022723F6400D58779 /* DashWalletRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5DF22723F6400D58779 /* DashWalletRouter.swift */; }; + 6403F5E022723F6400D58779 /* DashWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5DF22723F6400D58779 /* DashWalletFactory.swift */; }; 6403F5E222723F7500D58779 /* DashWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5E122723F7500D58779 /* DashWallet.swift */; }; 6403F5E422723F8C00D58779 /* DashWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5E322723F8C00D58779 /* DashWalletService.swift */; }; 6403F5E622723FDA00D58779 /* DashWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5E522723FDA00D58779 /* DashWalletViewController.swift */; }; 6406D74A21C7F06000196713 /* SearchResultsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6406D74821C7F06000196713 /* SearchResultsViewController.xib */; }; 6414C18E217DF43100373FA6 /* String+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6414C18D217DF43100373FA6 /* String+adamant.swift */; }; - 6416B19D21AD7B92006089AC /* LskWalletRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6416B19C21AD7B92006089AC /* LskWalletRoutes.swift */; }; + 6416B19D21AD7B92006089AC /* LskWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6416B19C21AD7B92006089AC /* LskWalletFactory.swift */; }; 6416B19F21AD7CBE006089AC /* LskWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6416B19E21AD7CBE006089AC /* LskWalletViewController.swift */; }; 6416B1A121AD7D93006089AC /* LskTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6416B1A021AD7D93006089AC /* LskTransferViewController.swift */; }; 6416B1A321AD7EA1006089AC /* LskTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6416B1A221AD7EA1006089AC /* LskTransactionDetailsViewController.swift */; }; - 6416B1A521AEE157006089AC /* LskWalletService+Transfers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6416B1A421AEE157006089AC /* LskWalletService+Transfers.swift */; }; 6416B1A721B024B6006089AC /* LskWalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6416B1A621B024B6006089AC /* LskWalletService+Send.swift */; }; 644793C32166314A00FC4CF5 /* OnboardPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644793C22166314A00FC4CF5 /* OnboardPage.swift */; }; 644793C52166315900FC4CF5 /* OnboardPage.xib in Resources */ = {isa = PBXBuildFile; fileRef = 644793C42166315900FC4CF5 /* OnboardPage.xib */; }; @@ -113,11 +124,11 @@ 6449BA6C235CA0930033B936 /* ERC20WalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA62235CA0930033B936 /* ERC20WalletViewController.swift */; }; 6449BA6D235CA0930033B936 /* ERC20TransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA63235CA0930033B936 /* ERC20TransactionsViewController.swift */; }; 6449BA6F235CA0930033B936 /* ERC20WalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA65235CA0930033B936 /* ERC20WalletService+Send.swift */; }; - 6449BA70235CA0930033B936 /* ERC20WalletRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA66235CA0930033B936 /* ERC20WalletRouter.swift */; }; + 6449BA70235CA0930033B936 /* ERC20WalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA66235CA0930033B936 /* ERC20WalletFactory.swift */; }; 6449BA71235CA0930033B936 /* ERC20WalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA67235CA0930033B936 /* ERC20WalletService+RichMessageProvider.swift */; }; 644EC34D20EFA60900F40C73 /* AdamantApi+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC34C20EFA60900F40C73 /* AdamantApi+Delegates.swift */; }; 644EC34F20EFA77A00F40C73 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC34E20EFA77A00F40C73 /* Delegate.swift */; }; - 644EC35220EFA9A300F40C73 /* DelegateRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35120EFA9A300F40C73 /* DelegateRoutes.swift */; }; + 644EC35220EFA9A300F40C73 /* DelegatesFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35120EFA9A300F40C73 /* DelegatesFactory.swift */; }; 644EC35720EFAAB700F40C73 /* DelegatesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35520EFAAB700F40C73 /* DelegatesListViewController.swift */; }; 644EC35B20EFB8E900F40C73 /* AdamantDelegateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35920EFB8E900F40C73 /* AdamantDelegateCell.swift */; }; 644EC35E20F34F1E00F40C73 /* DelegateDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35D20F34F1E00F40C73 /* DelegateDetailsViewController.swift */; }; @@ -129,9 +140,6 @@ 645AE06621E67D3300AD3623 /* UITextField+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645AE06521E67D3300AD3623 /* UITextField+adamant.swift */; }; 645FEB34213E72C100D6BA2D /* OnboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645FEB32213E72C100D6BA2D /* OnboardViewController.swift */; }; 645FEB35213E72C100D6BA2D /* OnboardViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 645FEB33213E72C100D6BA2D /* OnboardViewController.xib */; }; - 6489794D24CE00C000C33A68 /* SwiftyOnboardPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6489794A24CE00C000C33A68 /* SwiftyOnboardPage.swift */; }; - 6489794E24CE00C000C33A68 /* SwiftyOnboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6489794B24CE00C000C33A68 /* SwiftyOnboard.swift */; }; - 6489794F24CE00C000C33A68 /* SwiftyOnboardOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6489794C24CE00C000C33A68 /* SwiftyOnboardOverlay.swift */; }; 648BCA6D213D384F00875EB5 /* AvatarService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648BCA6C213D384F00875EB5 /* AvatarService.swift */; }; 648C696F22915A12006645F5 /* DashTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648C696E22915A12006645F5 /* DashTransaction.swift */; }; 648C697122915CB8006645F5 /* BTCRPCServerResponce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648C697022915CB8006645F5 /* BTCRPCServerResponce.swift */; }; @@ -156,13 +164,12 @@ 649D6BF021BFF481009E727B /* AdamantChatsProvider+search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649D6BEF21BFF481009E727B /* AdamantChatsProvider+search.swift */; }; 649D6BF221C27D5C009E727B /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649D6BF121C27D5C009E727B /* SearchResultsViewController.swift */; }; 64A223D620F760BB005157CB /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D520F760BB005157CB /* Localization.swift */; }; - 64A223D820F7A08E005157CB /* LskApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D720F7A08E005157CB /* LskApiService.swift */; }; 64B5736F2209B892005DC968 /* BtcTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B5736E2209B892005DC968 /* BtcTransactionDetailsViewController.swift */; }; 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */; }; 64BD2B7720E2820300E2CD36 /* TransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7620E2820300E2CD36 /* TransactionDetails.swift */; }; 64C65F4523893C7600DC0425 /* OnboardOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C65F4423893C7600DC0425 /* OnboardOverlay.swift */; }; 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; - 64E1C82D222E95E2006C4DA7 /* DogeWalletRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C82C222E95E2006C4DA7 /* DogeWalletRoutes.swift */; }; + 64E1C82D222E95E2006C4DA7 /* DogeWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C82C222E95E2006C4DA7 /* DogeWalletFactory.swift */; }; 64E1C82F222E95F6006C4DA7 /* DogeWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */; }; 64E1C831222E9617006C4DA7 /* DogeWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */; }; 64E1C833222EA0F0006C4DA7 /* DogeWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C832222EA0F0006C4DA7 /* DogeWalletViewController.swift */; }; @@ -181,11 +188,29 @@ 9322E875297042F000B8357C /* ChatSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9322E874297042F000B8357C /* ChatSender.swift */; }; 9322E877297042FA00B8357C /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9322E876297042FA00B8357C /* ChatMessage.swift */; }; 9322E87B2970431200B8357C /* ChatMessageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9322E87A2970431200B8357C /* ChatMessageFactory.swift */; }; - 9324C75E297170600022D7EA /* RichTransactionStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9324C75D297170600022D7EA /* RichTransactionStatusService.swift */; }; - 9324C760297171040022D7EA /* AdamantRichTransactionStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9324C75F297171040022D7EA /* AdamantRichTransactionStatusService.swift */; }; + 9324C75E297170600022D7EA /* TransactionStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9324C75D297170600022D7EA /* TransactionStatusService.swift */; }; + 9324C760297171040022D7EA /* AdamantTransactionStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9324C75F297171040022D7EA /* AdamantTransactionStatusService.swift */; }; + 93294B7D2AAD067000911109 /* AppContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B7C2AAD067000911109 /* AppContainer.swift */; }; + 93294B822AAD0BB400911109 /* BtcWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B812AAD0BB400911109 /* BtcWalletFactory.swift */; }; + 93294B842AAD0C8F00911109 /* Assembler+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B832AAD0C8F00911109 /* Assembler+Extension.swift */; }; + 93294B872AAD0E0A00911109 /* AdmWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B852AAD0E0A00911109 /* AdmWallet.swift */; }; + 93294B882AAD0E0A00911109 /* AdmWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B862AAD0E0A00911109 /* AdmWalletService.swift */; }; + 93294B8E2AAD2C6B00911109 /* SwiftyOnboardPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B8B2AAD2C6B00911109 /* SwiftyOnboardPage.swift */; }; + 93294B8F2AAD2C6B00911109 /* SwiftyOnboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B8C2AAD2C6B00911109 /* SwiftyOnboard.swift */; }; + 93294B902AAD2C6B00911109 /* SwiftyOnboardOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B8D2AAD2C6B00911109 /* SwiftyOnboardOverlay.swift */; }; + 93294B962AAD320B00911109 /* ScreensFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B952AAD320B00911109 /* ScreensFactory.swift */; }; + 93294B982AAD364F00911109 /* AdamantScreensFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B972AAD364F00911109 /* AdamantScreensFactory.swift */; }; + 93294B9A2AAD624100911109 /* WalletFactoryCompose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B992AAD624100911109 /* WalletFactoryCompose.swift */; }; 932B34E92974AA4A002A75BA /* ChatPreservationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932B34E82974AA4A002A75BA /* ChatPreservationDelegate.swift */; }; 932BD15B29D2F75200AA1947 /* RichMessageProviderWithStatusCheck+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932BD15A29D2F75200AA1947 /* RichMessageProviderWithStatusCheck+Extension.swift */; }; 932F77592989F999006D8801 /* ChatCellManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932F77582989F999006D8801 /* ChatCellManager.swift */; }; + 9338AE7F2AEF43DA001D32DF /* NodesStorageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9338AE7E2AEF43DA001D32DF /* NodesStorageProtocol.swift */; }; + 9338AE812AEF4B8E001D32DF /* NodesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9338AE802AEF4B8E001D32DF /* NodesStorage.swift */; }; + 9338AE842AEF5EFA001D32DF /* APICoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9338AE832AEF5EFA001D32DF /* APICoreProtocol.swift */; }; + 9338AE862AEF6A97001D32DF /* APICore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9338AE852AEF6A97001D32DF /* APICore.swift */; }; + 9338AE8B2AEF7E37001D32DF /* APIParametersEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9338AE8A2AEF7E37001D32DF /* APIParametersEncoding.swift */; }; + 9338AE8D2AEF7E9C001D32DF /* BodyStringEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9338AE8C2AEF7E9C001D32DF /* BodyStringEncoding.swift */; }; + 9338AE8F2AEF8131001D32DF /* InternalAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9338AE8E2AEF8131001D32DF /* InternalAPIError.swift */; }; 9340078029AC341100A20622 /* ChatAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9340077F29AC341000A20622 /* ChatAction.swift */; }; 9342F6C22A6A35E300A9B39F /* CommonKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9342F6C12A6A35E300A9B39F /* CommonKit */; }; 9345769528FD0C34004E6C7A /* UIViewController+email.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9345769428FD0C34004E6C7A /* UIViewController+email.swift */; }; @@ -203,10 +228,22 @@ 93496BB62A6CAED100DD062F /* Roboto_400_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BAB2A6CAED100DD062F /* Roboto_400_normal.ttf */; }; 93496BB72A6CAED100DD062F /* Roboto_500_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BAC2A6CAED100DD062F /* Roboto_500_normal.ttf */; }; 93547BCA29E2262D00B0914B /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93547BC929E2262D00B0914B /* WelcomeViewController.swift */; }; - 935F53D629BE8F7400779492 /* RichTransactionStatusPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 935F53D529BE8F7400779492 /* RichTransactionStatusPublisher.swift */; }; + 935F53D629BE8F7400779492 /* TransactionStatusPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 935F53D529BE8F7400779492 /* TransactionStatusPublisher.swift */; }; + 9366588D2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9366588C2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift */; }; + 9366588F2B0AB97500BDB2D3 /* CoinsNodesListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9366588E2B0AB97500BDB2D3 /* CoinsNodesListMapper.swift */; }; + 936658912B0AB9DC00BDB2D3 /* NodeWithGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658902B0AB9DC00BDB2D3 /* NodeWithGroup.swift */; }; + 936658932B0AC03700BDB2D3 /* CoinsNodesListStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658922B0AC03700BDB2D3 /* CoinsNodesListStrings.swift */; }; + 936658952B0AC15300BDB2D3 /* Node+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658942B0AC15300BDB2D3 /* Node+UI.swift */; }; + 936658972B0ACB1500BDB2D3 /* CoinsNodesListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658962B0ACB1500BDB2D3 /* CoinsNodesListViewModel.swift */; }; + 936658992B0AD32600BDB2D3 /* CoinsNodesListViewModel+ApiServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658982B0AD32600BDB2D3 /* CoinsNodesListViewModel+ApiServices.swift */; }; + 9366589B2B0AD3E600BDB2D3 /* WalletApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9366589A2B0AD3E600BDB2D3 /* WalletApiService.swift */; }; + 9366589D2B0ADBAF00BDB2D3 /* CoinsNodesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9366589C2B0ADBAF00BDB2D3 /* CoinsNodesListView.swift */; }; + 936658A32B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658A22B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift */; }; + 936658A52B0AE67A00BDB2D3 /* CoinsNodesListFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658A42B0AE67A00BDB2D3 /* CoinsNodesListFactory.swift */; }; 93684A2A29EFA28A00F9EFFE /* FixedTextMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93684A2929EFA28A00F9EFFE /* FixedTextMessageSizeCalculator.swift */; }; 9371130F2996EDA900F64CF9 /* ChatRefreshMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9371130E2996EDA900F64CF9 /* ChatRefreshMock.swift */; }; 9371E561295CD53100438F2C /* ChatLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9371E560295CD53100438F2C /* ChatLocalization.swift */; }; + 937736822B0949C500B35C7A /* NodeCell+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 937736812B0949C500B35C7A /* NodeCell+Model.swift */; }; 937751A52A68B3320054BD65 /* CommonKit in Frameworks */ = {isa = PBXBuildFile; productRef = 937751A42A68B3320054BD65 /* CommonKit */; }; 937751A72A68B33A0054BD65 /* CommonKit in Frameworks */ = {isa = PBXBuildFile; productRef = 937751A62A68B33A0054BD65 /* CommonKit */; }; 937751A92A68B3400054BD65 /* CommonKit in Frameworks */ = {isa = PBXBuildFile; productRef = 937751A82A68B3400054BD65 /* CommonKit */; }; @@ -216,6 +253,8 @@ 9377FBDF296C2A2F00C9211B /* ChatTransactionContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9377FBDE296C2A2F00C9211B /* ChatTransactionContentView.swift */; }; 9377FBE2296C2ACA00C9211B /* ChatTransactionContentView+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9377FBE1296C2ACA00C9211B /* ChatTransactionContentView+Model.swift */; }; 9382F61329DEC0A3005E6216 /* ChatModelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9382F61229DEC0A3005E6216 /* ChatModelView.swift */; }; + 938A46A42AE6103E00FC03DB /* HealthCheckWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938A46A32AE6103E00FC03DB /* HealthCheckWrapper.swift */; }; + 938A46A62AE6106300FC03DB /* BlockchainHealthCheckWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938A46A52AE6106300FC03DB /* BlockchainHealthCheckWrapper.swift */; }; 938F7D582955C1DA001915CA /* MessageKit in Frameworks */ = {isa = PBXBuildFile; productRef = 938F7D572955C1DA001915CA /* MessageKit */; }; 938F7D5B2955C8DA001915CA /* ChatDisplayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938F7D5A2955C8DA001915CA /* ChatDisplayManager.swift */; }; 938F7D5D2955C8F9001915CA /* ChatLayoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938F7D5C2955C8F9001915CA /* ChatLayoutManager.swift */; }; @@ -229,14 +268,41 @@ 9390C5052976B53000270CDF /* ChatDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9390C5042976B53000270CDF /* ChatDialog.swift */; }; 93996A972968209C008D080B /* ChatMessagesCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93996A962968209C008D080B /* ChatMessagesCollection.swift */; }; 9399F5ED29A85A48006C3E30 /* ChatCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9399F5EC29A85A48006C3E30 /* ChatCacheService.swift */; }; + 939FA3422B0D6F0000710EC6 /* SelfRemovableHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939FA3412B0D6F0000710EC6 /* SelfRemovableHostingController.swift */; }; 93A118512993167500E144CC /* ChatMessageBackgroundColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A118502993167500E144CC /* ChatMessageBackgroundColor.swift */; }; 93A118532993241D00E144CC /* ChatMessagesListFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A118522993241D00E144CC /* ChatMessagesListFactory.swift */; }; + 93A18C862AAEACC100D0AB98 /* AdamantWalletFactoryCompose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A18C852AAEACC100D0AB98 /* AdamantWalletFactoryCompose.swift */; }; + 93A18C892AAEAE7700D0AB98 /* WalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A18C882AAEAE7700D0AB98 /* WalletFactory.swift */; }; 93A91FD1297972B7001DB1F8 /* ChatScrollDownButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A91FD0297972B7001DB1F8 /* ChatScrollDownButton.swift */; }; 93A91FD329799298001DB1F8 /* ChatStartPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A91FD229799298001DB1F8 /* ChatStartPosition.swift */; }; + 93ADC17B2B08283500F2DF77 /* ForceQueryItemsEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ADC17A2B08283500F2DF77 /* ForceQueryItemsEncoding.swift */; }; + 93ADC17D2B083C3B00F2DF77 /* NodesAdditionalParamsStorageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ADC17C2B083C3B00F2DF77 /* NodesAdditionalParamsStorageProtocol.swift */; }; + 93ADC17F2B083D7A00F2DF77 /* NodesAdditionalParamsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ADC17E2B083D7A00F2DF77 /* NodesAdditionalParamsStorage.swift */; }; + 93ADE0712ACA66AF008ED641 /* VibrationSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ADE06E2ACA66AF008ED641 /* VibrationSelectionViewModel.swift */; }; + 93ADE0722ACA66AF008ED641 /* VibrationSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ADE06F2ACA66AF008ED641 /* VibrationSelectionView.swift */; }; + 93ADE0732ACA66AF008ED641 /* VibrationSelectionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ADE0702ACA66AF008ED641 /* VibrationSelectionFactory.swift */; }; + 93B28EC02B076667007F268B /* APIResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93B28EBF2B076667007F268B /* APIResponseModel.swift */; }; + 93B28EC22B076D31007F268B /* DashApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93B28EC12B076D31007F268B /* DashApiService.swift */; }; + 93B28EC52B076E2C007F268B /* DashBlockchainInfoDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93B28EC42B076E2C007F268B /* DashBlockchainInfoDTO.swift */; }; + 93B28EC82B076E68007F268B /* DashResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93B28EC72B076E68007F268B /* DashResponseDTO.swift */; }; + 93B28ECA2B076E88007F268B /* DashErrorDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93B28EC92B076E88007F268B /* DashErrorDTO.swift */; }; 93BF4A6629E4859900505CD0 /* DelegatesBottomPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93BF4A6529E4859900505CD0 /* DelegatesBottomPanel.swift */; }; 93BF4A6C29E4B4BF00505CD0 /* DelegatesBottomPanel+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93BF4A6B29E4B4BF00505CD0 /* DelegatesBottomPanel+Model.swift */; }; + 93C794442B07725C00408826 /* DashGetAddressBalanceDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C794432B07725C00408826 /* DashGetAddressBalanceDTO.swift */; }; + 93C794462B07768F00408826 /* DashGetRawTransactionDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C794452B07768F00408826 /* DashGetRawTransactionDTO.swift */; }; + 93C794482B0778C700408826 /* DashGetBlockDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C794472B0778C700408826 /* DashGetBlockDTO.swift */; }; + 93C7944A2B077A1C00408826 /* DashGetUnspentTransactionsDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C794492B077A1C00408826 /* DashGetUnspentTransactionsDTO.swift */; }; + 93C7944C2B077B2700408826 /* DashGetAddressTransactionIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C7944B2B077B2700408826 /* DashGetAddressTransactionIds.swift */; }; + 93C7944E2B077C1F00408826 /* DashSendRawTransactionDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C7944D2B077C1F00408826 /* DashSendRawTransactionDTO.swift */; }; 93CC8DC7296F00D6003772BF /* ChatTransactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CC8DC6296F00D6003772BF /* ChatTransactionContainerView.swift */; }; 93CC8DC9296F01DE003772BF /* ChatTransactionContainerView+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CC8DC8296F01DE003772BF /* ChatTransactionContainerView+Model.swift */; }; + 93CC94C12B17EE73004842AC /* EthApiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CC94C02B17EE73004842AC /* EthApiCore.swift */; }; + 93CCAE752B06CC3600EA5B94 /* LskNodeApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CCAE742B06CC3600EA5B94 /* LskNodeApiService.swift */; }; + 93CCAE772B06D6CC00EA5B94 /* LskServiceApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CCAE762B06D6CC00EA5B94 /* LskServiceApiService.swift */; }; + 93CCAE792B06D81D00EA5B94 /* DogeApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CCAE782B06D81D00EA5B94 /* DogeApiService.swift */; }; + 93CCAE7B2B06D9B500EA5B94 /* DogeBlocksDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CCAE7A2B06D9B500EA5B94 /* DogeBlocksDTO.swift */; }; + 93CCAE7E2B06DA6C00EA5B94 /* DogeBlockDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CCAE7D2B06DA6C00EA5B94 /* DogeBlockDTO.swift */; }; + 93CCAE802B06E2D100EA5B94 /* ApiServiceError+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CCAE7F2B06E2D100EA5B94 /* ApiServiceError+Extension.swift */; }; 93E1232F2A6DF8EF004DF33B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 93E123312A6DF8EF004DF33B /* InfoPlist.strings */; }; 93E123382A6DFD15004DF33B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 93E1233A2A6DFD15004DF33B /* Localizable.strings */; }; 93E1233F2A6DFE24004DF33B /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 93E123412A6DFE24004DF33B /* Localizable.stringsdict */; }; @@ -254,10 +320,16 @@ 93E5D4DC293000BE00439298 /* UnregisteredTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E5D4DA293000BE00439298 /* UnregisteredTransaction.swift */; }; 93E5D4DD293000BE00439298 /* UnregisteredTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E5D4DA293000BE00439298 /* UnregisteredTransaction.swift */; }; 93E5D4E02930029300439298 /* AdamantCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E5D4DF2930029300439298 /* AdamantCore+Extensions.swift */; }; - 93EE9C3329C2666200D9853F /* RichTransactionStatusSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93EE9C3229C2666200D9853F /* RichTransactionStatusSubscription.swift */; }; + 93E8EDCD2AF1BD65003E163C /* AdamantApiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E8EDCC2AF1BD65003E163C /* AdamantApiCore.swift */; }; + 93E8EDCF2AF1CD9F003E163C /* NodeStatusInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E8EDCE2AF1CD9F003E163C /* NodeStatusInfo.swift */; }; + 93E8EDD12AF1DF8E003E163C /* ServerResponse+Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E8EDD02AF1DF8E003E163C /* ServerResponse+Resolver.swift */; }; + 93EE9C3329C2666200D9853F /* TransactionStatusSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93EE9C3229C2666200D9853F /* TransactionStatusSubscription.swift */; }; 93F391502962F5D400BFD6AE /* SpinnerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93F3914F2962F5D400BFD6AE /* SpinnerView.swift */; }; 93FA403629401BFC00D20DB6 /* PopupKit in Frameworks */ = {isa = PBXBuildFile; productRef = 93FA403529401BFC00D20DB6 /* PopupKit */; }; - A50A41072822F8CE006BDFE1 /* BtcWalletRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A41032822F8CE006BDFE1 /* BtcWalletRoutes.swift */; }; + 93FC169B2B0197FD0062B507 /* BtcApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93FC169A2B0197FD0062B507 /* BtcApiService.swift */; }; + 93FC169D2B019F440062B507 /* EthApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93FC169C2B019F440062B507 /* EthApiService.swift */; }; + 93FC169F2B01A3630062B507 /* LskApiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93FC169E2B01A3630062B507 /* LskApiCore.swift */; }; + 93FC16A12B01DE120062B507 /* ERC20ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93FC16A02B01DE120062B507 /* ERC20ApiService.swift */; }; A50A41082822F8CE006BDFE1 /* BtcWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A41042822F8CE006BDFE1 /* BtcWalletService.swift */; }; A50A41092822F8CE006BDFE1 /* BtcWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A41052822F8CE006BDFE1 /* BtcWalletViewController.swift */; }; A50A410A2822F8CE006BDFE1 /* BtcWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A41062822F8CE006BDFE1 /* BtcWallet.swift */; }; @@ -346,7 +418,7 @@ E91947B020002393001362F8 /* AdamantApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91947AF20002393001362F8 /* AdamantApiService.swift */; }; E91947B22000246A001362F8 /* AdamantError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91947B12000246A001362F8 /* AdamantError.swift */; }; E91947B420002809001362F8 /* AdamantAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91947B320002809001362F8 /* AdamantAccount.swift */; }; - E91E5BF220DAF05500B06B3C /* EurekaNodeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91E5BF120DAF05500B06B3C /* EurekaNodeRow.swift */; }; + E91E5BF220DAF05500B06B3C /* NodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91E5BF120DAF05500B06B3C /* NodeCell.swift */; }; E9204B5220C9762400F3B9AB /* MessageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9204B5120C9762300F3B9AB /* MessageStatus.swift */; }; E921534E20EE1E8700C0843F /* EurekaAlertLabelRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E921534C20EE1E8700C0843F /* EurekaAlertLabelRow.swift */; }; E921534F20EE1E8700C0843F /* AlertLabelCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E921534D20EE1E8700C0843F /* AlertLabelCell.xib */; }; @@ -361,24 +433,22 @@ E926E02E213EAABF005E536B /* TransferViewControllerBase+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = E926E02D213EAABF005E536B /* TransferViewControllerBase+Alert.swift */; }; E926E032213EC43B005E536B /* FullscreenAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E926E031213EC43B005E536B /* FullscreenAlertView.swift */; }; E926E034213EC454005E536B /* FullscreenAlertView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E926E033213EC454005E536B /* FullscreenAlertView.xib */; }; - E9332B8921F1FA4400D56E72 /* OnboardRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9332B8821F1FA4400D56E72 /* OnboardRoutes.swift */; }; + E9332B8921F1FA4400D56E72 /* OnboardFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9332B8821F1FA4400D56E72 /* OnboardFactory.swift */; }; E933475B225539390083839E /* DogeGetTransactionsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E933475A225539390083839E /* DogeGetTransactionsResponse.swift */; }; E9393FAA2055D03300EE6F30 /* AdamantMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9393FA92055D03300EE6F30 /* AdamantMessage.swift */; }; E93B0D742028B21400126346 /* ChatsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93B0D732028B21400126346 /* ChatsProvider.swift */; }; E93B0D762028B28E00126346 /* AdamantChatsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93B0D752028B28E00126346 /* AdamantChatsProvider.swift */; }; E93D7ABE2052CEE1005D19DC /* NotificationsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93D7ABD2052CEE1005D19DC /* NotificationsService.swift */; }; E93D7AC02052CF63005D19DC /* AdamantNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93D7ABF2052CF63005D19DC /* AdamantNotificationService.swift */; }; - E93EB09F20DA3FA4001F9601 /* NodesEditorRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93EB09E20DA3FA4001F9601 /* NodesEditorRoutes.swift */; }; + E93EB09F20DA3FA4001F9601 /* NodesEditorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93EB09E20DA3FA4001F9601 /* NodesEditorFactory.swift */; }; E940086B2114A70600CD2D67 /* LskAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940086A2114A70600CD2D67 /* LskAccount.swift */; }; E940086E2114AA2E00CD2D67 /* WalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940086D2114AA2E00CD2D67 /* WalletService.swift */; }; E94008722114EACF00CD2D67 /* WalletAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94008712114EACF00CD2D67 /* WalletAccount.swift */; }; E940087B2114ED0600CD2D67 /* EthWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940087A2114ED0600CD2D67 /* EthWalletService.swift */; }; E940087D2114EDEE00CD2D67 /* EthWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940087C2114EDEE00CD2D67 /* EthWallet.swift */; }; - E94008802114EE2000CD2D67 /* AdmWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940087F2114EE2000CD2D67 /* AdmWallet.swift */; }; E94008832114EE4700CD2D67 /* LskWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94008822114EE4700CD2D67 /* LskWallet.swift */; }; E94008852114EE7500CD2D67 /* LskWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94008842114EE7500CD2D67 /* LskWalletService.swift */; }; E94008872114F05B00CD2D67 /* AddressValidationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94008862114F05B00CD2D67 /* AddressValidationResult.swift */; }; - E94008892114F0F700CD2D67 /* AdmWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94008882114F0F700CD2D67 /* AdmWalletService.swift */; }; E940088B2114F63000CD2D67 /* NSRegularExpression+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940088A2114F63000CD2D67 /* NSRegularExpression+adamant.swift */; }; E940088F2119A9E800CD2D67 /* BigInt+Decimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940088E2119A9E800CD2D67 /* BigInt+Decimal.swift */; }; E941CCDB20E786D800C96220 /* AccountHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = E941CCDA20E786D700C96220 /* AccountHeader.xib */; }; @@ -389,10 +459,10 @@ E9484B7D2285BAD9008E10F0 /* PrivateKeyGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9484B7C2285BAD8008E10F0 /* PrivateKeyGenerator.swift */; }; E9484B7F2285C016008E10F0 /* PKGeneratorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9484B7E2285C016008E10F0 /* PKGeneratorViewController.swift */; }; E94883E7203F07CD00F6E1B0 /* PassphraseValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94883E6203F07CD00F6E1B0 /* PassphraseValidation.swift */; }; - E948E03B20235E2300975D6B /* SettingsRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E948E03A20235E2300975D6B /* SettingsRoutes.swift */; }; + E948E03B20235E2300975D6B /* SettingsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E948E03A20235E2300975D6B /* SettingsFactory.swift */; }; E948E0482024F02700975D6B /* VersionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = E948E0472024F02700975D6B /* VersionFooter.xib */; }; E94E7B01205D3F090042B639 /* ChatListViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B00205D3F090042B639 /* ChatListViewController.xib */; }; - E94E7B08205D4CB80042B639 /* SharedRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94E7B07205D4CB80042B639 /* SharedRoutes.swift */; }; + E94E7B08205D4CB80042B639 /* ShareQRFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94E7B07205D4CB80042B639 /* ShareQRFactory.swift */; }; E94E7B0C205D5E4A0042B639 /* TransactionsListViewControllerBase.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B0B205D5E4A0042B639 /* TransactionsListViewControllerBase.xib */; }; E9502740202E257E002C1098 /* RepeaterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E950273F202E257E002C1098 /* RepeaterService.swift */; }; E950652120404BF0008352E5 /* AdamantUriBuilding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E950652020404BF0008352E5 /* AdamantUriBuilding.swift */; }; @@ -409,7 +479,7 @@ E95F856F2007B61D0070534A /* GetPublicKeyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F856E2007B61D0070534A /* GetPublicKeyResponse.swift */; }; E95F85712007D98D0070534A /* CurrencyFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85702007D98D0070534A /* CurrencyFormatterTests.swift */; }; E95F85752007E4790070534A /* HexAndBytesUtilitiesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85742007E4790070534A /* HexAndBytesUtilitiesTest.swift */; }; - E95F85802008C8D70070534A /* ChatsRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F857E2008C8D60070534A /* ChatsRoutes.swift */; }; + E95F85802008C8D70070534A /* ChatListFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F857E2008C8D60070534A /* ChatListFactory.swift */; }; E95F85852008CB3A0070534A /* ChatListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85842008CB3A0070534A /* ChatListViewController.swift */; }; E95F85B7200A4D8F0070534A /* TestTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85B6200A4D8F0070534A /* TestTools.swift */; }; E95F85BC200A4E670070534A /* ParsingModelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85BB200A4E670070534A /* ParsingModelsTests.swift */; }; @@ -449,8 +519,8 @@ E987024920C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = E987024820C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift */; }; E98FC34420F920BD00032D65 /* UIFont+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98FC34320F920BD00032D65 /* UIFont+adamant.swift */; }; E993301E212EF39700CD5200 /* EthTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993301D212EF39700CD5200 /* EthTransferViewController.swift */; }; - E993302021354B1800CD5200 /* AdmWalletRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993301F21354B1800CD5200 /* AdmWalletRoutes.swift */; }; - E993302221354BC300CD5200 /* EthWalletRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993302121354BC300CD5200 /* EthWalletRoutes.swift */; }; + E993302021354B1800CD5200 /* AdmWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993301F21354B1800CD5200 /* AdmWalletFactory.swift */; }; + E993302221354BC300CD5200 /* EthWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993302121354BC300CD5200 /* EthWalletFactory.swift */; }; E99330262136B0E500CD5200 /* TransferViewControllerBase+QR.swift in Sources */ = {isa = PBXBuildFile; fileRef = E99330252136B0E500CD5200 /* TransferViewControllerBase+QR.swift */; }; E9942B80203C058C00C163AF /* QRGeneratorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9942B7F203C058C00C163AF /* QRGeneratorViewController.swift */; }; E9942B84203CBFCE00C163AF /* AdamantQRTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9942B83203CBFCE00C163AF /* AdamantQRTools.swift */; }; @@ -465,19 +535,14 @@ E9981898212096ED0018C84C /* WalletViewControllerBase.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9981897212096ED0018C84C /* WalletViewControllerBase.xib */; }; E9A03FD220DBC0F2007653A1 /* NodeEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A03FD120DBC0F2007653A1 /* NodeEditorViewController.swift */; }; E9A03FD420DBC824007653A1 /* NodeVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A03FD320DBC824007653A1 /* NodeVersion.swift */; }; - E9A03FD620DBC8E2007653A1 /* AdamantApi+Peers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A03FD520DBC8E2007653A1 /* AdamantApi+Peers.swift */; }; - E9A03FD820DC0ABA007653A1 /* AdamantNodesSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A03FD720DC0ABA007653A1 /* AdamantNodesSource.swift */; }; - E9A03FDA20DC0B14007653A1 /* NodesSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A03FD920DC0B14007653A1 /* NodesSource.swift */; }; E9A174B32057EC47003667CD /* BackgroundFetchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A174B22057EC47003667CD /* BackgroundFetchService.swift */; }; E9A174B52057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A174B42057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift */; }; E9A174B72057F1B3003667CD /* AdamantChatsProvider+backgroundFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A174B62057F1B3003667CD /* AdamantChatsProvider+backgroundFetch.swift */; }; E9A174B920587B84003667CD /* notification.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = E9A174B820587B83003667CD /* notification.mp3 */; }; E9AA8BF82129F13000F9249F /* ComplexTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AA8BF72129F13000F9249F /* ComplexTransferViewController.swift */; }; E9AA8BFA212C166600F9249F /* EthWalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AA8BF9212C166600F9249F /* EthWalletService+Send.swift */; }; - E9AA8BFC212C169200F9249F /* EthWalletService+Transfers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AA8BFB212C169200F9249F /* EthWalletService+Transfers.swift */; }; E9AA8C02212C5BF500F9249F /* AdmWalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AA8C01212C5BF500F9249F /* AdmWalletService+Send.swift */; }; E9B1AA572121ACC000080A2A /* AdmWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B1AA562121ACBF00080A2A /* AdmWalletViewController.swift */; }; - E9B1AA592122D59600080A2A /* WalletsRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B1AA582122D59600080A2A /* WalletsRoutes.swift */; }; E9B1AA5B21283E0F00080A2A /* AdmTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B1AA5A21283E0F00080A2A /* AdmTransferViewController.swift */; }; E9B3D39A201F90570019EB36 /* AccountsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B3D399201F90570019EB36 /* AccountsProvider.swift */; }; E9B3D39E201F99F40019EB36 /* DataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B3D39D201F99F40019EB36 /* DataProvider.swift */; }; @@ -498,12 +563,10 @@ E9E7CD8B20026B0600DFC4DB /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD8A20026B0600DFC4DB /* AccountService.swift */; }; E9E7CD8D20026B6600DFC4DB /* DialogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD8C20026B6600DFC4DB /* DialogService.swift */; }; E9E7CD8F20026CD300DFC4DB /* AdamantDialogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD8E20026CD300DFC4DB /* AdamantDialogService.swift */; }; - E9E7CD9120026FA100DFC4DB /* SwinjectDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD9020026FA100DFC4DB /* SwinjectDependencies.swift */; }; + E9E7CD9120026FA100DFC4DB /* AppAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD9020026FA100DFC4DB /* AppAssembly.swift */; }; E9E7CD932002740500DFC4DB /* AdamantAccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD922002740500DFC4DB /* AdamantAccountService.swift */; }; - E9E7CDAF2002B8A100DFC4DB /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDAE2002B8A100DFC4DB /* Router.swift */; }; - E9E7CDB12002B97B00DFC4DB /* AccountRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDB02002B97B00DFC4DB /* AccountRoutes.swift */; }; - E9E7CDB32002B9FB00DFC4DB /* LoginRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDB22002B9FB00DFC4DB /* LoginRoutes.swift */; }; - E9E7CDB52002BA6900DFC4DB /* SwinjectedRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDB42002BA6900DFC4DB /* SwinjectedRouter.swift */; }; + E9E7CDB12002B97B00DFC4DB /* AccountFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDB02002B97B00DFC4DB /* AccountFactory.swift */; }; + E9E7CDB32002B9FB00DFC4DB /* LoginFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDB22002B9FB00DFC4DB /* LoginFactory.swift */; }; E9E7CDB72003994E00DFC4DB /* AdamantUtilities+extended.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDB62003994E00DFC4DB /* AdamantUtilities+extended.swift */; }; E9E7CDBE2003AEFB00DFC4DB /* CellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDBD2003AEFB00DFC4DB /* CellFactory.swift */; }; E9E7CDC02003AF6D00DFC4DB /* AdamantCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDBF2003AF6D00DFC4DB /* AdamantCellFactory.swift */; }; @@ -581,15 +644,30 @@ /* Begin PBXFileReference section */ 33975C0D891698AA7E74EBCC /* Pods_Adamant.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Adamant.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 36AB8CE9537B3B873972548B /* Pods_AdmCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AdmCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A20D93A2AE7F316005475A6 /* AdamantTransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantTransactionDetails.swift; sourceTree = ""; }; + 3A2F55F72AC6F308000A3F26 /* CoinTransaction+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoinTransaction+CoreDataClass.swift"; sourceTree = ""; }; + 3A2F55F82AC6F308000A3F26 /* CoinTransaction+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoinTransaction+CoreDataProperties.swift"; sourceTree = ""; }; + 3A2F55FB2AC6F885000A3F26 /* CoinStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinStorage.swift; sourceTree = ""; }; + 3A2F55FD2AC6F90E000A3F26 /* AdamantCoinStorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCoinStorageService.swift; sourceTree = ""; }; 3A33F9F92A7A53DA002B8003 /* EmojiUpdateType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiUpdateType.swift; sourceTree = ""; }; + 3A4068332ACD7C18007E87BD /* CoinTransaction+TransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoinTransaction+TransactionDetails.swift"; sourceTree = ""; }; 3A41938E2A580C57006A6B22 /* AdamantRichTransactionReactService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantRichTransactionReactService.swift; sourceTree = ""; }; 3A4193902A580C85006A6B22 /* RichTransactionReactService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTransactionReactService.swift; sourceTree = ""; }; 3A4193992A5D554A006A6B22 /* Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reaction.swift; sourceTree = ""; }; + 3A770E4B2AE14F130008D98F /* SimpleTransactionDetails+Hashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SimpleTransactionDetails+Hashable.swift"; sourceTree = ""; }; + 3A7BD00D2AA9BCE80045AAB0 /* VibroService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibroService.swift; sourceTree = ""; }; + 3A7BD00F2AA9BD030045AAB0 /* AdamantVibroService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantVibroService.swift; sourceTree = ""; }; + 3A7BD0112AA9BD5A0045AAB0 /* AdamantVibroType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantVibroType.swift; sourceTree = ""; }; 3A9015A42A614A18002A2464 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = ""; }; 3A9015A62A614A62002A2464 /* AdamantEmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantEmojiService.swift; sourceTree = ""; }; 3A9015A82A615893002A2464 /* ChatMessagesListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesListViewModel.swift; sourceTree = ""; }; + 3A96E3792AED27D7001F5A52 /* AdamantPartnerQRService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantPartnerQRService.swift; sourceTree = ""; }; + 3A96E37B2AED27F8001F5A52 /* PartnerQRService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartnerQRService.swift; sourceTree = ""; }; 3AA2D5F6280EADE3000ED971 /* SocketService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketService.swift; sourceTree = ""; }; 3AA2D5F9280EAF5D000ED971 /* AdamantSocketService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantSocketService.swift; sourceTree = ""; }; + 3AA50DEE2AEBE65D00C58FC8 /* PartnerQRView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartnerQRView.swift; sourceTree = ""; }; + 3AA50DF02AEBE66A00C58FC8 /* PartnerQRViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartnerQRViewModel.swift; sourceTree = ""; }; + 3AA50DF22AEBE67C00C58FC8 /* PartnerQRFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartnerQRFactory.swift; sourceTree = ""; }; 41047B6F294B5EE10039E956 /* VisibleWalletsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsViewController.swift; sourceTree = ""; }; 41047B71294B5F210039E956 /* VisibleWalletsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsTableViewCell.swift; sourceTree = ""; }; 41047B73294C61D10039E956 /* VisibleWalletsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsService.swift; sourceTree = ""; }; @@ -643,29 +721,25 @@ 41E3C9CB2A0E20F500AF0985 /* AdamantCoinTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCoinTools.swift; sourceTree = ""; }; 4A4D67BD3DC89C07D1351248 /* Pods-AdmCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AdmCore.release.xcconfig"; path = "Target Support Files/Pods-AdmCore/Pods-AdmCore.release.xcconfig"; sourceTree = ""; }; 4E9EE86E28CE793D008359F7 /* SafeDecimalRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeDecimalRow.swift; sourceTree = ""; }; - 550066C4284D65DB0044C0B1 /* HealthCheckService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthCheckService.swift; sourceTree = ""; }; - 550066C6284D682D0044C0B1 /* AdamantHealthCheckService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantHealthCheckService.swift; sourceTree = ""; }; 551F66E528959A5200DE5D69 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 551F66E72895B3DA00DE5D69 /* AdamantHealthCheckServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantHealthCheckServiceTests.swift; sourceTree = ""; }; 5551CC8E28A8B75300B52AD0 /* ApiServiceStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceStub.swift; sourceTree = ""; }; - 5558A435282AAFCC0024DDD6 /* AdamantApi+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Status.swift"; sourceTree = ""; }; 5558A437282AB9390024DDD6 /* NodeStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeStatus.swift; sourceTree = ""; }; 557AC307287B1365004699D7 /* CheckmarkRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkRowView.swift; sourceTree = ""; }; 55D1D854287B890300F94A4E /* AddressGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressGeneratorTests.swift; sourceTree = ""; }; 55E69E162868D7920025D82E /* CheckmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkView.swift; sourceTree = ""; }; 55FBAAFA28C550920066E629 /* NodesAllowanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesAllowanceTests.swift; sourceTree = ""; }; 6403F5DD22723C6800D58779 /* DashMainnet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashMainnet.swift; sourceTree = ""; }; - 6403F5DF22723F6400D58779 /* DashWalletRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashWalletRouter.swift; sourceTree = ""; }; + 6403F5DF22723F6400D58779 /* DashWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashWalletFactory.swift; sourceTree = ""; }; 6403F5E122723F7500D58779 /* DashWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashWallet.swift; sourceTree = ""; }; 6403F5E322723F8C00D58779 /* DashWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashWalletService.swift; sourceTree = ""; }; 6403F5E522723FDA00D58779 /* DashWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashWalletViewController.swift; sourceTree = ""; }; 6406D74821C7F06000196713 /* SearchResultsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchResultsViewController.xib; sourceTree = ""; }; 6414C18D217DF43100373FA6 /* String+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+adamant.swift"; sourceTree = ""; }; - 6416B19C21AD7B92006089AC /* LskWalletRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskWalletRoutes.swift; sourceTree = ""; }; + 6416B19C21AD7B92006089AC /* LskWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskWalletFactory.swift; sourceTree = ""; }; 6416B19E21AD7CBE006089AC /* LskWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskWalletViewController.swift; sourceTree = ""; }; 6416B1A021AD7D93006089AC /* LskTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskTransferViewController.swift; sourceTree = ""; }; 6416B1A221AD7EA1006089AC /* LskTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskTransactionDetailsViewController.swift; sourceTree = ""; }; - 6416B1A421AEE157006089AC /* LskWalletService+Transfers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LskWalletService+Transfers.swift"; sourceTree = ""; }; 6416B1A621B024B6006089AC /* LskWalletService+Send.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LskWalletService+Send.swift"; sourceTree = ""; }; 644793C22166314A00FC4CF5 /* OnboardPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardPage.swift; sourceTree = ""; }; 644793C42166315900FC4CF5 /* OnboardPage.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OnboardPage.xib; sourceTree = ""; }; @@ -677,11 +751,11 @@ 6449BA63235CA0930033B936 /* ERC20TransactionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ERC20TransactionsViewController.swift; sourceTree = ""; }; 6449BA64235CA0930033B936 /* ERC20WalletService+RichMessageProviderWithStatusCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ERC20WalletService+RichMessageProviderWithStatusCheck.swift"; sourceTree = ""; }; 6449BA65235CA0930033B936 /* ERC20WalletService+Send.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ERC20WalletService+Send.swift"; sourceTree = ""; }; - 6449BA66235CA0930033B936 /* ERC20WalletRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ERC20WalletRouter.swift; sourceTree = ""; }; + 6449BA66235CA0930033B936 /* ERC20WalletFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ERC20WalletFactory.swift; sourceTree = ""; }; 6449BA67235CA0930033B936 /* ERC20WalletService+RichMessageProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ERC20WalletService+RichMessageProvider.swift"; sourceTree = ""; }; 644EC34C20EFA60900F40C73 /* AdamantApi+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Delegates.swift"; sourceTree = ""; }; 644EC34E20EFA77A00F40C73 /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = ""; }; - 644EC35120EFA9A300F40C73 /* DelegateRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegateRoutes.swift; sourceTree = ""; }; + 644EC35120EFA9A300F40C73 /* DelegatesFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatesFactory.swift; sourceTree = ""; }; 644EC35520EFAAB700F40C73 /* DelegatesListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatesListViewController.swift; sourceTree = ""; }; 644EC35920EFB8E900F40C73 /* AdamantDelegateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantDelegateCell.swift; sourceTree = ""; }; 644EC35D20F34F1E00F40C73 /* DelegateDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegateDetailsViewController.swift; sourceTree = ""; }; @@ -693,9 +767,6 @@ 645AE06521E67D3300AD3623 /* UITextField+adamant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+adamant.swift"; sourceTree = ""; }; 645FEB32213E72C100D6BA2D /* OnboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardViewController.swift; sourceTree = ""; }; 645FEB33213E72C100D6BA2D /* OnboardViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OnboardViewController.xib; sourceTree = ""; }; - 6489794A24CE00C000C33A68 /* SwiftyOnboardPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyOnboardPage.swift; sourceTree = ""; }; - 6489794B24CE00C000C33A68 /* SwiftyOnboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyOnboard.swift; sourceTree = ""; }; - 6489794C24CE00C000C33A68 /* SwiftyOnboardOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyOnboardOverlay.swift; sourceTree = ""; }; 648BCA6C213D384F00875EB5 /* AvatarService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarService.swift; sourceTree = ""; }; 648C696E22915A12006645F5 /* DashTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashTransaction.swift; sourceTree = ""; }; 648C697022915CB8006645F5 /* BTCRPCServerResponce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTCRPCServerResponce.swift; sourceTree = ""; }; @@ -721,14 +792,13 @@ 649D6BF121C27D5C009E727B /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = ""; }; 649E9A142111B3C200686B01 /* Mnemonic+extended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mnemonic+extended.swift"; sourceTree = ""; }; 64A223D520F760BB005157CB /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; - 64A223D720F7A08E005157CB /* LskApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskApiService.swift; sourceTree = ""; }; 64B5736C2201E196005DC968 /* BtcTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcTransactionsViewController.swift; sourceTree = ""; }; 64B5736E2209B892005DC968 /* BtcTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcTransactionDetailsViewController.swift; sourceTree = ""; }; 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransaction.swift; sourceTree = ""; }; 64BD2B7620E2820300E2CD36 /* TransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetails.swift; sourceTree = ""; }; 64C65F4423893C7600DC0425 /* OnboardOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardOverlay.swift; sourceTree = ""; }; 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; - 64E1C82C222E95E2006C4DA7 /* DogeWalletRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletRoutes.swift; sourceTree = ""; }; + 64E1C82C222E95E2006C4DA7 /* DogeWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletFactory.swift; sourceTree = ""; }; 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWallet.swift; sourceTree = ""; }; 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletService.swift; sourceTree = ""; }; 64E1C832222EA0F0006C4DA7 /* DogeWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletViewController.swift; sourceTree = ""; }; @@ -748,11 +818,29 @@ 9322E874297042F000B8357C /* ChatSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSender.swift; sourceTree = ""; }; 9322E876297042FA00B8357C /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = ""; }; 9322E87A2970431200B8357C /* ChatMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageFactory.swift; sourceTree = ""; }; - 9324C75D297170600022D7EA /* RichTransactionStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTransactionStatusService.swift; sourceTree = ""; }; - 9324C75F297171040022D7EA /* AdamantRichTransactionStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantRichTransactionStatusService.swift; sourceTree = ""; }; + 9324C75D297170600022D7EA /* TransactionStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionStatusService.swift; sourceTree = ""; }; + 9324C75F297171040022D7EA /* AdamantTransactionStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantTransactionStatusService.swift; sourceTree = ""; }; + 93294B7C2AAD067000911109 /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContainer.swift; sourceTree = ""; }; + 93294B812AAD0BB400911109 /* BtcWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcWalletFactory.swift; sourceTree = ""; }; + 93294B832AAD0C8F00911109 /* Assembler+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Assembler+Extension.swift"; sourceTree = ""; }; + 93294B852AAD0E0A00911109 /* AdmWallet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdmWallet.swift; sourceTree = ""; }; + 93294B862AAD0E0A00911109 /* AdmWalletService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdmWalletService.swift; sourceTree = ""; }; + 93294B8B2AAD2C6B00911109 /* SwiftyOnboardPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyOnboardPage.swift; sourceTree = ""; }; + 93294B8C2AAD2C6B00911109 /* SwiftyOnboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyOnboard.swift; sourceTree = ""; }; + 93294B8D2AAD2C6B00911109 /* SwiftyOnboardOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyOnboardOverlay.swift; sourceTree = ""; }; + 93294B952AAD320B00911109 /* ScreensFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreensFactory.swift; sourceTree = ""; }; + 93294B972AAD364F00911109 /* AdamantScreensFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantScreensFactory.swift; sourceTree = ""; }; + 93294B992AAD624100911109 /* WalletFactoryCompose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletFactoryCompose.swift; sourceTree = ""; }; 932B34E82974AA4A002A75BA /* ChatPreservationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreservationDelegate.swift; sourceTree = ""; }; 932BD15A29D2F75200AA1947 /* RichMessageProviderWithStatusCheck+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RichMessageProviderWithStatusCheck+Extension.swift"; sourceTree = ""; }; 932F77582989F999006D8801 /* ChatCellManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCellManager.swift; sourceTree = ""; }; + 9338AE7E2AEF43DA001D32DF /* NodesStorageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesStorageProtocol.swift; sourceTree = ""; }; + 9338AE802AEF4B8E001D32DF /* NodesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesStorage.swift; sourceTree = ""; }; + 9338AE832AEF5EFA001D32DF /* APICoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICoreProtocol.swift; sourceTree = ""; }; + 9338AE852AEF6A97001D32DF /* APICore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICore.swift; sourceTree = ""; }; + 9338AE8A2AEF7E37001D32DF /* APIParametersEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIParametersEncoding.swift; sourceTree = ""; }; + 9338AE8C2AEF7E9C001D32DF /* BodyStringEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyStringEncoding.swift; sourceTree = ""; }; + 9338AE8E2AEF8131001D32DF /* InternalAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalAPIError.swift; sourceTree = ""; }; 9340077F29AC341000A20622 /* ChatAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAction.swift; sourceTree = ""; }; 9345769428FD0C34004E6C7A /* UIViewController+email.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+email.swift"; sourceTree = ""; }; 93496B822A6C85F400DD062F /* AdamantResources+CoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantResources+CoreData.swift"; sourceTree = ""; }; @@ -769,16 +857,30 @@ 93496BAB2A6CAED100DD062F /* Roboto_400_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Roboto_400_normal.ttf; sourceTree = ""; }; 93496BAC2A6CAED100DD062F /* Roboto_500_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Roboto_500_normal.ttf; sourceTree = ""; }; 93547BC929E2262D00B0914B /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; - 935F53D529BE8F7400779492 /* RichTransactionStatusPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTransactionStatusPublisher.swift; sourceTree = ""; }; + 935F53D529BE8F7400779492 /* TransactionStatusPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionStatusPublisher.swift; sourceTree = ""; }; + 9366588C2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListState.swift; sourceTree = ""; }; + 9366588E2B0AB97500BDB2D3 /* CoinsNodesListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListMapper.swift; sourceTree = ""; }; + 936658902B0AB9DC00BDB2D3 /* NodeWithGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeWithGroup.swift; sourceTree = ""; }; + 936658922B0AC03700BDB2D3 /* CoinsNodesListStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListStrings.swift; sourceTree = ""; }; + 936658942B0AC15300BDB2D3 /* Node+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Node+UI.swift"; sourceTree = ""; }; + 936658962B0ACB1500BDB2D3 /* CoinsNodesListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListViewModel.swift; sourceTree = ""; }; + 936658982B0AD32600BDB2D3 /* CoinsNodesListViewModel+ApiServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoinsNodesListViewModel+ApiServices.swift"; sourceTree = ""; }; + 9366589A2B0AD3E600BDB2D3 /* WalletApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletApiService.swift; sourceTree = ""; }; + 9366589C2B0ADBAF00BDB2D3 /* CoinsNodesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListView.swift; sourceTree = ""; }; + 936658A22B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoinsNodesListView+Row.swift"; sourceTree = ""; }; + 936658A42B0AE67A00BDB2D3 /* CoinsNodesListFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListFactory.swift; sourceTree = ""; }; 93684A2929EFA28A00F9EFFE /* FixedTextMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixedTextMessageSizeCalculator.swift; sourceTree = ""; }; 9371130E2996EDA900F64CF9 /* ChatRefreshMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRefreshMock.swift; sourceTree = ""; }; 9371E560295CD53100438F2C /* ChatLocalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLocalization.swift; sourceTree = ""; }; + 937736812B0949C500B35C7A /* NodeCell+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NodeCell+Model.swift"; sourceTree = ""; }; 937751AA2A68BB390054BD65 /* ChatTransactionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTransactionCell.swift; sourceTree = ""; }; 937751AC2A68BCE10054BD65 /* MessageCellWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellWrapper.swift; sourceTree = ""; }; 93775E452A674FA9009061AC /* Markdown+Adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Markdown+Adamant.swift"; sourceTree = ""; }; 9377FBDE296C2A2F00C9211B /* ChatTransactionContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTransactionContentView.swift; sourceTree = ""; }; 9377FBE1296C2ACA00C9211B /* ChatTransactionContentView+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatTransactionContentView+Model.swift"; sourceTree = ""; }; 9382F61229DEC0A3005E6216 /* ChatModelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModelView.swift; sourceTree = ""; }; + 938A46A32AE6103E00FC03DB /* HealthCheckWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthCheckWrapper.swift; sourceTree = ""; }; + 938A46A52AE6106300FC03DB /* BlockchainHealthCheckWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockchainHealthCheckWrapper.swift; sourceTree = ""; }; 938F7D5A2955C8DA001915CA /* ChatDisplayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDisplayManager.swift; sourceTree = ""; }; 938F7D5C2955C8F9001915CA /* ChatLayoutManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLayoutManager.swift; sourceTree = ""; }; 938F7D5E2955C90D001915CA /* ChatInputBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputBarManager.swift; sourceTree = ""; }; @@ -791,14 +893,41 @@ 9390C5042976B53000270CDF /* ChatDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDialog.swift; sourceTree = ""; }; 93996A962968209C008D080B /* ChatMessagesCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesCollection.swift; sourceTree = ""; }; 9399F5EC29A85A48006C3E30 /* ChatCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCacheService.swift; sourceTree = ""; }; + 939FA3412B0D6F0000710EC6 /* SelfRemovableHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfRemovableHostingController.swift; sourceTree = ""; }; 93A118502993167500E144CC /* ChatMessageBackgroundColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageBackgroundColor.swift; sourceTree = ""; }; 93A118522993241D00E144CC /* ChatMessagesListFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesListFactory.swift; sourceTree = ""; }; + 93A18C852AAEACC100D0AB98 /* AdamantWalletFactoryCompose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantWalletFactoryCompose.swift; sourceTree = ""; }; + 93A18C882AAEAE7700D0AB98 /* WalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletFactory.swift; sourceTree = ""; }; 93A91FD0297972B7001DB1F8 /* ChatScrollDownButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatScrollDownButton.swift; sourceTree = ""; }; 93A91FD229799298001DB1F8 /* ChatStartPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStartPosition.swift; sourceTree = ""; }; + 93ADC17A2B08283500F2DF77 /* ForceQueryItemsEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceQueryItemsEncoding.swift; sourceTree = ""; }; + 93ADC17C2B083C3B00F2DF77 /* NodesAdditionalParamsStorageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesAdditionalParamsStorageProtocol.swift; sourceTree = ""; }; + 93ADC17E2B083D7A00F2DF77 /* NodesAdditionalParamsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesAdditionalParamsStorage.swift; sourceTree = ""; }; + 93ADE06E2ACA66AF008ED641 /* VibrationSelectionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VibrationSelectionViewModel.swift; sourceTree = ""; }; + 93ADE06F2ACA66AF008ED641 /* VibrationSelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VibrationSelectionView.swift; sourceTree = ""; }; + 93ADE0702ACA66AF008ED641 /* VibrationSelectionFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VibrationSelectionFactory.swift; sourceTree = ""; }; + 93B28EBF2B076667007F268B /* APIResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIResponseModel.swift; sourceTree = ""; }; + 93B28EC12B076D31007F268B /* DashApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashApiService.swift; sourceTree = ""; }; + 93B28EC42B076E2C007F268B /* DashBlockchainInfoDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashBlockchainInfoDTO.swift; sourceTree = ""; }; + 93B28EC72B076E68007F268B /* DashResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashResponseDTO.swift; sourceTree = ""; }; + 93B28EC92B076E88007F268B /* DashErrorDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashErrorDTO.swift; sourceTree = ""; }; 93BF4A6529E4859900505CD0 /* DelegatesBottomPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatesBottomPanel.swift; sourceTree = ""; }; 93BF4A6B29E4B4BF00505CD0 /* DelegatesBottomPanel+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DelegatesBottomPanel+Model.swift"; sourceTree = ""; }; + 93C794432B07725C00408826 /* DashGetAddressBalanceDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashGetAddressBalanceDTO.swift; sourceTree = ""; }; + 93C794452B07768F00408826 /* DashGetRawTransactionDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashGetRawTransactionDTO.swift; sourceTree = ""; }; + 93C794472B0778C700408826 /* DashGetBlockDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashGetBlockDTO.swift; sourceTree = ""; }; + 93C794492B077A1C00408826 /* DashGetUnspentTransactionsDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashGetUnspentTransactionsDTO.swift; sourceTree = ""; }; + 93C7944B2B077B2700408826 /* DashGetAddressTransactionIds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashGetAddressTransactionIds.swift; sourceTree = ""; }; + 93C7944D2B077C1F00408826 /* DashSendRawTransactionDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSendRawTransactionDTO.swift; sourceTree = ""; }; 93CC8DC6296F00D6003772BF /* ChatTransactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTransactionContainerView.swift; sourceTree = ""; }; 93CC8DC8296F01DE003772BF /* ChatTransactionContainerView+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatTransactionContainerView+Model.swift"; sourceTree = ""; }; + 93CC94C02B17EE73004842AC /* EthApiCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiCore.swift; sourceTree = ""; }; + 93CCAE742B06CC3600EA5B94 /* LskNodeApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskNodeApiService.swift; sourceTree = ""; }; + 93CCAE762B06D6CC00EA5B94 /* LskServiceApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskServiceApiService.swift; sourceTree = ""; }; + 93CCAE782B06D81D00EA5B94 /* DogeApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeApiService.swift; sourceTree = ""; }; + 93CCAE7A2B06D9B500EA5B94 /* DogeBlocksDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeBlocksDTO.swift; sourceTree = ""; }; + 93CCAE7D2B06DA6C00EA5B94 /* DogeBlockDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeBlockDTO.swift; sourceTree = ""; }; + 93CCAE7F2B06E2D100EA5B94 /* ApiServiceError+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApiServiceError+Extension.swift"; sourceTree = ""; }; 93E123302A6DF8EF004DF33B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 93E123322A6DF8F1004DF33B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; 93E123332A6DF8F2004DF33B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -811,9 +940,15 @@ 93E1234A2A6DFEF7004DF33B /* NotificationStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStrings.swift; sourceTree = ""; }; 93E5D4DA293000BE00439298 /* UnregisteredTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnregisteredTransaction.swift; sourceTree = ""; }; 93E5D4DF2930029300439298 /* AdamantCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantCore+Extensions.swift"; sourceTree = ""; }; - 93EE9C3229C2666200D9853F /* RichTransactionStatusSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTransactionStatusSubscription.swift; sourceTree = ""; }; + 93E8EDCC2AF1BD65003E163C /* AdamantApiCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantApiCore.swift; sourceTree = ""; }; + 93E8EDCE2AF1CD9F003E163C /* NodeStatusInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeStatusInfo.swift; sourceTree = ""; }; + 93E8EDD02AF1DF8E003E163C /* ServerResponse+Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerResponse+Resolver.swift"; sourceTree = ""; }; + 93EE9C3229C2666200D9853F /* TransactionStatusSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionStatusSubscription.swift; sourceTree = ""; }; 93F3914F2962F5D400BFD6AE /* SpinnerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerView.swift; sourceTree = ""; }; - A50A41032822F8CE006BDFE1 /* BtcWalletRoutes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcWalletRoutes.swift; sourceTree = ""; }; + 93FC169A2B0197FD0062B507 /* BtcApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcApiService.swift; sourceTree = ""; }; + 93FC169C2B019F440062B507 /* EthApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiService.swift; sourceTree = ""; }; + 93FC169E2B01A3630062B507 /* LskApiCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskApiCore.swift; sourceTree = ""; }; + 93FC16A02B01DE120062B507 /* ERC20ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ERC20ApiService.swift; sourceTree = ""; }; A50A41042822F8CE006BDFE1 /* BtcWalletService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcWalletService.swift; sourceTree = ""; }; A50A41052822F8CE006BDFE1 /* BtcWalletViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcWalletViewController.swift; sourceTree = ""; }; A50A41062822F8CE006BDFE1 /* BtcWallet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcWallet.swift; sourceTree = ""; }; @@ -877,7 +1012,7 @@ E91947AF20002393001362F8 /* AdamantApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantApiService.swift; sourceTree = ""; }; E91947B12000246A001362F8 /* AdamantError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantError.swift; sourceTree = ""; }; E91947B320002809001362F8 /* AdamantAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantAccount.swift; sourceTree = ""; }; - E91E5BF120DAF05500B06B3C /* EurekaNodeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EurekaNodeRow.swift; sourceTree = ""; }; + E91E5BF120DAF05500B06B3C /* NodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeCell.swift; sourceTree = ""; }; E9204B5120C9762300F3B9AB /* MessageStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageStatus.swift; sourceTree = ""; }; E921534C20EE1E8700C0843F /* EurekaAlertLabelRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EurekaAlertLabelRow.swift; sourceTree = ""; }; E921534D20EE1E8700C0843F /* AlertLabelCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlertLabelCell.xib; sourceTree = ""; }; @@ -895,24 +1030,22 @@ E926E02D213EAABF005E536B /* TransferViewControllerBase+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TransferViewControllerBase+Alert.swift"; sourceTree = ""; }; E926E031213EC43B005E536B /* FullscreenAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenAlertView.swift; sourceTree = ""; }; E926E033213EC454005E536B /* FullscreenAlertView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FullscreenAlertView.xib; sourceTree = ""; }; - E9332B8821F1FA4400D56E72 /* OnboardRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardRoutes.swift; sourceTree = ""; }; + E9332B8821F1FA4400D56E72 /* OnboardFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardFactory.swift; sourceTree = ""; }; E933475A225539390083839E /* DogeGetTransactionsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeGetTransactionsResponse.swift; sourceTree = ""; }; E9393FA92055D03300EE6F30 /* AdamantMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantMessage.swift; sourceTree = ""; }; E93B0D732028B21400126346 /* ChatsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsProvider.swift; sourceTree = ""; }; E93B0D752028B28E00126346 /* AdamantChatsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantChatsProvider.swift; sourceTree = ""; }; E93D7ABD2052CEE1005D19DC /* NotificationsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsService.swift; sourceTree = ""; }; E93D7ABF2052CF63005D19DC /* AdamantNotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantNotificationService.swift; sourceTree = ""; }; - E93EB09E20DA3FA4001F9601 /* NodesEditorRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesEditorRoutes.swift; sourceTree = ""; }; + E93EB09E20DA3FA4001F9601 /* NodesEditorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesEditorFactory.swift; sourceTree = ""; }; E940086A2114A70600CD2D67 /* LskAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskAccount.swift; sourceTree = ""; }; E940086D2114AA2E00CD2D67 /* WalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletService.swift; sourceTree = ""; }; E94008712114EACF00CD2D67 /* WalletAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletAccount.swift; sourceTree = ""; }; E940087A2114ED0600CD2D67 /* EthWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthWalletService.swift; sourceTree = ""; }; E940087C2114EDEE00CD2D67 /* EthWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthWallet.swift; sourceTree = ""; }; - E940087F2114EE2000CD2D67 /* AdmWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AdmWallet.swift; path = Adamant/Wallets/Adamant/AdmWallet.swift; sourceTree = SOURCE_ROOT; }; E94008822114EE4700CD2D67 /* LskWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskWallet.swift; sourceTree = ""; }; E94008842114EE7500CD2D67 /* LskWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskWalletService.swift; sourceTree = ""; }; E94008862114F05B00CD2D67 /* AddressValidationResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressValidationResult.swift; sourceTree = ""; }; - E94008882114F0F700CD2D67 /* AdmWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AdmWalletService.swift; path = Adamant/Wallets/Adamant/AdmWalletService.swift; sourceTree = SOURCE_ROOT; }; E940088A2114F63000CD2D67 /* NSRegularExpression+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+adamant.swift"; sourceTree = ""; }; E940088E2119A9E800CD2D67 /* BigInt+Decimal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BigInt+Decimal.swift"; sourceTree = ""; }; E941CCDA20E786D700C96220 /* AccountHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountHeader.xib; sourceTree = ""; }; @@ -923,10 +1056,10 @@ E9484B7C2285BAD8008E10F0 /* PrivateKeyGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateKeyGenerator.swift; sourceTree = ""; }; E9484B7E2285C016008E10F0 /* PKGeneratorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKGeneratorViewController.swift; sourceTree = ""; }; E94883E6203F07CD00F6E1B0 /* PassphraseValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassphraseValidation.swift; sourceTree = ""; }; - E948E03A20235E2300975D6B /* SettingsRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRoutes.swift; sourceTree = ""; }; + E948E03A20235E2300975D6B /* SettingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFactory.swift; sourceTree = ""; }; E948E0472024F02700975D6B /* VersionFooter.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VersionFooter.xib; sourceTree = ""; }; E94E7B00205D3F090042B639 /* ChatListViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ChatListViewController.xib; sourceTree = ""; }; - E94E7B07205D4CB80042B639 /* SharedRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedRoutes.swift; sourceTree = ""; }; + E94E7B07205D4CB80042B639 /* ShareQRFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareQRFactory.swift; sourceTree = ""; }; E94E7B0B205D5E4A0042B639 /* TransactionsListViewControllerBase.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransactionsListViewControllerBase.xib; sourceTree = ""; }; E950273F202E257E002C1098 /* RepeaterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeaterService.swift; sourceTree = ""; }; E950652020404BF0008352E5 /* AdamantUriBuilding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantUriBuilding.swift; sourceTree = ""; }; @@ -948,7 +1081,7 @@ E95F85702007D98D0070534A /* CurrencyFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyFormatterTests.swift; sourceTree = ""; }; E95F85742007E4790070534A /* HexAndBytesUtilitiesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HexAndBytesUtilitiesTest.swift; sourceTree = ""; }; E95F85762007E8EC0070534A /* JSAdamantCoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAdamantCoreTests.swift; sourceTree = ""; }; - E95F857E2008C8D60070534A /* ChatsRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsRoutes.swift; sourceTree = ""; }; + E95F857E2008C8D60070534A /* ChatListFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListFactory.swift; sourceTree = ""; }; E95F85842008CB3A0070534A /* ChatListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListViewController.swift; sourceTree = ""; }; E95F85B6200A4D8F0070534A /* TestTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTools.swift; sourceTree = ""; }; E95F85B9200A4DC90070534A /* TransactionSend.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TransactionSend.json; sourceTree = ""; }; @@ -983,8 +1116,8 @@ E987024820C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantChatsProvider+fakeMessages.swift"; sourceTree = ""; }; E98FC34320F920BD00032D65 /* UIFont+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+adamant.swift"; sourceTree = ""; }; E993301D212EF39700CD5200 /* EthTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransferViewController.swift; sourceTree = ""; }; - E993301F21354B1800CD5200 /* AdmWalletRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmWalletRoutes.swift; sourceTree = ""; }; - E993302121354BC300CD5200 /* EthWalletRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthWalletRoutes.swift; sourceTree = ""; }; + E993301F21354B1800CD5200 /* AdmWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmWalletFactory.swift; sourceTree = ""; }; + E993302121354BC300CD5200 /* EthWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthWalletFactory.swift; sourceTree = ""; }; E99330252136B0E500CD5200 /* TransferViewControllerBase+QR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TransferViewControllerBase+QR.swift"; sourceTree = ""; }; E9942B7F203C058C00C163AF /* QRGeneratorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRGeneratorViewController.swift; sourceTree = ""; }; E9942B83203CBFCE00C163AF /* AdamantQRTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantQRTools.swift; sourceTree = ""; }; @@ -999,19 +1132,14 @@ E9981897212096ED0018C84C /* WalletViewControllerBase.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WalletViewControllerBase.xib; sourceTree = ""; }; E9A03FD120DBC0F2007653A1 /* NodeEditorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeEditorViewController.swift; sourceTree = ""; }; E9A03FD320DBC824007653A1 /* NodeVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeVersion.swift; sourceTree = ""; }; - E9A03FD520DBC8E2007653A1 /* AdamantApi+Peers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Peers.swift"; sourceTree = ""; }; - E9A03FD720DC0ABA007653A1 /* AdamantNodesSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantNodesSource.swift; sourceTree = ""; }; - E9A03FD920DC0B14007653A1 /* NodesSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesSource.swift; sourceTree = ""; }; E9A174B22057EC47003667CD /* BackgroundFetchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundFetchService.swift; sourceTree = ""; }; E9A174B42057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantTransfersProvider+backgroundFetch.swift"; sourceTree = ""; }; E9A174B62057F1B3003667CD /* AdamantChatsProvider+backgroundFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantChatsProvider+backgroundFetch.swift"; sourceTree = ""; }; E9A174B820587B83003667CD /* notification.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = notification.mp3; sourceTree = ""; }; E9AA8BF72129F13000F9249F /* ComplexTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplexTransferViewController.swift; sourceTree = ""; }; E9AA8BF9212C166600F9249F /* EthWalletService+Send.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EthWalletService+Send.swift"; sourceTree = ""; }; - E9AA8BFB212C169200F9249F /* EthWalletService+Transfers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EthWalletService+Transfers.swift"; sourceTree = ""; }; E9AA8C01212C5BF500F9249F /* AdmWalletService+Send.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdmWalletService+Send.swift"; sourceTree = ""; }; E9B1AA562121ACBF00080A2A /* AdmWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmWalletViewController.swift; sourceTree = ""; }; - E9B1AA582122D59600080A2A /* WalletsRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletsRoutes.swift; sourceTree = ""; }; E9B1AA5A21283E0F00080A2A /* AdmTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmTransferViewController.swift; sourceTree = ""; }; E9B3D399201F90570019EB36 /* AccountsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsProvider.swift; sourceTree = ""; }; E9B3D39D201F99F40019EB36 /* DataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProvider.swift; sourceTree = ""; }; @@ -1036,12 +1164,10 @@ E9E7CD8A20026B0600DFC4DB /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = ""; }; E9E7CD8C20026B6600DFC4DB /* DialogService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogService.swift; sourceTree = ""; }; E9E7CD8E20026CD300DFC4DB /* AdamantDialogService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdamantDialogService.swift; sourceTree = ""; }; - E9E7CD9020026FA100DFC4DB /* SwinjectDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwinjectDependencies.swift; sourceTree = ""; }; + E9E7CD9020026FA100DFC4DB /* AppAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppAssembly.swift; sourceTree = ""; }; E9E7CD922002740500DFC4DB /* AdamantAccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantAccountService.swift; sourceTree = ""; }; - E9E7CDAE2002B8A100DFC4DB /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; - E9E7CDB02002B97B00DFC4DB /* AccountRoutes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRoutes.swift; sourceTree = ""; }; - E9E7CDB22002B9FB00DFC4DB /* LoginRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginRoutes.swift; sourceTree = ""; }; - E9E7CDB42002BA6900DFC4DB /* SwinjectedRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwinjectedRouter.swift; sourceTree = ""; }; + E9E7CDB02002B97B00DFC4DB /* AccountFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountFactory.swift; sourceTree = ""; }; + E9E7CDB22002B9FB00DFC4DB /* LoginFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFactory.swift; sourceTree = ""; }; E9E7CDB62003994E00DFC4DB /* AdamantUtilities+extended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantUtilities+extended.swift"; sourceTree = ""; }; E9E7CDBD2003AEFB00DFC4DB /* CellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellFactory.swift; sourceTree = ""; }; E9E7CDBF2003AF6D00DFC4DB /* AdamantCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdamantCellFactory.swift; sourceTree = ""; }; @@ -1157,6 +1283,15 @@ path = Pods; sourceTree = ""; }; + 3A20D9392AE7F305005475A6 /* Models */ = { + isa = PBXGroup; + children = ( + 64BD2B7620E2820300E2CD36 /* TransactionDetails.swift */, + 3A20D93A2AE7F316005475A6 /* AdamantTransactionDetails.swift */, + ); + path = Models; + sourceTree = ""; + }; 3A41938D2A580C3B006A6B22 /* RichTransactionReactService */ = { isa = PBXGroup; children = ( @@ -1165,6 +1300,14 @@ path = RichTransactionReactService; sourceTree = ""; }; + 3A770E4A2AE14EFD0008D98F /* Mappers */ = { + isa = PBXGroup; + children = ( + 3A770E4B2AE14F130008D98F /* SimpleTransactionDetails+Hashable.swift */, + ); + path = Mappers; + sourceTree = ""; + }; 3AA2D5F8280EAF49000ED971 /* SocketService */ = { isa = PBXGroup; children = ( @@ -1173,6 +1316,16 @@ path = SocketService; sourceTree = ""; }; + 3AA50DED2AEBE61C00C58FC8 /* PartnerQR */ = { + isa = PBXGroup; + children = ( + 3AA50DF22AEBE67C00C58FC8 /* PartnerQRFactory.swift */, + 3AA50DEE2AEBE65D00C58FC8 /* PartnerQRView.swift */, + 3AA50DF02AEBE66A00C58FC8 /* PartnerQRViewModel.swift */, + ); + path = PartnerQR; + sourceTree = ""; + }; 411742FE2A39B1B1008CD98A /* Contribute */ = { isa = PBXGroup; children = ( @@ -1241,10 +1394,12 @@ 6403F5DC22723C2800D58779 /* Dash */ = { isa = PBXGroup; children = ( + 93B28EC32B076DFD007F268B /* DTO */, 6403F5DD22723C6800D58779 /* DashMainnet.swift */, - 6403F5DF22723F6400D58779 /* DashWalletRouter.swift */, + 6403F5DF22723F6400D58779 /* DashWalletFactory.swift */, 6403F5E122723F7500D58779 /* DashWallet.swift */, 6403F5E322723F8C00D58779 /* DashWalletService.swift */, + 93B28EC12B076D31007F268B /* DashApiService.swift */, 4186B339294200F4006594A3 /* DashWalletService+DynamicConstants.swift */, A578BDE42623051C00090141 /* DashWalletService+Transactions.swift */, 648CE3A5229AD1CD0070A2CC /* DashWalletService+Send.swift */, @@ -1261,9 +1416,10 @@ 6449BA5D235CA0930033B936 /* ERC20 */ = { isa = PBXGroup; children = ( - 6449BA66235CA0930033B936 /* ERC20WalletRouter.swift */, + 6449BA66235CA0930033B936 /* ERC20WalletFactory.swift */, 6449BA60235CA0930033B936 /* ERC20Wallet.swift */, 6449BA5E235CA0930033B936 /* ERC20WalletService.swift */, + 93FC16A02B01DE120062B507 /* ERC20ApiService.swift */, 6449BA65235CA0930033B936 /* ERC20WalletService+Send.swift */, 6449BA67235CA0930033B936 /* ERC20WalletService+RichMessageProvider.swift */, 6449BA64235CA0930033B936 /* ERC20WalletService+RichMessageProviderWithStatusCheck.swift */, @@ -1279,7 +1435,7 @@ isa = PBXGroup; children = ( 93BF4A6A29E4B4B600505CD0 /* DelegatesBottomPanel */, - 644EC35120EFA9A300F40C73 /* DelegateRoutes.swift */, + 644EC35120EFA9A300F40C73 /* DelegatesFactory.swift */, 644EC35520EFAAB700F40C73 /* DelegatesListViewController.swift */, 644EC35920EFB8E900F40C73 /* AdamantDelegateCell.swift */, 644EC35D20F34F1E00F40C73 /* DelegateDetailsViewController.swift */, @@ -1288,24 +1444,15 @@ path = Delegates; sourceTree = ""; }; - 6489794924CE00C000C33A68 /* SwiftyOnboard */ = { - isa = PBXGroup; - children = ( - 6489794A24CE00C000C33A68 /* SwiftyOnboardPage.swift */, - 6489794B24CE00C000C33A68 /* SwiftyOnboard.swift */, - 6489794C24CE00C000C33A68 /* SwiftyOnboardOverlay.swift */, - ); - name = SwiftyOnboard; - path = Adamant/Stories/SwiftyOnboard; - sourceTree = SOURCE_ROOT; - }; 64E1C82B222E958C006C4DA7 /* Doge */ = { isa = PBXGroup; children = ( + 93CCAE7C2B06D9B900EA5B94 /* DTO */, E907350D2256779C00BF02CC /* DogeMainnet.swift */, - 64E1C82C222E95E2006C4DA7 /* DogeWalletRoutes.swift */, + 64E1C82C222E95E2006C4DA7 /* DogeWalletFactory.swift */, 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */, 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */, + 93CCAE782B06D81D00EA5B94 /* DogeApiService.swift */, 4186B337294200E8006594A3 /* DogeWalletService+DynamicConstants.swift */, 648DD7A32237DB9E00B811FD /* DogeWalletService+Send.swift */, 648DD7A72239147800B811FD /* DogeWalletService+RichMessageProvider.swift */, @@ -1331,6 +1478,62 @@ path = Models; sourceTree = ""; }; + 93294B7A2AAD054C00911109 /* App */ = { + isa = PBXGroup; + children = ( + 93294B7B2AAD060600911109 /* DI */, + E913C8F11FFFA51D001A83F7 /* AppDelegate.swift */, + ); + path = App; + sourceTree = ""; + }; + 93294B7B2AAD060600911109 /* DI */ = { + isa = PBXGroup; + children = ( + E9E7CD9020026FA100DFC4DB /* AppAssembly.swift */, + 93294B7C2AAD067000911109 /* AppContainer.swift */, + ); + path = DI; + sourceTree = ""; + }; + 93294B892AAD2BD900911109 /* ShareQR */ = { + isa = PBXGroup; + children = ( + E94E7B07205D4CB80042B639 /* ShareQRFactory.swift */, + E9FAE5E0203ED1AE008D3A6B /* ShareQrViewController.swift */, + E9FAE5E1203ED1AE008D3A6B /* ShareQrViewController.xib */, + ); + path = ShareQR; + sourceTree = ""; + }; + 93294B8A2AAD2C6B00911109 /* SwiftyOnboard */ = { + isa = PBXGroup; + children = ( + 93294B8B2AAD2C6B00911109 /* SwiftyOnboardPage.swift */, + 93294B8C2AAD2C6B00911109 /* SwiftyOnboard.swift */, + 93294B8D2AAD2C6B00911109 /* SwiftyOnboardOverlay.swift */, + ); + path = SwiftyOnboard; + sourceTree = ""; + }; + 93294B912AAD2CA500911109 /* Welcome */ = { + isa = PBXGroup; + children = ( + 6458548A211B3AB1004C5909 /* WelcomeViewController.xib */, + 93547BC929E2262D00B0914B /* WelcomeViewController.swift */, + ); + path = Welcome; + sourceTree = ""; + }; + 93294B942AAD31F200911109 /* ScreensFactory */ = { + isa = PBXGroup; + children = ( + 93294B952AAD320B00911109 /* ScreensFactory.swift */, + 93294B972AAD364F00911109 /* AdamantScreensFactory.swift */, + ); + path = ScreensFactory; + sourceTree = ""; + }; 932BD15929D2F74500AA1947 /* RichMessageProviderWithStatusCheck */ = { isa = PBXGroup; children = ( @@ -1361,13 +1564,53 @@ 935F53D429BE8F4800779492 /* RichTransactionStatusService */ = { isa = PBXGroup; children = ( - 9324C75F297171040022D7EA /* AdamantRichTransactionStatusService.swift */, - 935F53D529BE8F7400779492 /* RichTransactionStatusPublisher.swift */, - 93EE9C3229C2666200D9853F /* RichTransactionStatusSubscription.swift */, + 9324C75F297171040022D7EA /* AdamantTransactionStatusService.swift */, + 935F53D529BE8F7400779492 /* TransactionStatusPublisher.swift */, + 93EE9C3229C2666200D9853F /* TransactionStatusSubscription.swift */, ); path = RichTransactionStatusService; sourceTree = ""; }; + 9366588B2B0AB68300BDB2D3 /* CoinsNodesList */ = { + isa = PBXGroup; + children = ( + 936658A12B0ADE3100BDB2D3 /* View */, + 936658A02B0ADE2300BDB2D3 /* ViewModel */, + 936658A42B0AE67A00BDB2D3 /* CoinsNodesListFactory.swift */, + ); + path = CoinsNodesList; + sourceTree = ""; + }; + 936658A02B0ADE2300BDB2D3 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 936658922B0AC03700BDB2D3 /* CoinsNodesListStrings.swift */, + 9366588C2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift */, + 9366588E2B0AB97500BDB2D3 /* CoinsNodesListMapper.swift */, + 936658962B0ACB1500BDB2D3 /* CoinsNodesListViewModel.swift */, + 936658982B0AD32600BDB2D3 /* CoinsNodesListViewModel+ApiServices.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 936658A12B0ADE3100BDB2D3 /* View */ = { + isa = PBXGroup; + children = ( + 9366589C2B0ADBAF00BDB2D3 /* CoinsNodesListView.swift */, + 936658A22B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift */, + ); + path = View; + sourceTree = ""; + }; + 937736832B0949C700B35C7A /* NodeCell */ = { + isa = PBXGroup; + children = ( + E91E5BF120DAF05500B06B3C /* NodeCell.swift */, + 937736812B0949C500B35C7A /* NodeCell+Model.swift */, + ); + path = NodeCell; + sourceTree = ""; + }; 9377FBE0296C2AB700C9211B /* ChatTransaction */ = { isa = PBXGroup; children = ( @@ -1446,6 +1689,42 @@ path = Subviews; sourceTree = ""; }; + 93A18C872AAEAE5600D0AB98 /* DI */ = { + isa = PBXGroup; + children = ( + 93A18C882AAEAE7700D0AB98 /* WalletFactory.swift */, + 93294B992AAD624100911109 /* WalletFactoryCompose.swift */, + 93A18C852AAEACC100D0AB98 /* AdamantWalletFactoryCompose.swift */, + ); + path = DI; + sourceTree = ""; + }; + 93ADE06D2ACA66AF008ED641 /* TestVibration */ = { + isa = PBXGroup; + children = ( + 93ADE06E2ACA66AF008ED641 /* VibrationSelectionViewModel.swift */, + 93ADE06F2ACA66AF008ED641 /* VibrationSelectionView.swift */, + 93ADE0702ACA66AF008ED641 /* VibrationSelectionFactory.swift */, + ); + path = TestVibration; + sourceTree = ""; + }; + 93B28EC32B076DFD007F268B /* DTO */ = { + isa = PBXGroup; + children = ( + 93B28EC42B076E2C007F268B /* DashBlockchainInfoDTO.swift */, + 93B28EC72B076E68007F268B /* DashResponseDTO.swift */, + 93B28EC92B076E88007F268B /* DashErrorDTO.swift */, + 93C794432B07725C00408826 /* DashGetAddressBalanceDTO.swift */, + 93C794452B07768F00408826 /* DashGetRawTransactionDTO.swift */, + 93C794472B0778C700408826 /* DashGetBlockDTO.swift */, + 93C794492B077A1C00408826 /* DashGetUnspentTransactionsDTO.swift */, + 93C7944B2B077B2700408826 /* DashGetAddressTransactionIds.swift */, + 93C7944D2B077C1F00408826 /* DashSendRawTransactionDTO.swift */, + ); + path = DTO; + sourceTree = ""; + }; 93BF4A6A29E4B4B600505CD0 /* DelegatesBottomPanel */ = { isa = PBXGroup; children = ( @@ -1473,6 +1752,15 @@ path = Container; sourceTree = ""; }; + 93CCAE7C2B06D9B900EA5B94 /* DTO */ = { + isa = PBXGroup; + children = ( + 93CCAE7A2B06D9B500EA5B94 /* DogeBlocksDTO.swift */, + 93CCAE7D2B06DA6C00EA5B94 /* DogeBlockDTO.swift */, + ); + path = DTO; + sourceTree = ""; + }; 93E123342A6DFCA6004DF33B /* NotificationsShared */ = { isa = PBXGroup; children = ( @@ -1503,10 +1791,11 @@ A50A41022822F8CE006BDFE1 /* Bitcoin */ = { isa = PBXGroup; children = ( - A5E04225282A8BC70076CD13 /* Models */, + A5E04225282A8BC70076CD13 /* DTO */, A50A41062822F8CE006BDFE1 /* BtcWallet.swift */, - A50A41032822F8CE006BDFE1 /* BtcWalletRoutes.swift */, + 93294B812AAD0BB400911109 /* BtcWalletFactory.swift */, A50A41042822F8CE006BDFE1 /* BtcWalletService.swift */, + 93FC169A2B0197FD0062B507 /* BtcApiService.swift */, 4186B331294200B4006594A3 /* BtcWalletService+DynamicConstants.swift */, A50A410F2822FC35006BDFE1 /* BtcWalletService+Send.swift */, A50A410E2822FC35006BDFE1 /* BtcWalletService+RichMessageProvider.swift */, @@ -1519,14 +1808,14 @@ path = Bitcoin; sourceTree = ""; }; - A5E04225282A8BC70076CD13 /* Models */ = { + A5E04225282A8BC70076CD13 /* DTO */ = { isa = PBXGroup; children = ( A5E04226282A8BDC0076CD13 /* BtcBalanceResponse.swift */, A5E04228282A998C0076CD13 /* BtcTransactionResponse.swift */, A5E0422A282AB18B0076CD13 /* BtcUnspentTransactionResponse.swift */, ); - path = Models; + path = DTO; sourceTree = ""; }; B92CFC9A479739E2046C81E9 /* Frameworks */ = { @@ -1583,19 +1872,16 @@ E913C8F01FFFA51D001A83F7 /* Adamant */ = { isa = PBXGroup; children = ( - E95F859220094B8E0070534A /* CoreData */, + 93294B7A2AAD054C00911109 /* App */, E913C9101FFFAA4B001A83F7 /* Helpers */, E950651F20404997008352E5 /* Utilities */, E913C9091FFFA95A001A83F7 /* Models */, - E91947B72000326B001362F8 /* ServerResponses */, E913C9041FFFA8FE001A83F7 /* ServiceProtocols */, E913C9061FFFA92E001A83F7 /* Services */, - E940086C2114A8FD00CD2D67 /* Wallets */, E9E7CDB82003AA8E00DFC4DB /* SharedViews */, - E919479920000FFD001362F8 /* Stories */, + E919479920000FFD001362F8 /* Modules */, E913C9111FFFAB05001A83F7 /* Assets */, - E913C8F11FFFA51D001A83F7 /* AppDelegate.swift */, - E9E7CD9020026FA100DFC4DB /* SwinjectDependencies.swift */, + E90847192196FE590095825D /* Adamant.xcdatamodeld */, E913C8FD1FFFA51E001A83F7 /* Info.plist */, 93E123312A6DF8EF004DF33B /* InfoPlist.strings */, E9B994C222BFD73F004CD645 /* Release.entitlements */, @@ -1613,6 +1899,7 @@ E9E7CD8A20026B0600DFC4DB /* AccountService.swift */, 6455E9F021075D3600B2E94C /* AddressBookService.swift */, E91947AB20001A9A001362F8 /* ApiService.swift */, + 9338AE832AEF5EFA001D32DF /* APICoreProtocol.swift */, 3AA2D5F6280EADE3000ED971 /* SocketService.swift */, 4164A9D628F17D4000EEF16D /* ChatTransactionService.swift */, 648BCA6C213D384F00875EB5 /* AvatarService.swift */, @@ -1621,21 +1908,22 @@ 64EAB37322463E020018D9B2 /* CurrencyInfoService.swift */, E9E7CD8C20026B6600DFC4DB /* DialogService.swift */, E90A494C204DA932009F6A65 /* LocalAuthentication.swift */, - 64A223D720F7A08E005157CB /* LskApiService.swift */, - E9A03FD920DC0B14007653A1 /* NodesSource.swift */, E93D7ABD2052CEE1005D19DC /* NotificationsService.swift */, E9215972206119FB0000CA5C /* ReachabilityMonitor.swift */, E9FEECA321413659007DD7C8 /* RichMessageProvider.swift */, - 550066C4284D65DB0044C0B1 /* HealthCheckService.swift */, - E9E7CDAE2002B8A100DFC4DB /* Router.swift */, 9304F8C1292F895C00173F18 /* PushNotificationsTokenService.swift */, - 9324C75D297170600022D7EA /* RichTransactionStatusService.swift */, + 9324C75D297170600022D7EA /* TransactionStatusService.swift */, 41047B73294C61D10039E956 /* VisibleWalletsService.swift */, 4153045A29C09C6C000E4BEA /* IncreaseFeeService.swift */, 4184F1742A33106200D7B8B9 /* CrashlysticsService.swift */, 3A9015A42A614A18002A2464 /* EmojiService.swift */, + 3A7BD00D2AA9BCE80045AAB0 /* VibroService.swift */, 41C1698B29E7F34900FEB3CB /* RichTransactionReplyService.swift */, 3A4193902A580C85006A6B22 /* RichTransactionReactService.swift */, + 9338AE7E2AEF43DA001D32DF /* NodesStorageProtocol.swift */, + 93ADC17C2B083C3B00F2DF77 /* NodesAdditionalParamsStorageProtocol.swift */, + 3A2F55FB2AC6F885000A3F26 /* CoinStorage.swift */, + 3A96E37B2AED27F8001F5A52 /* PartnerQRService.swift */, ); path = ServiceProtocols; sourceTree = ""; @@ -1655,18 +1943,23 @@ E9E7CDBF2003AF6D00DFC4DB /* AdamantCellFactory.swift */, 64EAB37522463F680018D9B2 /* AdamantCurrencyInfoService.swift */, E9E7CD8E20026CD300DFC4DB /* AdamantDialogService.swift */, - E9A03FD720DC0ABA007653A1 /* AdamantNodesSource.swift */, E93D7ABF2052CF63005D19DC /* AdamantNotificationService.swift */, 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */, 4153045829C09902000E4BEA /* AdamantIncreaseFeeService.swift */, 4184F1722A33102800D7B8B9 /* AdamantCrashlysticsService.swift */, 3A9015A62A614A62002A2464 /* AdamantEmojiService.swift */, + 3A7BD00F2AA9BD030045AAB0 /* AdamantVibroService.swift */, E921597420611A6A0000CA5C /* AdamantReachability.swift */, E950273F202E257E002C1098 /* RepeaterService.swift */, - E9E7CDB42002BA6900DFC4DB /* SwinjectedRouter.swift */, E9771D9D22997A6F0099AAC7 /* NativeCore+AdamantCore.swift */, - 550066C6284D682D0044C0B1 /* AdamantHealthCheckService.swift */, 9304F8C3292F8A3100173F18 /* AdamantPushNotificationsTokenService.swift */, + 9338AE852AEF6A97001D32DF /* APICore.swift */, + 938A46A32AE6103E00FC03DB /* HealthCheckWrapper.swift */, + 938A46A52AE6106300FC03DB /* BlockchainHealthCheckWrapper.swift */, + 9338AE802AEF4B8E001D32DF /* NodesStorage.swift */, + 93ADC17E2B083D7A00F2DF77 /* NodesAdditionalParamsStorage.swift */, + 3A2F55FD2AC6F90E000A3F26 /* AdamantCoinStorageService.swift */, + 3A96E3792AED27D7001F5A52 /* AdamantPartnerQRService.swift */, ); path = Services; sourceTree = ""; @@ -1674,6 +1967,8 @@ E913C9091FFFA95A001A83F7 /* Models */ = { isa = PBXGroup; children = ( + E91947B72000326B001362F8 /* ServerResponses */, + E95F859220094B8E0070534A /* CoreData */, E91947B320002809001362F8 /* AdamantAccount.swift */, E9393FA92055D03300EE6F30 /* AdamantMessage.swift */, 644EC34E20EFA77A00F40C73 /* Delegate.swift */, @@ -1695,7 +1990,15 @@ 9304F8C5292F971600173F18 /* ApiServiceResult.swift */, 9304F8C7292F972600173F18 /* ApiServiceError.swift */, 3A4193992A5D554A006A6B22 /* Reaction.swift */, + 3A7BD0112AA9BD5A0045AAB0 /* AdamantVibroType.swift */, 3A33F9F92A7A53DA002B8003 /* EmojiUpdateType.swift */, + 9338AE8A2AEF7E37001D32DF /* APIParametersEncoding.swift */, + 9338AE8C2AEF7E9C001D32DF /* BodyStringEncoding.swift */, + 93ADC17A2B08283500F2DF77 /* ForceQueryItemsEncoding.swift */, + 9338AE8E2AEF8131001D32DF /* InternalAPIError.swift */, + 93E8EDCE2AF1CD9F003E163C /* NodeStatusInfo.swift */, + 93B28EBF2B076667007F268B /* APIResponseModel.swift */, + 936658902B0AB9DC00BDB2D3 /* NodeWithGroup.swift */, ); path = Models; sourceTree = ""; @@ -1727,6 +2030,11 @@ 4133AF232A1CE1A3001A0A1E /* UITableView+Adamant.swift */, 41A1994129D2D3920031AD75 /* SwipePanGestureRecognizer.swift */, 4193AE1529FBEFBF002F21BE /* NSAttributedText+Adamant.swift */, + 93294B832AAD0C8F00911109 /* Assembler+Extension.swift */, + 93E8EDD02AF1DF8E003E163C /* ServerResponse+Resolver.swift */, + 93CCAE7F2B06E2D100EA5B94 /* ApiServiceError+Extension.swift */, + 939FA3412B0D6F0000710EC6 /* SelfRemovableHostingController.swift */, + 936658942B0AC15300BDB2D3 /* Node+UI.swift */, ); path = Helpers; sourceTree = ""; @@ -1748,9 +2056,17 @@ path = Assets; sourceTree = ""; }; - E919479920000FFD001362F8 /* Stories */ = { + E919479920000FFD001362F8 /* Modules */ = { isa = PBXGroup; children = ( + 9366588B2B0AB68300BDB2D3 /* CoinsNodesList */, + 3AA50DED2AEBE61C00C58FC8 /* PartnerQR */, + 93ADE06D2ACA66AF008ED641 /* TestVibration */, + 93294B942AAD31F200911109 /* ScreensFactory */, + 93294B912AAD2CA500911109 /* Welcome */, + 93294B8A2AAD2C6B00911109 /* SwiftyOnboard */, + 93294B892AAD2BD900911109 /* ShareQR */, + E940086C2114A8FD00CD2D67 /* Wallets */, 938F7D552955C05D001915CA /* Chat */, E9E7CDA52002AE1C00DFC4DB /* Account */, E95F857B2008C8B20070534A /* ChatsList */, @@ -1759,15 +2075,14 @@ E93EB09D20DA3F3A001F9601 /* NodesEditor */, E982F69820235AF000566AC7 /* Settings */, E9332B8721F1F9D100D56E72 /* Onboard */, - E9FAE5DB203ECD41008D3A6B /* Shared */, ); - path = Stories; + path = Modules; sourceTree = ""; }; E919479A20001007001362F8 /* Login */ = { isa = PBXGroup; children = ( - E9E7CDB22002B9FB00DFC4DB /* LoginRoutes.swift */, + E9E7CDB22002B9FB00DFC4DB /* LoginFactory.swift */, E905D39E204C281400DDB504 /* LoginViewController.swift */, E9147B602050599000145913 /* LoginViewController+QR.swift */, E9147B6E205088DE00145913 /* LoginViewController+Pinpad.swift */, @@ -1799,9 +2114,8 @@ E9332B8721F1F9D100D56E72 /* Onboard */ = { isa = PBXGroup; children = ( - 6489794924CE00C000C33A68 /* SwiftyOnboard */, 64C65F4423893C7600DC0425 /* OnboardOverlay.swift */, - E9332B8821F1FA4400D56E72 /* OnboardRoutes.swift */, + E9332B8821F1FA4400D56E72 /* OnboardFactory.swift */, 645938922378395E00A2BE7C /* EulaViewController.swift */, 645938932378395E00A2BE7C /* EulaViewController.xib */, 645FEB32213E72C100D6BA2D /* OnboardViewController.swift */, @@ -1816,10 +2130,10 @@ E93EB09D20DA3F3A001F9601 /* NodesEditor */ = { isa = PBXGroup; children = ( + 937736832B0949C700B35C7A /* NodeCell */, 64D059FE20D3116A003AD655 /* NodesListViewController.swift */, - E93EB09E20DA3FA4001F9601 /* NodesEditorRoutes.swift */, + E93EB09E20DA3FA4001F9601 /* NodesEditorFactory.swift */, E9A03FD120DBC0F2007653A1 /* NodeEditorViewController.swift */, - E91E5BF120DAF05500B06B3C /* EurekaNodeRow.swift */, ); path = NodesEditor; sourceTree = ""; @@ -1827,6 +2141,9 @@ E940086C2114A8FD00CD2D67 /* Wallets */ = { isa = PBXGroup; children = ( + 3A20D9392AE7F305005475A6 /* Models */, + 3A770E4A2AE14EFD0008D98F /* Mappers */, + 93A18C872AAEAE5600D0AB98 /* DI */, A50A41022822F8CE006BDFE1 /* Bitcoin */, E94008902119D22400CD2D67 /* Adamant */, E94008792114ECF100CD2D67 /* Ethereum */, @@ -1836,7 +2153,7 @@ 6449BA5D235CA0930033B936 /* ERC20 */, E94008712114EACF00CD2D67 /* WalletAccount.swift */, E940086D2114AA2E00CD2D67 /* WalletService.swift */, - E9B1AA582122D59600080A2A /* WalletsRoutes.swift */, + 9366589A2B0AD3E600BDB2D3 /* WalletApiService.swift */, E99818932120892F0018C84C /* WalletViewControllerBase.swift */, E9981897212096ED0018C84C /* WalletViewControllerBase.xib */, E9EC342020052ABB00C0E546 /* TransferViewControllerBase.swift */, @@ -1844,7 +2161,6 @@ E926E02D213EAABF005E536B /* TransferViewControllerBase+Alert.swift */, E9E7CDC12003F5A400DFC4DB /* TransactionsListViewControllerBase.swift */, E94E7B0B205D5E4A0042B639 /* TransactionsListViewControllerBase.xib */, - 64BD2B7620E2820300E2CD36 /* TransactionDetails.swift */, 64FA53D020E24941006783C9 /* TransactionDetailsViewControllerBase.swift */, E9E7CDC52003F6D200DFC4DB /* TransactionTableViewCell.swift */, E9E7CDC62003F6D200DFC4DB /* TransactionTableViewCell.xib */, @@ -1858,11 +2174,12 @@ isa = PBXGroup; children = ( E940087C2114EDEE00CD2D67 /* EthWallet.swift */, - E993302121354BC300CD5200 /* EthWalletRoutes.swift */, + E993302121354BC300CD5200 /* EthWalletFactory.swift */, E940087A2114ED0600CD2D67 /* EthWalletService.swift */, + 93FC169C2B019F440062B507 /* EthApiService.swift */, + 93CC94C02B17EE73004842AC /* EthApiCore.swift */, 4186B333294200C5006594A3 /* EthWalletService+DynamicConstants.swift */, E9AA8BF9212C166600F9249F /* EthWalletService+Send.swift */, - E9AA8BFB212C169200F9249F /* EthWalletService+Transfers.swift */, E9FEECA52143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift */, E971591B2168209800A5F904 /* EthWalletService+RichMessageProviderWithStatusCheck.swift */, E9981895212095CA0018C84C /* EthWalletViewController.swift */, @@ -1877,11 +2194,13 @@ isa = PBXGroup; children = ( E94008822114EE4700CD2D67 /* LskWallet.swift */, - 6416B19C21AD7B92006089AC /* LskWalletRoutes.swift */, + 6416B19C21AD7B92006089AC /* LskWalletFactory.swift */, E94008842114EE7500CD2D67 /* LskWalletService.swift */, + 93FC169E2B01A3630062B507 /* LskApiCore.swift */, + 93CCAE742B06CC3600EA5B94 /* LskNodeApiService.swift */, + 93CCAE762B06D6CC00EA5B94 /* LskServiceApiService.swift */, 4186B335294200D2006594A3 /* LskWalletService+DynamicConstants.swift */, 6416B1A621B024B6006089AC /* LskWalletService+Send.swift */, - 6416B1A421AEE157006089AC /* LskWalletService+Transfers.swift */, 649D6BE721B95DB7009E727B /* LskWalletService+RichMessageProvider.swift */, 649D6BE921B9627B009E727B /* LskWalletService+RichMessageProviderWithStatusCheck.swift */, 6416B19E21AD7CBE006089AC /* LskWalletViewController.swift */, @@ -1895,9 +2214,9 @@ E94008902119D22400CD2D67 /* Adamant */ = { isa = PBXGroup; children = ( - E940087F2114EE2000CD2D67 /* AdmWallet.swift */, - E993301F21354B1800CD5200 /* AdmWalletRoutes.swift */, - E94008882114F0F700CD2D67 /* AdmWalletService.swift */, + 93294B852AAD0E0A00911109 /* AdmWallet.swift */, + 93294B862AAD0E0A00911109 /* AdmWalletService.swift */, + E993301F21354B1800CD5200 /* AdmWalletFactory.swift */, 4186B32F2941E642006594A3 /* AdmWalletService+DynamicConstants.swift */, E9AA8C01212C5BF500F9249F /* AdmWalletService+Send.swift */, E9240BF4215D686500187B09 /* AdmWalletService+RichMessageProvider.swift */, @@ -1952,7 +2271,7 @@ E95F857B2008C8B20070534A /* ChatsList */ = { isa = PBXGroup; children = ( - E95F857E2008C8D60070534A /* ChatsRoutes.swift */, + E95F857E2008C8D60070534A /* ChatListFactory.swift */, E95F85842008CB3A0070534A /* ChatListViewController.swift */, E94E7B00205D3F090042B639 /* ChatListViewController.xib */, E9C51EF02013F18000385EB7 /* NewChatViewController.swift */, @@ -1988,7 +2307,9 @@ E908471F2196FEA80095825D /* CoreDataAccount+CoreDataProperties.swift */, E90847282196FEA80095825D /* Chatroom+CoreDataClass.swift */, E90847292196FEA80095825D /* Chatroom+CoreDataProperties.swift */, - E90847192196FE590095825D /* Adamant.xcdatamodeld */, + 3A2F55F72AC6F308000A3F26 /* CoinTransaction+CoreDataClass.swift */, + 3A2F55F82AC6F308000A3F26 /* CoinTransaction+CoreDataProperties.swift */, + 3A4068332ACD7C18007E87BD /* CoinTransaction+TransactionDetails.swift */, ); path = CoreData; sourceTree = ""; @@ -2043,7 +2364,7 @@ children = ( 411742FE2A39B1B1008CD98A /* Contribute */, 4197B9C72952FAA2004CAF64 /* VisibleWallets */, - E948E03A20235E2300975D6B /* SettingsRoutes.swift */, + E948E03A20235E2300975D6B /* SettingsFactory.swift */, E90055F420EBF5DA00D0CB2D /* AboutViewController.swift */, E90055F620EC200900D0CB2D /* SecurityViewController.swift */, E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */, @@ -2088,6 +2409,7 @@ E9CAE8D02018AA5000345E76 /* ApiService */ = { isa = PBXGroup; children = ( + 93E8EDCC2AF1BD65003E163C /* AdamantApiCore.swift */, E91947AF20002393001362F8 /* AdamantApiService.swift */, E9CAE8D12018AA7700345E76 /* AdamantApi+Accounts.swift */, E9CAE8D92018ACD300345E76 /* AdamantApi+Chats.swift */, @@ -2095,9 +2417,7 @@ E9CAE8D52018AC5300345E76 /* AdamantApi+Transactions.swift */, E9CAE8D72018ACA700345E76 /* AdamantApi+Transfers.swift */, E965A52F20B594120041A3EA /* AdamantApi+States.swift */, - E9A03FD520DBC8E2007653A1 /* AdamantApi+Peers.swift */, 644EC34C20EFA60900F40C73 /* AdamantApi+Delegates.swift */, - 5558A435282AAFCC0024DDD6 /* AdamantApi+Status.swift */, ); path = ApiService; sourceTree = ""; @@ -2105,7 +2425,7 @@ E9E7CDA52002AE1C00DFC4DB /* Account */ = { isa = PBXGroup; children = ( - E9E7CDB02002B97B00DFC4DB /* AccountRoutes.swift */, + E9E7CDB02002B97B00DFC4DB /* AccountFactory.swift */, E983AE2820E65F3200497E1A /* AccountViewController.swift */, E908473A219707200095825D /* AccountViewController+StayIn.swift */, E983AE2020E655C500497E1A /* AccountHeaderView.swift */, @@ -2121,6 +2441,8 @@ E9E7CDB82003AA8E00DFC4DB /* SharedViews */ = { isa = PBXGroup; children = ( + E921597A206503000000CA5C /* ButtonsStripeView.swift */, + E921597C2065031D0000CA5C /* ButtonsStripe.xib */, 93496B9F2A6CAE9300DD062F /* LogoFullHeader.xib */, E9942B86203D9E5100C163AF /* EurekaQRRow.swift */, E9942B88203D9ECA00C163AF /* QrCell.xib */, @@ -2164,20 +2486,6 @@ path = AdamantTests; sourceTree = ""; }; - E9FAE5DB203ECD41008D3A6B /* Shared */ = { - isa = PBXGroup; - children = ( - E94E7B07205D4CB80042B639 /* SharedRoutes.swift */, - 6458548A211B3AB1004C5909 /* WelcomeViewController.xib */, - E9FAE5E0203ED1AE008D3A6B /* ShareQrViewController.swift */, - 93547BC929E2262D00B0914B /* WelcomeViewController.swift */, - E9FAE5E1203ED1AE008D3A6B /* ShareQrViewController.xib */, - E921597A206503000000CA5C /* ButtonsStripeView.swift */, - E921597C2065031D0000CA5C /* ButtonsStripe.xib */, - ); - path = Shared; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -2632,26 +2940,33 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 93CCAE7E2B06DA6C00EA5B94 /* DogeBlockDTO.swift in Sources */, 6448C291235CA6E100F3F15B /* ERC20WalletService+RichMessageProviderWithStatusCheck.swift in Sources */, E9256F5F2034C21100DE86E9 /* String+localized.swift in Sources */, 6414C18E217DF43100373FA6 /* String+adamant.swift in Sources */, + 9338AE812AEF4B8E001D32DF /* NodesStorage.swift in Sources */, 93A118532993241D00E144CC /* ChatMessagesListFactory.swift in Sources */, E908472A2196FEA80095825D /* RichMessageTransaction+CoreDataClass.swift in Sources */, 4E9EE86F28CE793D008359F7 /* SafeDecimalRow.swift in Sources */, + 93ADE0732ACA66AF008ED641 /* VibrationSelectionFactory.swift in Sources */, + 9366589B2B0AD3E600BDB2D3 /* WalletApiService.swift in Sources */, 937751AB2A68BB390054BD65 /* ChatTransactionCell.swift in Sources */, 64A223D620F760BB005157CB /* Localization.swift in Sources */, 64E1C82F222E95F6006C4DA7 /* DogeWallet.swift in Sources */, E9484B7D2285BAD9008E10F0 /* PrivateKeyGenerator.swift in Sources */, - E94E7B08205D4CB80042B639 /* SharedRoutes.swift in Sources */, + E94E7B08205D4CB80042B639 /* ShareQRFactory.swift in Sources */, 9377FBDF296C2A2F00C9211B /* ChatTransactionContentView.swift in Sources */, E9960B3621F5154300C840A8 /* DummyAccount+CoreDataProperties.swift in Sources */, 4186B332294200B4006594A3 /* BtcWalletService+DynamicConstants.swift in Sources */, 93A118512993167500E144CC /* ChatMessageBackgroundColor.swift in Sources */, E9CAE8D22018AA7700345E76 /* AdamantApi+Accounts.swift in Sources */, + 936658A32B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift in Sources */, 648CE3A6229AD1CD0070A2CC /* DashWalletService+Send.swift in Sources */, E987024920C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift in Sources */, 6403F5E222723F7500D58779 /* DashWallet.swift in Sources */, + 93294B822AAD0BB400911109 /* BtcWalletFactory.swift in Sources */, 648DD7A42237DB9E00B811FD /* DogeWalletService+Send.swift in Sources */, + 93294B7D2AAD067000911109 /* AppContainer.swift in Sources */, 41A1994229D2D3920031AD75 /* SwipePanGestureRecognizer.swift in Sources */, E9942B84203CBFCE00C163AF /* AdamantQRTools.swift in Sources */, 4184F1732A33102800D7B8B9 /* AdamantCrashlysticsService.swift in Sources */, @@ -2663,24 +2978,27 @@ 93496B832A6C85F400DD062F /* AdamantResources+CoreData.swift in Sources */, 41A1995229D42C460031AD75 /* ChatMessageCell.swift in Sources */, E94008872114F05B00CD2D67 /* AddressValidationResult.swift in Sources */, - 64A223D820F7A08E005157CB /* LskApiService.swift in Sources */, E9E7CD8F20026CD300DFC4DB /* AdamantDialogService.swift in Sources */, E993301E212EF39700CD5200 /* EthTransferViewController.swift in Sources */, E9CAE8DA2018ACD300345E76 /* AdamantApi+Chats.swift in Sources */, 648CE3A42299A94D0070A2CC /* DashTransactionDetailsViewController.swift in Sources */, E90847362196FEA80095825D /* Chatroom+CoreDataClass.swift in Sources */, + 3A4068342ACD7C18007E87BD /* CoinTransaction+TransactionDetails.swift in Sources */, E91947B020002393001362F8 /* AdamantApiService.swift in Sources */, E921597B206503000000CA5C /* ButtonsStripeView.swift in Sources */, - E9A03FD820DC0ABA007653A1 /* AdamantNodesSource.swift in Sources */, + 93FC16A12B01DE120062B507 /* ERC20ApiService.swift in Sources */, + 93294B9A2AAD624100911109 /* WalletFactoryCompose.swift in Sources */, 41C1698E29E7F36900FEB3CB /* AdamantRichTransactionReplyService.swift in Sources */, 649D6BF221C27D5C009E727B /* SearchResultsViewController.swift in Sources */, E9E7CD8D20026B6600DFC4DB /* DialogService.swift in Sources */, E9E7CDB72003994E00DFC4DB /* AdamantUtilities+extended.swift in Sources */, E9147B6320505C7500145913 /* QRCodeReader+adamant.swift in Sources */, E90055F720EC200900D0CB2D /* SecurityViewController.swift in Sources */, + 939FA3422B0D6F0000710EC6 /* SelfRemovableHostingController.swift in Sources */, 644EC35E20F34F1E00F40C73 /* DelegateDetailsViewController.swift in Sources */, E9942B80203C058C00C163AF /* QRGeneratorViewController.swift in Sources */, 4184F1772A33173100D7B8B9 /* ContributeView.swift in Sources */, + 3A7BD0122AA9BD5A0045AAB0 /* AdamantVibroType.swift in Sources */, E921597520611A6A0000CA5C /* AdamantReachability.swift in Sources */, E9960B3321F5154300C840A8 /* BaseAccount+CoreDataClass.swift in Sources */, E9FCA1E6218334C00005E83D /* SimpleTransactionDetails.swift in Sources */, @@ -2688,31 +3006,36 @@ 932F77592989F999006D8801 /* ChatCellManager.swift in Sources */, 9377FBE2296C2ACA00C9211B /* ChatTransactionContentView+Model.swift in Sources */, E933475B225539390083839E /* DogeGetTransactionsResponse.swift in Sources */, - 6489794D24CE00C000C33A68 /* SwiftyOnboardPage.swift in Sources */, 9340078029AC341100A20622 /* ChatAction.swift in Sources */, - 550066C5284D65DB0044C0B1 /* HealthCheckService.swift in Sources */, 648DD7A02236A59200B811FD /* DogeTransactionDetailsViewController.swift in Sources */, + 93ADC17D2B083C3B00F2DF77 /* NodesAdditionalParamsStorageProtocol.swift in Sources */, + 938A46A42AE6103E00FC03DB /* HealthCheckWrapper.swift in Sources */, 557AC308287B1365004699D7 /* CheckmarkRowView.swift in Sources */, 9390C5052976B53000270CDF /* ChatDialog.swift in Sources */, 6455E9F321075D8000B2E94C /* AdamantAddressBookService.swift in Sources */, - 9324C75E297170600022D7EA /* RichTransactionStatusService.swift in Sources */, + 9324C75E297170600022D7EA /* TransactionStatusService.swift in Sources */, 9304F8BE292F88F900173F18 /* ANSPayload.swift in Sources */, 41CA598C29A0D84F002BFDE4 /* TaskManager.swift in Sources */, - E9E7CD9120026FA100DFC4DB /* SwinjectDependencies.swift in Sources */, + E9E7CD9120026FA100DFC4DB /* AppAssembly.swift in Sources */, E96D64CA2295C4A800CA5587 /* WordList.swift in Sources */, 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */, + 93B28EC22B076D31007F268B /* DashApiService.swift in Sources */, E908472B2196FEA80095825D /* RichMessageTransaction+CoreDataProperties.swift in Sources */, A578BDE52623051C00090141 /* DashWalletService+Transactions.swift in Sources */, + 93C794442B07725C00408826 /* DashGetAddressBalanceDTO.swift in Sources */, 6449BA69235CA0930033B936 /* ERC20TransferViewController.swift in Sources */, 93684A2A29EFA28A00F9EFFE /* FixedTextMessageSizeCalculator.swift in Sources */, E9E7CD8B20026B0600DFC4DB /* AccountService.swift in Sources */, 41E3C9CC2A0E20F500AF0985 /* AdamantCoinTools.swift in Sources */, + 93ADE0712ACA66AF008ED641 /* VibrationSelectionViewModel.swift in Sources */, + 93CC94C12B17EE73004842AC /* EthApiCore.swift in Sources */, + 93FC169D2B019F440062B507 /* EthApiService.swift in Sources */, 9371130F2996EDA900F64CF9 /* ChatRefreshMock.swift in Sources */, 93547BCA29E2262D00B0914B /* WelcomeViewController.swift in Sources */, 41047B74294C61D10039E956 /* VisibleWalletsService.swift in Sources */, 648CE3AC229AD2190070A2CC /* DashTransferViewController.swift in Sources */, + 3A7BD0102AA9BD030045AAB0 /* AdamantVibroService.swift in Sources */, A5E04227282A8BDC0076CD13 /* BtcBalanceResponse.swift in Sources */, - A50A41072822F8CE006BDFE1 /* BtcWalletRoutes.swift in Sources */, 64F085D920E2D7600006DE68 /* AdmTransactionsViewController.swift in Sources */, 9322E87B2970431200B8357C /* ChatMessageFactory.swift in Sources */, 648DD7AA2239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, @@ -2723,14 +3046,18 @@ E9B3D39A201F90570019EB36 /* AccountsProvider.swift in Sources */, 4186B338294200E8006594A3 /* DogeWalletService+DynamicConstants.swift in Sources */, 417BA7F428BF894F00DF94C5 /* NotificationSoundsViewController.swift in Sources */, + 3A96E37C2AED27F8001F5A52 /* PartnerQRService.swift in Sources */, E950652320404C84008352E5 /* AdamantUriTools.swift in Sources */, 3A41938F2A580C57006A6B22 /* AdamantRichTransactionReactService.swift in Sources */, + 93E8EDD12AF1DF8E003E163C /* ServerResponse+Resolver.swift in Sources */, E95F85C7200A9B070070534A /* ChatTableViewCell.swift in Sources */, A50A41082822F8CE006BDFE1 /* BtcWalletService.swift in Sources */, + 937736822B0949C500B35C7A /* NodeCell+Model.swift in Sources */, 6455E9F121075D3600B2E94C /* AddressBookService.swift in Sources */, + 93C794482B0778C700408826 /* DashGetBlockDTO.swift in Sources */, 6449BA6D235CA0930033B936 /* ERC20TransactionsViewController.swift in Sources */, E983AE2A20E65F3200497E1A /* AccountViewController.swift in Sources */, - 6449BA70235CA0930033B936 /* ERC20WalletRouter.swift in Sources */, + 6449BA70235CA0930033B936 /* ERC20WalletFactory.swift in Sources */, 4184F1752A33106200D7B8B9 /* CrashlysticsService.swift in Sources */, 4197B9C92952FAFF004CAF64 /* VisibleWalletsCheckmarkView.swift in Sources */, E9E7CD932002740500DFC4DB /* AdamantAccountService.swift in Sources */, @@ -2739,10 +3066,11 @@ 64FA53CD20E1300B006783C9 /* EthTransactionsViewController.swift in Sources */, 6449BA6A235CA0930033B936 /* ERC20Wallet.swift in Sources */, E9147B612050599000145913 /* LoginViewController+QR.swift in Sources */, - 6489794E24CE00C000C33A68 /* SwiftyOnboard.swift in Sources */, 9399F5ED29A85A48006C3E30 /* ChatCacheService.swift in Sources */, 3A9015A92A615893002A2464 /* ChatMessagesListViewModel.swift in Sources */, + 936658952B0AC15300BDB2D3 /* Node+UI.swift in Sources */, 41A1995429D56E340031AD75 /* ChatMessageReplyCell.swift in Sources */, + 93294B872AAD0E0A00911109 /* AdmWallet.swift in Sources */, 6449BA6B235CA0930033B936 /* ERC20TransactionDetailsViewController.swift in Sources */, E907350E2256779C00BF02CC /* DogeMainnet.swift in Sources */, 41CE153A297FF98200CC9254 /* Web3Swift+Adamant.swift in Sources */, @@ -2751,31 +3079,33 @@ E9FAE5E2203ED1AE008D3A6B /* ShareQrViewController.swift in Sources */, E983AE2120E655C500497E1A /* AccountHeaderView.swift in Sources */, E971591C2168209800A5F904 /* EthWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, - 644EC35220EFA9A300F40C73 /* DelegateRoutes.swift in Sources */, + 644EC35220EFA9A300F40C73 /* DelegatesFactory.swift in Sources */, E96BBE3121F70F5E009AA738 /* ReadonlyTextView.swift in Sources */, A50A41112822FC35006BDFE1 /* BtcWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, E926E032213EC43B005E536B /* FullscreenAlertView.swift in Sources */, 644EC35B20EFB8E900F40C73 /* AdamantDelegateCell.swift in Sources */, 6403F5DB2272389800D58779 /* (null) in Sources */, - 6416B1A521AEE157006089AC /* LskWalletService+Transfers.swift in Sources */, 648DD7A62237DC4000B811FD /* DogeTransferViewController.swift in Sources */, 93E1234B2A6DFEF7004DF33B /* NotificationStrings.swift in Sources */, E9960B3421F5154300C840A8 /* BaseAccount+CoreDataProperties.swift in Sources */, - 550066C7284D682D0044C0B1 /* AdamantHealthCheckService.swift in Sources */, 4153045929C09902000E4BEA /* AdamantIncreaseFeeService.swift in Sources */, + 3AA50DEF2AEBE65D00C58FC8 /* PartnerQRView.swift in Sources */, E90A494B204D9EB8009F6A65 /* AdamantAuthentication.swift in Sources */, + 936658972B0ACB1500BDB2D3 /* CoinsNodesListViewModel.swift in Sources */, E9215973206119FB0000CA5C /* ReachabilityMonitor.swift in Sources */, + 9338AE8B2AEF7E37001D32DF /* APIParametersEncoding.swift in Sources */, + 3A2F55FC2AC6F885000A3F26 /* CoinStorage.swift in Sources */, E91947B420002809001362F8 /* AdamantAccount.swift in Sources */, + 3AA50DF12AEBE66A00C58FC8 /* PartnerQRViewModel.swift in Sources */, 6449BA6C235CA0930033B936 /* ERC20WalletViewController.swift in Sources */, E9E7CDC22003F5A400DFC4DB /* TransactionsListViewControllerBase.swift in Sources */, E905D39F204C281400DDB504 /* LoginViewController.swift in Sources */, - E9B1AA592122D59600080A2A /* WalletsRoutes.swift in Sources */, A50A41142822FC35006BDFE1 /* BtcTransferViewController.swift in Sources */, + 93294B8E2AAD2C6B00911109 /* SwiftyOnboardPage.swift in Sources */, E971591A21681D6900A5F904 /* TransactionStatus.swift in Sources */, E96D64B62295BED700CA5587 /* NormalizedTransaction.swift in Sources */, 648CE3A022999C890070A2CC /* BaseBtcTransaction.swift in Sources */, A50A410A2822F8CE006BDFE1 /* BtcWallet.swift in Sources */, - E9E7CDB52002BA6900DFC4DB /* SwinjectedRouter.swift in Sources */, E908472C2196FEA80095825D /* CoreDataAccount+CoreDataClass.swift in Sources */, E9A03FD220DBC0F2007653A1 /* NodeEditorViewController.swift in Sources */, E9393FAA2055D03300EE6F30 /* AdamantMessage.swift in Sources */, @@ -2783,13 +3113,16 @@ E90A494D204DA932009F6A65 /* LocalAuthentication.swift in Sources */, E96D64C62295C3ED00CA5587 /* Mnemonic+extended.swift in Sources */, 41047B70294B5EE10039E956 /* VisibleWalletsViewController.swift in Sources */, + 93B28EC82B076E68007F268B /* DashResponseDTO.swift in Sources */, A5BBD811262C657300B5C40C /* ByteBackpacker.swift in Sources */, 648BCA6D213D384F00875EB5 /* AvatarService.swift in Sources */, E95F856F2007B61D0070534A /* GetPublicKeyResponse.swift in Sources */, + 936658992B0AD32600BDB2D3 /* CoinsNodesListViewModel+ApiServices.swift in Sources */, 644EC34D20EFA60900F40C73 /* AdamantApi+Delegates.swift in Sources */, - E9E7CDAF2002B8A100DFC4DB /* Router.swift in Sources */, E940088F2119A9E800CD2D67 /* BigInt+Decimal.swift in Sources */, E9E7CDC72003F6D200DFC4DB /* TransactionTableViewCell.swift in Sources */, + 936658912B0AB9DC00BDB2D3 /* NodeWithGroup.swift in Sources */, + 9338AE862AEF6A97001D32DF /* APICore.swift in Sources */, 938F7D5D2955C8F9001915CA /* ChatLayoutManager.swift in Sources */, 6403F5DE22723C6800D58779 /* DashMainnet.swift in Sources */, 649D6BEA21B9627B009E727B /* LskWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, @@ -2803,7 +3136,7 @@ E96BBE3321F71290009AA738 /* BuyAndSellViewController.swift in Sources */, 64EAB37422463E020018D9B2 /* CurrencyInfoService.swift in Sources */, 93CC8DC7296F00D6003772BF /* ChatTransactionContainerView.swift in Sources */, - E9E7CDB32002B9FB00DFC4DB /* LoginRoutes.swift in Sources */, + E9E7CDB32002B9FB00DFC4DB /* LoginFactory.swift in Sources */, E941CCDE20E7B70200C96220 /* WalletCollectionViewCell.swift in Sources */, 4186B33A294200F4006594A3 /* DashWalletService+DynamicConstants.swift in Sources */, E9AA8BFA212C166600F9249F /* EthWalletService+Send.swift in Sources */, @@ -2811,41 +3144,50 @@ 93E5D4DB293000BE00439298 /* UnregisteredTransaction.swift in Sources */, 411DB8332A14D01F006AB158 /* ChatKeyboardManager.swift in Sources */, 6449BA68235CA0930033B936 /* ERC20WalletService.swift in Sources */, + 93B28ECA2B076E88007F268B /* DashErrorDTO.swift in Sources */, 644793C32166314A00FC4CF5 /* OnboardPage.swift in Sources */, 9345769528FD0C34004E6C7A /* UIViewController+email.swift in Sources */, 64E1C831222E9617006C4DA7 /* DogeWalletService.swift in Sources */, E91947B22000246A001362F8 /* AdamantError.swift in Sources */, 3A4193912A580C85006A6B22 /* RichTransactionReactService.swift in Sources */, + 93FC169B2B0197FD0062B507 /* BtcApiService.swift in Sources */, 3AA2D5F7280EADE3000ED971 /* SocketService.swift in Sources */, - E95F85802008C8D70070534A /* ChatsRoutes.swift in Sources */, + E95F85802008C8D70070534A /* ChatListFactory.swift in Sources */, 41A1994429D2D3CF0031AD75 /* MessageModel.swift in Sources */, 93775E462A674FA9009061AC /* Markdown+Adamant.swift in Sources */, + 93E8EDCD2AF1BD65003E163C /* AdamantApiCore.swift in Sources */, 6416B1A721B024B6006089AC /* LskWalletService+Send.swift in Sources */, E9942B87203D9E5100C163AF /* EurekaQRRow.swift in Sources */, + 3AA50DF32AEBE67C00C58FC8 /* PartnerQRFactory.swift in Sources */, E9AA8C02212C5BF500F9249F /* AdmWalletService+Send.swift in Sources */, E90847332196FEA80095825D /* TransferTransaction+CoreDataProperties.swift in Sources */, + 9366588D2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift in Sources */, E99818942120892F0018C84C /* WalletViewControllerBase.swift in Sources */, E9B3D39E201F99F40019EB36 /* DataProvider.swift in Sources */, 93BF4A6C29E4B4BF00505CD0 /* DelegatesBottomPanel+Model.swift in Sources */, + 93294B882AAD0E0A00911109 /* AdmWalletService.swift in Sources */, 648DD7A82239147800B811FD /* DogeWalletService+RichMessageProvider.swift in Sources */, E9E7CDC02003AF6D00DFC4DB /* AdamantCellFactory.swift in Sources */, E9DFB71C21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift in Sources */, - 6403F5E022723F6400D58779 /* DashWalletRouter.swift in Sources */, + 6403F5E022723F6400D58779 /* DashWalletFactory.swift in Sources */, E94008722114EACF00CD2D67 /* WalletAccount.swift in Sources */, + 3A7BD00E2AA9BCE80045AAB0 /* VibroService.swift in Sources */, E93B0D742028B21400126346 /* ChatsProvider.swift in Sources */, E9CAE8D42018AC1800345E76 /* AdamantApi+Keys.swift in Sources */, E9C51EEF20139DC600385EB7 /* TransactionIdResponse.swift in Sources */, A50A41092822F8CE006BDFE1 /* BtcWalletViewController.swift in Sources */, E9722066201F42BB004F2AAD /* CoreDataStack.swift in Sources */, - 93EE9C3329C2666200D9853F /* RichTransactionStatusSubscription.swift in Sources */, + 93EE9C3329C2666200D9853F /* TransactionStatusSubscription.swift in Sources */, + 9338AE7F2AEF43DA001D32DF /* NodesStorageProtocol.swift in Sources */, + 93EE9C3329C2666200D9853F /* TransactionStatusSubscription.swift in Sources */, E913C8F21FFFA51D001A83F7 /* AppDelegate.swift in Sources */, 648CE3A222999CE70070A2CC /* BTCRawTransaction.swift in Sources */, + 93C794462B07768F00408826 /* DashGetRawTransactionDTO.swift in Sources */, 648DD79E2236A0B500B811FD /* DogeTransactionsViewController.swift in Sources */, 64B5736F2209B892005DC968 /* BtcTransactionDetailsViewController.swift in Sources */, 938F7D612955C92B001915CA /* ChatDataSourceManager.swift in Sources */, - E9AA8BFC212C169200F9249F /* EthWalletService+Transfers.swift in Sources */, E96D64C82295C44400CA5587 /* Data+utilites.swift in Sources */, - 64E1C82D222E95E2006C4DA7 /* DogeWalletRoutes.swift in Sources */, + 64E1C82D222E95E2006C4DA7 /* DogeWalletFactory.swift in Sources */, E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */, E90847322196FEA80095825D /* TransferTransaction+CoreDataClass.swift in Sources */, 9304F8C6292F971600173F18 /* ApiServiceResult.swift in Sources */, @@ -2857,56 +3199,71 @@ 649D6BF021BFF481009E727B /* AdamantChatsProvider+search.swift in Sources */, 932BD15B29D2F75200AA1947 /* RichMessageProviderWithStatusCheck+Extension.swift in Sources */, E908473B219707200095825D /* AccountViewController+StayIn.swift in Sources */, + 93C7944C2B077B2700408826 /* DashGetAddressTransactionIds.swift in Sources */, E9204B5220C9762400F3B9AB /* MessageStatus.swift in Sources */, + 3A770E4C2AE14F130008D98F /* SimpleTransactionDetails+Hashable.swift in Sources */, E908471B2196FE590095825D /* Adamant.xcdatamodeld in Sources */, E940087B2114ED0600CD2D67 /* EthWalletService.swift in Sources */, - E948E03B20235E2300975D6B /* SettingsRoutes.swift in Sources */, + 93294B902AAD2C6B00911109 /* SwiftyOnboardOverlay.swift in Sources */, + E948E03B20235E2300975D6B /* SettingsFactory.swift in Sources */, E9CAE8D82018ACA700345E76 /* AdamantApi+Transfers.swift in Sources */, 4186B3302941E642006594A3 /* AdmWalletService+DynamicConstants.swift in Sources */, E95F85852008CB3A0070534A /* ChatListViewController.swift in Sources */, E9FEECA62143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift in Sources */, 6403F5E622723FDA00D58779 /* DashWalletViewController.swift in Sources */, + 93C7944E2B077C1F00408826 /* DashSendRawTransactionDTO.swift in Sources */, 4193AE1629FBEFBF002F21BE /* NSAttributedText+Adamant.swift in Sources */, + 9338AE8F2AEF8131001D32DF /* InternalAPIError.swift in Sources */, 41A1994829D325800031AD75 /* SwipeableView.swift in Sources */, 5558A438282AB9390024DDD6 /* NodeStatus.swift in Sources */, + 93FC169F2B01A3630062B507 /* LskApiCore.swift in Sources */, E91947AC20001A9A001362F8 /* ApiService.swift in Sources */, 4164A9D928F17DA700EEF16D /* AdamantChatTransactionService.swift in Sources */, - E993302221354BC300CD5200 /* EthWalletRoutes.swift in Sources */, + E993302221354BC300CD5200 /* EthWalletFactory.swift in Sources */, 418FDE502A25CA340055E3CD /* ChatMenuManager.swift in Sources */, E90055F520EBF5DA00D0CB2D /* AboutViewController.swift in Sources */, E965A53020B594120041A3EA /* AdamantApi+States.swift in Sources */, E908473D219713300095825D /* NotificationsViewController.swift in Sources */, E908472E2196FEA80095825D /* BaseTransaction+CoreDataClass.swift in Sources */, 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */, - E91E5BF220DAF05500B06B3C /* EurekaNodeRow.swift in Sources */, + E91E5BF220DAF05500B06B3C /* NodeCell.swift in Sources */, 4133AF242A1CE1A3001A0A1E /* UITableView+Adamant.swift in Sources */, 41A1995629D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift in Sources */, E90847312196FEA80095825D /* ChatTransaction+CoreDataProperties.swift in Sources */, + 3A2F55FA2AC6F308000A3F26 /* CoinTransaction+CoreDataProperties.swift in Sources */, E99330262136B0E500CD5200 /* TransferViewControllerBase+QR.swift in Sources */, + 93ADC17F2B083D7A00F2DF77 /* NodesAdditionalParamsStorage.swift in Sources */, E9B1AA5B21283E0F00080A2A /* AdmTransferViewController.swift in Sources */, 648C697322916192006645F5 /* DashTransactionsViewController.swift in Sources */, + 93E8EDCF2AF1CD9F003E163C /* NodeStatusInfo.swift in Sources */, 55E69E172868D7920025D82E /* CheckmarkView.swift in Sources */, + 93B28EC02B076667007F268B /* APIResponseModel.swift in Sources */, 9304F8C2292F895C00173F18 /* PushNotificationsTokenService.swift in Sources */, E940086B2114A70600CD2D67 /* LskAccount.swift in Sources */, - 6416B19D21AD7B92006089AC /* LskWalletRoutes.swift in Sources */, + 6416B19D21AD7B92006089AC /* LskWalletFactory.swift in Sources */, E9B3D3A1201FA26B0019EB36 /* AdamantAccountsProvider.swift in Sources */, E9FAE5DA203DBFEF008D3A6B /* Comparable+clamped.swift in Sources */, 93A91FD329799298001DB1F8 /* ChatStartPosition.swift in Sources */, - E94008802114EE2000CD2D67 /* AdmWallet.swift in Sources */, + 3A2F55F92AC6F308000A3F26 /* CoinTransaction+CoreDataClass.swift in Sources */, 93A91FD1297972B7001DB1F8 /* ChatScrollDownButton.swift in Sources */, 41C1698C29E7F34900FEB3CB /* RichTransactionReplyService.swift in Sources */, - E9A03FDA20DC0B14007653A1 /* NodesSource.swift in Sources */, - 935F53D629BE8F7400779492 /* RichTransactionStatusPublisher.swift in Sources */, + 935F53D629BE8F7400779492 /* TransactionStatusPublisher.swift in Sources */, + 935F53D629BE8F7400779492 /* TransactionStatusPublisher.swift in Sources */, 9390C5032976B42800270CDF /* ChatDialogManager.swift in Sources */, E926E02E213EAABF005E536B /* TransferViewControllerBase+Alert.swift in Sources */, + 936658A52B0AE67A00BDB2D3 /* CoinsNodesListFactory.swift in Sources */, E9B1AA572121ACC000080A2A /* AdmWalletViewController.swift in Sources */, + 93C7944A2B077A1C00408826 /* DashGetUnspentTransactionsDTO.swift in Sources */, E9240BF5215D686500187B09 /* AdmWalletService+RichMessageProvider.swift in Sources */, 648C697122915CB8006645F5 /* BTCRPCServerResponce.swift in Sources */, E9A174B32057EC47003667CD /* BackgroundFetchService.swift in Sources */, 649D6BE821B95DB7009E727B /* LskWalletService+RichMessageProvider.swift in Sources */, E9E7CDBE2003AEFB00DFC4DB /* CellFactory.swift in Sources */, + 9366589D2B0ADBAF00BDB2D3 /* CoinsNodesListView.swift in Sources */, 411743022A39B208008CD98A /* ContributeState.swift in Sources */, + 9366588F2B0AB97500BDB2D3 /* CoinsNodesListMapper.swift in Sources */, 93F391502962F5D400BFD6AE /* SpinnerView.swift in Sources */, + 93A18C892AAEAE7700D0AB98 /* WalletFactory.swift in Sources */, E923222621135F9000A7E5AF /* EthAccount.swift in Sources */, E9061B97207501E40011F104 /* AdamantUserInfoKey.swift in Sources */, E9CAE8D62018AC5300345E76 /* AdamantApi+Transactions.swift in Sources */, @@ -2915,27 +3272,34 @@ 6416B1A121AD7D93006089AC /* LskTransferViewController.swift in Sources */, A5E0422B282AB18B0076CD13 /* BtcUnspentTransactionResponse.swift in Sources */, E972206B201F44CA004F2AAD /* TransfersProvider.swift in Sources */, + 3A20D93B2AE7F316005475A6 /* AdamantTransactionDetails.swift in Sources */, + 93294B962AAD320B00911109 /* ScreensFactory.swift in Sources */, + 93CCAE772B06D6CC00EA5B94 /* LskServiceApiService.swift in Sources */, E9FEECA421413659007DD7C8 /* RichMessageProvider.swift in Sources */, 3A9015A72A614A62002A2464 /* AdamantEmojiService.swift in Sources */, + 93ADE0722ACA66AF008ED641 /* VibrationSelectionView.swift in Sources */, 648C696F22915A12006645F5 /* DashTransaction.swift in Sources */, 3A41939A2A5D554A006A6B22 /* Reaction.swift in Sources */, + 93CCAE752B06CC3600EA5B94 /* LskNodeApiService.swift in Sources */, 6416B1A321AD7EA1006089AC /* LskTransactionDetailsViewController.swift in Sources */, - E9A03FD620DBC8E2007653A1 /* AdamantApi+Peers.swift in Sources */, 6449BA6F235CA0930033B936 /* ERC20WalletService+Send.swift in Sources */, A50A41132822FC35006BDFE1 /* BtcWalletService+Send.swift in Sources */, A5E04229282A998C0076CD13 /* BtcTransactionResponse.swift in Sources */, 4186B334294200C5006594A3 /* EthWalletService+DynamicConstants.swift in Sources */, + 93CCAE802B06E2D100EA5B94 /* ApiServiceError+Extension.swift in Sources */, 644EC34F20EFA77A00F40C73 /* Delegate.swift in Sources */, 64EAB37622463F680018D9B2 /* AdamantCurrencyInfoService.swift in Sources */, + 93294B842AAD0C8F00911109 /* Assembler+Extension.swift in Sources */, 938F7D5B2955C8DA001915CA /* ChatDisplayManager.swift in Sources */, E9722068201F42CC004F2AAD /* InMemoryCoreDataStack.swift in Sources */, 4133AED429769EEC00F3D017 /* UpdatingIndicatorView.swift in Sources */, 551F66E628959A5300DE5D69 /* LoadingView.swift in Sources */, + 93B28EC52B076E2C007F268B /* DashBlockchainInfoDTO.swift in Sources */, E98FC34420F920BD00032D65 /* UIFont+adamant.swift in Sources */, 644EC35720EFAAB700F40C73 /* DelegatesListViewController.swift in Sources */, 4153045B29C09C6C000E4BEA /* IncreaseFeeService.swift in Sources */, E9C51EF12013F18000385EB7 /* NewChatViewController.swift in Sources */, - 9324C760297171040022D7EA /* AdamantRichTransactionStatusService.swift in Sources */, + 9324C760297171040022D7EA /* AdamantTransactionStatusService.swift in Sources */, E9B4E1A8210F079E007E77FC /* DoubleDetailsTableViewCell.swift in Sources */, E9502740202E257E002C1098 /* RepeaterService.swift in Sources */, E93D7AC02052CF63005D19DC /* AdamantNotificationService.swift in Sources */, @@ -2946,13 +3310,17 @@ 649D6BEC21BD5A53009E727B /* UISuffixTextField.swift in Sources */, E93B0D762028B28E00126346 /* AdamantChatsProvider.swift in Sources */, 3A33F9FA2A7A53DA002B8003 /* EmojiUpdateType.swift in Sources */, - E993302021354B1800CD5200 /* AdmWalletRoutes.swift in Sources */, - E9332B8921F1FA4400D56E72 /* OnboardRoutes.swift in Sources */, + 936658932B0AC03700BDB2D3 /* CoinsNodesListStrings.swift in Sources */, + E993302021354B1800CD5200 /* AdmWalletFactory.swift in Sources */, + E9332B8921F1FA4400D56E72 /* OnboardFactory.swift in Sources */, 938F7D722955CE72001915CA /* ChatFactory.swift in Sources */, + 93CCAE792B06D81D00EA5B94 /* DogeApiService.swift in Sources */, 938F7D5F2955C90D001915CA /* ChatInputBarManager.swift in Sources */, E908472D2196FEA80095825D /* CoreDataAccount+CoreDataProperties.swift in Sources */, 64FA53D120E24942006783C9 /* TransactionDetailsViewControllerBase.swift in Sources */, + 9338AE842AEF5EFA001D32DF /* APICoreProtocol.swift in Sources */, 41047B76294C62710039E956 /* AdamantVisibleWalletsService.swift in Sources */, + 93294B982AAD364F00911109 /* AdamantScreensFactory.swift in Sources */, 64BD2B7720E2820300E2CD36 /* TransactionDetails.swift in Sources */, 3A9015A52A614A18002A2464 /* EmojiService.swift in Sources */, 9322E875297042F000B8357C /* ChatSender.swift in Sources */, @@ -2963,14 +3331,17 @@ E90847302196FEA80095825D /* ChatTransaction+CoreDataClass.swift in Sources */, E913C9081FFFA943001A83F7 /* AdamantCore.swift in Sources */, 41935848287841E20083363B /* MacOSDeterminer.swift in Sources */, + 3A96E37A2AED27D7001F5A52 /* AdamantPartnerQRService.swift in Sources */, E9EC342120052ABB00C0E546 /* TransferViewControllerBase.swift in Sources */, 9304F8C4292F8A3100173F18 /* AdamantPushNotificationsTokenService.swift in Sources */, 9300F94629D0149100FEDDB8 /* RichMessageProviderWithStatusCheck.swift in Sources */, E9771D9E22997A6F0099AAC7 /* NativeCore+AdamantCore.swift in Sources */, 416380E12A51765F00F90E6D /* ChatReactionsView.swift in Sources */, - 5558A436282AAFCC0024DDD6 /* AdamantApi+Status.swift in Sources */, + 938A46A62AE6106300FC03DB /* BlockchainHealthCheckWrapper.swift in Sources */, E921534E20EE1E8700C0843F /* EurekaAlertLabelRow.swift in Sources */, + 9338AE8D2AEF7E9C001D32DF /* BodyStringEncoding.swift in Sources */, 64C65F4523893C7600DC0425 /* OnboardOverlay.swift in Sources */, + 93A18C862AAEACC100D0AB98 /* AdamantWalletFactoryCompose.swift in Sources */, E9484B79227C617E008E10F0 /* BalanceTableViewCell.swift in Sources */, E90847352196FEA80095825D /* MessageTransaction+CoreDataProperties.swift in Sources */, 4186B336294200D2006594A3 /* LskWalletService+DynamicConstants.swift in Sources */, @@ -2981,14 +3352,15 @@ 41A1994629D2FCF80031AD75 /* ReplyView.swift in Sources */, E90847342196FEA80095825D /* MessageTransaction+CoreDataClass.swift in Sources */, E9960B3521F5154300C840A8 /* DummyAccount+CoreDataClass.swift in Sources */, - E94008892114F0F700CD2D67 /* AdmWalletService.swift in Sources */, 64EE46B220FE0C8D00194DDA /* LskTransactionsViewController.swift in Sources */, E94008832114EE4700CD2D67 /* LskWallet.swift in Sources */, 64E1C833222EA0F0006C4DA7 /* DogeWalletViewController.swift in Sources */, - E93EB09F20DA3FA4001F9601 /* NodesEditorRoutes.swift in Sources */, - 6489794F24CE00C000C33A68 /* SwiftyOnboardOverlay.swift in Sources */, + E93EB09F20DA3FA4001F9601 /* NodesEditorFactory.swift in Sources */, + 93294B8F2AAD2C6B00911109 /* SwiftyOnboard.swift in Sources */, + 93ADC17B2B08283500F2DF77 /* ForceQueryItemsEncoding.swift in Sources */, 41BCB310295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift in Sources */, - E9E7CDB12002B97B00DFC4DB /* AccountRoutes.swift in Sources */, + 93CCAE7B2B06D9B500EA5B94 /* DogeBlocksDTO.swift in Sources */, + E9E7CDB12002B97B00DFC4DB /* AccountFactory.swift in Sources */, E9AA8BF82129F13000F9249F /* ComplexTransferViewController.swift in Sources */, E9A174B52057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift in Sources */, 9382F61329DEC0A3005E6216 /* ChatModelView.swift in Sources */, @@ -3000,6 +3372,7 @@ E90055FB20ECE78A00D0CB2D /* SecurityViewController+notifications.swift in Sources */, 938F7D662955C966001915CA /* ChatInputBar.swift in Sources */, A50A41122822FC35006BDFE1 /* BtcWalletService+RichMessageProvider.swift in Sources */, + 3A2F55FE2AC6F90E000A3F26 /* AdamantCoinStorageService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3140,7 +3513,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.2.1; + MARKETING_VERSION = 3.3.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev.MessageNotificationContentExtension"; @@ -3170,7 +3543,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.2.1; + MARKETING_VERSION = 3.3.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger.MessageNotificationContentExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3322,7 +3695,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.2.1; + MARKETING_VERSION = 3.3.0; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3353,7 +3726,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.2.1; + MARKETING_VERSION = 3.3.0; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3381,7 +3754,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.2.1; + MARKETING_VERSION = 3.3.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev.TransferNotificationContentExtension"; @@ -3411,7 +3784,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.2.1; + MARKETING_VERSION = 3.3.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger.TransferNotificationContentExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3442,7 +3815,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.2.1; + MARKETING_VERSION = 3.3.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev.NotificationServiceExtension"; @@ -3472,7 +3845,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.2.1; + MARKETING_VERSION = 3.3.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger.NotificationServiceExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved index 95b69cc0b..88a2fda6f 100644 --- a/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -105,8 +105,8 @@ "repositoryURL": "https://github.com/google/GoogleDataTransport.git", "state": { "branch": null, - "revision": "98a00258d4518b7521253a70b7f70bb76d2120fe", - "version": "9.2.4" + "revision": "aae45a320fd0d11811820335b1eabc8753902a40", + "version": "9.2.5" } }, { @@ -114,8 +114,8 @@ "repositoryURL": "https://github.com/google/GoogleUtilities.git", "state": { "branch": null, - "revision": "4446686bc3714d49ce043d0f68318f42ed718cb6", - "version": "7.11.4" + "revision": "bc27fad73504f3d4af235de451f02ee22586ebd3", + "version": "7.12.1" } }, { @@ -141,8 +141,8 @@ "repositoryURL": "https://github.com/nathantannar4/InputBarAccessoryView", "state": { "branch": null, - "revision": "039a9cb3ae8c5bc4d39242a6aa688b88023633d3", - "version": "6.2.0" + "revision": "17ced92a5dccb36512b408b6276353631d7cbe57", + "version": "6.3.0" } }, { @@ -159,8 +159,8 @@ "repositoryURL": "https://github.com/firebase/leveldb.git", "state": { "branch": null, - "revision": "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", - "version": "1.22.2" + "revision": "9d108e9112aa1d65ce508facf804674546116d9c", + "version": "1.22.3" } }, { @@ -177,8 +177,8 @@ "repositoryURL": "https://github.com/MessageKit/MessageKit.git", "state": { "branch": null, - "revision": "14bfa7eb9f93267c3d7b8cdf58615bba27be672a", - "version": "4.1.1" + "revision": "1993e8e90d4e9a61b8d9bc8ceb733964ce943376", + "version": "4.2.0" } }, { @@ -276,7 +276,7 @@ "repositoryURL": "https://github.com/socketio/socket.io-client-swift", "state": { "branch": "master", - "revision": "a1ed825835a2d8c2555938e96557ccc05e4bebf3", + "revision": "175da8b5156f6b132436f0676cc84c2f6a766b6e", "version": null } }, @@ -285,8 +285,8 @@ "repositoryURL": "https://github.com/daltoniam/Starscream", "state": { "branch": null, - "revision": "df8d82047f6654d8e4b655d1b1525c64e1059d21", - "version": "4.0.4" + "revision": "ac6c0fc9da221873e01bd1a0d4818498a71eef33", + "version": "4.0.6" } }, { @@ -294,8 +294,8 @@ "repositoryURL": "https://github.com/apple/swift-protobuf.git", "state": { "branch": null, - "revision": "ce20dc083ee485524b802669890291c0d8090170", - "version": "1.22.1" + "revision": "07f7f26ded8df9645c072f220378879c4642e063", + "version": "1.25.1" } }, { diff --git a/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents b/Adamant/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents similarity index 86% rename from Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents rename to Adamant/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents index bce0ebf0e..bc9d6af50 100644 --- a/Adamant/CoreData/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents +++ b/Adamant/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents @@ -8,26 +8,10 @@ - - - - - - - - - - - + - - - - - - @@ -42,6 +26,7 @@ + @@ -50,6 +35,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Adamant/AppDelegate.swift b/Adamant/App/AppDelegate.swift similarity index 96% rename from Adamant/AppDelegate.swift rename to Adamant/App/AppDelegate.swift index 7af0cdaa8..5f47035aa 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/App/AppDelegate.swift @@ -40,7 +40,8 @@ extension StoreKey { class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var repeater: RepeaterService! - var container: Container! + var container: AppContainer! + var screensFactory: ScreensFactory! // MARK: Dependencies var accountService: AccountService! @@ -57,8 +58,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { KeychainStore.migrateIfNeeded() // MARK: 1. Initiating Swinject - container = Container() - container.registerAdamantServices() + container = AppContainer() + screensFactory = AdamantScreensFactory(assembler: container.assembler) accountService = container.resolve(AccountService.self) notificationService = container.resolve(NotificationsService.self) dialogService = container.resolve(DialogService.self) @@ -93,16 +94,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window.tintColor = UIColor.adamant.primary // MARK: 3. Prepare pages - guard let router = container.resolve(Router.self) else { - fatalError("Failed to get Router") - } - let chatList = UINavigationController( - rootViewController: router.get(scene: AdamantScene.Chats.chatList) + rootViewController: screensFactory.makeChatList() ) let account = UINavigationController( - rootViewController: router.get(scene: AdamantScene.Account.account) + rootViewController: screensFactory.makeAccount() ) let tabScreens: TabScreens = UIScreen.main.traitCollection.userInterfaceIdiom == .pad @@ -156,7 +153,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { dialogService.setup(window: window) // MARK: 5. Show login - let login = router.get(scene: AdamantScene.Login.login) as! LoginViewController + let login = screensFactory.makeLogin() let welcomeIsShown = UserDefaults.standard.bool(forKey: StoreKey.application.welcomeScreensIsShown) login.requestBiometryOnFirstTimeActive = welcomeIsShown @@ -164,7 +161,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window.rootViewController?.present(login, animated: false, completion: nil) if !welcomeIsShown { - let welcome = router.get(scene: AdamantScene.Onboard.welcome) + let welcome = screensFactory.makeOnboard() welcome.modalPresentationStyle = .overFullScreen login.present(welcome, animated: true, completion: nil) UserDefaults.standard.set(true, forKey: StoreKey.application.welcomeScreensIsShown) @@ -205,7 +202,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } // Setup transactions statuses observing - if let service = container.resolve(RichTransactionStatusService.self) { + if let service = container.resolve(TransactionStatusService.self) { Task { await service.startObserving() } } @@ -443,8 +440,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { // MARK: - Background Fetch extension AppDelegate { func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - let container = Container() - container.registerAdamantServices() + let container = AppContainer() guard let notificationsService = container.resolve(NotificationsService.self) else { UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalNever) @@ -621,11 +617,7 @@ extension AppDelegate { var chatList: UINavigationController? var chatDetail: ChatListViewController? - guard let tabbar = window?.rootViewController as? UITabBarController, - let router = container.resolve(Router.self) - else { - return - } + guard let tabbar = window?.rootViewController as? UITabBarController else { return } if let split = tabbar.viewControllers?.first as? UISplitViewController, let navigation = split.viewControllers.first as? UINavigationController, @@ -652,7 +644,6 @@ extension AppDelegate { with: adamantAdr, chatList: chatList, tabbar: tabbar, - router: router, chatDetail: chatDetail ) } @@ -663,17 +654,13 @@ extension AppDelegate { with adamantAdr: AdamantAddress, chatList: UINavigationController, tabbar: UITabBarController, - router: Router, chatDetail: ChatListViewController ) { chatList.popToRootViewController(animated: false) chatList.dismiss(animated: false, completion: nil) tabbar.selectedIndex = 0 - let newChat = router.get(scene: AdamantScene.Chats.newChat) as? NewChatViewController - - guard let newChat = newChat else { return } - + let newChat = screensFactory.makeNewChat() newChat.delegate = chatDetail.self if let split = chatDetail.splitViewController { diff --git a/Adamant/SwinjectDependencies.swift b/Adamant/App/DI/AppAssembly.swift similarity index 55% rename from Adamant/SwinjectDependencies.swift rename to Adamant/App/DI/AppAssembly.swift index 50d8a98a5..e47b09bac 100644 --- a/Adamant/SwinjectDependencies.swift +++ b/Adamant/App/DI/AppAssembly.swift @@ -1,5 +1,5 @@ // -// SwinjectDependencies.swift +// AppAssembly.swift // Adamant // // Created by Anokhov Pavel on 07.01.2018. @@ -10,43 +10,35 @@ import Swinject import BitcoinKit import CommonKit -// MARK: - Services -extension Container { - func registerAdamantServices() { +struct AppAssembly: Assembly { + func assemble(container: Container) { // MARK: - Standalone services // MARK: AdamantCore - self.register(AdamantCore.self) { _ in NativeAdamantCore() }.inObjectScope(.container) - - // MARK: Router - self.register(Router.self) { _ in - let router = SwinjectedRouter() - router.container = self - return router - }.inObjectScope(.container) + container.register(AdamantCore.self) { _ in NativeAdamantCore() }.inObjectScope(.container) // MARK: CellFactory - self.register(CellFactory.self) { _ in AdamantCellFactory() }.inObjectScope(.container) + container.register(CellFactory.self) { _ in AdamantCellFactory() }.inObjectScope(.container) // MARK: Secured Store - self.register(SecuredStore.self) { _ in KeychainStore() }.inObjectScope(.container) + container.register(SecuredStore.self) { _ in KeychainStore() }.inObjectScope(.container) // MARK: LocalAuthentication - self.register(LocalAuthentication.self) { _ in AdamantAuthentication() }.inObjectScope(.container) + container.register(LocalAuthentication.self) { _ in AdamantAuthentication() }.inObjectScope(.container) // MARK: Reachability - self.register(ReachabilityMonitor.self) { _ in AdamantReachability() }.inObjectScope(.container) + container.register(ReachabilityMonitor.self) { _ in AdamantReachability() }.inObjectScope(.container) // MARK: AdamantAvatarService - self.register(AvatarService.self) { _ in AdamantAvatarService() }.inObjectScope(.container) + container.register(AvatarService.self) { _ in AdamantAvatarService() }.inObjectScope(.container) // MARK: - Services with dependencies // MARK: DialogService - self.register(DialogService.self) { r in - AdamantDialogService(router: r.resolve(Router.self)!) + container.register(DialogService.self) { r in + AdamantDialogService(vibroService: r.resolve(VibroService.self)!) }.inObjectScope(.container) // MARK: Notifications - self.register(NotificationsService.self) { r in + container.register(NotificationsService.self) { r in AdamantNotificationsService(securedStore: r.resolve(SecuredStore.self)!) }.initCompleted { (r, c) in // Weak reference Task { @MainActor in @@ -56,7 +48,7 @@ extension Container { }.inObjectScope(.container) // MARK: VisibleWalletsService - self.register(VisibleWalletsService.self) { r in + container.register(VisibleWalletsService.self) { r in AdamantVisibleWalletsService( securedStore: r.resolve(SecuredStore.self)!, accountService: r.resolve(AccountService.self)! @@ -64,28 +56,33 @@ extension Container { }.inObjectScope(.container) // MARK: IncreaseFeeService - self.register(IncreaseFeeService.self) { r in + container.register(IncreaseFeeService.self) { r in AdamantIncreaseFeeService( securedStore: r.resolve(SecuredStore.self)! ) }.inObjectScope(.container) // MARK: EmojiService - self.register(EmojiService.self) { r in + container.register(EmojiService.self) { r in AdamantEmojiService( securedStore: r.resolve(SecuredStore.self)! ) }.inObjectScope(.container) + // MARK: VibroService + container.register(VibroService.self) { _ in + AdamantVibroService() + }.inObjectScope(.container) + // MARK: CrashlysticsService - self.register(CrashlyticsService.self) { r in + container.register(CrashlyticsService.self) { r in AdamantCrashlyticsService( securedStore: r.resolve(SecuredStore.self)! ) }.inObjectScope(.container) // MARK: PushNotificationsTokenService - self.register(PushNotificationsTokenService.self) { r in + container.register(PushNotificationsTokenService.self) { r in AdamantPushNotificationsTokenService( securedStore: r.resolve(SecuredStore.self)!, apiService: r.resolve(ApiService.self)!, @@ -94,41 +91,109 @@ extension Container { ) }.inObjectScope(.container) - // MARK: NodesSource - self.register(NodesSource.self) { r in - AdamantNodesSource( - apiService: r.resolve(ApiService.self)!, - healthCheckService: r.resolve(HealthCheckService.self)!, - securedStore: r.resolve(SecuredStore.self)!, - defaultNodesGetter: { AdamantResources.nodes } - ) + // MARK: NodesStorage + container.register(NodesStorageProtocol.self) { r in + NodesStorage(securedStore: r.resolve(SecuredStore.self)!) + }.inObjectScope(.container) + + // MARK: NodesAdditionalParamsStorage + container.register(NodesAdditionalParamsStorageProtocol.self) { r in + NodesAdditionalParamsStorage(securedStore: r.resolve(SecuredStore.self)!) + }.inObjectScope(.container) + + // MARK: ApiCore + container.register(APICoreProtocol.self) { _ in + APICore() }.inObjectScope(.container) // MARK: ApiService - self.register(ApiService.self) { r in - AdamantApiService(adamantCore: r.resolve(AdamantCore.self)!) - }.initCompleted { (r, c) in // Weak reference - Task { @MainActor in - guard let service = c as? AdamantApiService else { return } - await service.setupWeakDeps(nodesSource: r.resolve(NodesSource.self)!) - } + container.register(ApiService.self) { r in + AdamantApiService( + healthCheckWrapper: .init( + service: .init(apiCore: r.resolve(APICoreProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + nodeGroup: .adm + ), + adamantCore: r.resolve(AdamantCore.self)! + ) + }.inObjectScope(.container) + + // MARK: BtcApiService + container.register(BtcApiService.self) { r in + BtcApiService(api: .init( + service: .init(apiCore: r.resolve(APICoreProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + nodeGroup: .btc + )) }.inObjectScope(.container) - // MARK: HealthCheckService - self.register(HealthCheckService.self) { r in - AdamantHealthCheckService(apiService: r.resolve(ApiService.self)!) + // MARK: DogeApiService + container.register(DogeApiService.self) { r in + DogeApiService(api: .init( + service: .init(apiCore: r.resolve(APICoreProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + nodeGroup: .doge + )) + }.inObjectScope(.container) + + // MARK: DashApiService + container.register(DashApiService.self) { r in + DashApiService(api: .init( + service: .init(apiCore: r.resolve(APICoreProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + nodeGroup: .dash + )) + }.inObjectScope(.container) + + // MARK: LskNodeApiService + container.register(LskNodeApiService.self) { r in + LskNodeApiService(api: .init( + service: .init(), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + nodeGroup: .lskNode + )) + }.inObjectScope(.container) + + // MARK: LskServiceApiService + container.register(LskServiceApiService.self) { r in + LskServiceApiService(api: .init( + service: .init(), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + nodeGroup: .lskService + )) + }.inObjectScope(.container) + + // MARK: EthApiService + container.register(EthApiService.self) { r in + r.resolve(ERC20ApiService.self)! + }.inObjectScope(.transient) + + // MARK: ERC20ApiService + container.register(ERC20ApiService.self) { r in + ERC20ApiService(api: .init( + service: .init(apiCore: r.resolve(APICoreProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + nodeGroup: .eth + )) }.inObjectScope(.container) // MARK: SocketService - self.register(SocketService.self) { _ in - AdamantSocketService() - }.initCompleted { (r, c) in // Weak reference - guard let service = c as? AdamantSocketService else { return } - service.nodesSource = r.resolve(NodesSource.self) + container.register(SocketService.self) { r in + AdamantSocketService( + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)! + ) }.inObjectScope(.container) // MARK: AccountService - self.register(AccountService.self) { r in + container.register(AccountService.self) { r in AdamantAccountService( apiService: r.resolve(ApiService.self)!, adamantCore: r.resolve(AdamantCore.self)!, @@ -143,13 +208,13 @@ extension Container { service.currencyInfoService = r.resolve(CurrencyInfoService.self)! service.visibleWalletService = r.resolve(VisibleWalletsService.self)! for case let wallet as SwinjectDependentService in service.wallets { - wallet.injectDependencies(from: self) + wallet.injectDependencies(from: container) } } } // MARK: AddressBookServeice - self.register(AddressBookService.self) { r in + container.register(AddressBookService.self) { r in AdamantAddressBookService( apiService: r.resolve(ApiService.self)!, adamantCore: r.resolve(AdamantCore.self)!, @@ -159,7 +224,7 @@ extension Container { }.inObjectScope(.container) // MARK: CurrencyInfoService - self.register(CurrencyInfoService.self) { r in + container.register(CurrencyInfoService.self) { r in AdamantCurrencyInfoService(securedStore: r.resolve(SecuredStore.self)!) }.inObjectScope(.container).initCompleted { (r, c) in guard let service = c as? AdamantCurrencyInfoService else { return } @@ -168,12 +233,12 @@ extension Container { // MARK: - Data Providers // MARK: CoreData Stack - self.register(CoreDataStack.self) { _ in + container.register(CoreDataStack.self) { _ in try! InMemoryCoreDataStack(modelUrl: AdamantResources.coreDataModel) }.inObjectScope(.container) // MARK: Accounts - self.register(AccountsProvider.self) { r in + container.register(AccountsProvider.self) { r in AdamantAccountsProvider( stack: r.resolve(CoreDataStack.self)!, apiService: r.resolve(ApiService.self)!, @@ -182,7 +247,7 @@ extension Container { }.inObjectScope(.container) // MARK: Transfers - self.register(TransfersProvider.self) { r in + container.register(TransfersProvider.self) { r in AdamantTransfersProvider( apiService: r.resolve(ApiService.self)!, stack: r.resolve(CoreDataStack.self)!, @@ -196,7 +261,7 @@ extension Container { }.inObjectScope(.container) // MARK: Chats - self.register(ChatsProvider.self) { r in + container.register(ChatsProvider.self) { r in AdamantChatsProvider( accountService: r.resolve(AccountService.self)!, apiService: r.resolve(ApiService.self)!, @@ -210,51 +275,30 @@ extension Container { }.inObjectScope(.container) // MARK: Chat Transaction Service - self.register(ChatTransactionService.self) { r in + container.register(ChatTransactionService.self) { r in AdamantChatTransactionService( adamantCore: r.resolve(AdamantCore.self)!, accountService: r.resolve(AccountService.self)! ) }.inObjectScope(.container) - // MARK: Chat screen factory - self.register(ChatFactory.self) { r in - ChatFactory( - chatsProvider: r.resolve(ChatsProvider.self)!, - dialogService: r.resolve(DialogService.self)!, - transferProvider: r.resolve(TransfersProvider.self)!, - accountService: r.resolve(AccountService.self)!, - accountProvider: r.resolve(AccountsProvider.self)!, - richTransactionStatusService: r.resolve(RichTransactionStatusService.self)!, - addressBookService: r.resolve(AddressBookService.self)!, - visibleWalletService: r.resolve(VisibleWalletsService.self)!, - avatarService: r.resolve(AvatarService.self)!, - emojiService: r.resolve(EmojiService.self)!, - router: r.resolve(Router.self)! - ) - }.inObjectScope(.container) - - // MARK: Contribute screen factory - self.register(ContributeFactory.self) { r in - ContributeFactory(crashliticsService: r.resolve(CrashlyticsService.self)!) - }.inObjectScope(.container) - // MARK: Rich transaction status service - self.register(RichTransactionStatusService.self) { r in + container.register(TransactionStatusService.self) { r in let accountService = r.resolve(AccountService.self)! let richProviders = accountService.wallets .compactMap { $0 as? RichMessageProviderWithStatusCheck } .map { ($0.dynamicRichMessageType, $0) } - return AdamantRichTransactionStatusService( + return AdamantTransactionStatusService( coreDataStack: r.resolve(CoreDataStack.self)!, - richProviders: Dictionary(uniqueKeysWithValues: richProviders) + richProviders: Dictionary(uniqueKeysWithValues: richProviders), + nodesStorage: r.resolve(NodesStorageProtocol.self)! ) }.inObjectScope(.container) // MARK: Rich transaction reply service - self.register(RichTransactionReplyService.self) { r in + container.register(RichTransactionReplyService.self) { r in AdamantRichTransactionReplyService( coreDataStack: r.resolve(CoreDataStack.self)!, apiService: r.resolve(ApiService.self)!, @@ -264,7 +308,7 @@ extension Container { }.inObjectScope(.container) // MARK: Rich transaction react service - self.register(RichTransactionReactService.self) { r in + container.register(RichTransactionReactService.self) { r in AdamantRichTransactionReactService( coreDataStack: r.resolve(CoreDataStack.self)!, apiService: r.resolve(ApiService.self)!, @@ -274,7 +318,7 @@ extension Container { }.inObjectScope(.container) // MARK: Bitcoin AddressConverterFactory - self.register(AddressConverterFactory.self) { _ in + container.register(AddressConverterFactory.self) { _ in AddressConverterFactory() }.inObjectScope(.container) } diff --git a/Adamant/App/DI/AppContainer.swift b/Adamant/App/DI/AppContainer.swift new file mode 100644 index 000000000..3f8fa7602 --- /dev/null +++ b/Adamant/App/DI/AppContainer.swift @@ -0,0 +1,17 @@ +// +// AppContainer.swift +// Adamant +// +// Created by Andrew G on 09.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Swinject + +struct AppContainer { + let assembler = Assembler([AppAssembly()]) + + func resolve(_ type: T.Type) -> T? { + assembler.resolve(T.self) + } +} diff --git a/Adamant/CoreData/BaseTransaction+TransactionDetails.swift b/Adamant/CoreData/BaseTransaction+TransactionDetails.swift deleted file mode 100644 index 39c299b91..000000000 --- a/Adamant/CoreData/BaseTransaction+TransactionDetails.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// BaseTransaction+TransactionDetails.swift -// Adamant -// -// Created by Anokhov Pavel on 10/11/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation - -extension BaseTransaction: TransactionDetails { - var defaultCurrencySymbol: String? { AdmWalletService.currencySymbol } - - var txId: String { return transactionId } - var senderAddress: String { return senderId ?? "" } - var recipientAddress: String { return recipientId ?? "" } - var dateValue: Date? { return date as Date? } - var feeValue: Decimal? { return fee?.decimalValue } - - var confirmationsValue: String? { return isConfirmed ? String(confirmations) : nil } - var blockValue: String? { return isConfirmed ? blockId : nil } - - var amountValue: Decimal? { - if let amount = self.amount { - return amount.decimalValue - } else { - return 0 - } - } - - var block: UInt { - if let raw = blockId, let id = UInt(raw) { - return id - } else { - return 0 - } - } - - var blockHeight: UInt64? { - return nil - } -} diff --git a/Adamant/Helpers/ApiServiceError+Extension.swift b/Adamant/Helpers/ApiServiceError+Extension.swift new file mode 100644 index 000000000..b07097854 --- /dev/null +++ b/Adamant/Helpers/ApiServiceError+Extension.swift @@ -0,0 +1,22 @@ +// +// ApiServiceError+Extension.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Alamofire + +extension ApiServiceError { + init(error: Error) { + let afError = error as? AFError + + switch afError { + case .explicitlyCancelled: + self = .requestCancelled + default: + self = .networkError(error: error) + } + } +} diff --git a/Adamant/Helpers/Assembler+Extension.swift b/Adamant/Helpers/Assembler+Extension.swift new file mode 100644 index 000000000..8f2b58079 --- /dev/null +++ b/Adamant/Helpers/Assembler+Extension.swift @@ -0,0 +1,15 @@ +// +// Assembler+Extension.swift +// Adamant +// +// Created by Andrew G on 09.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Swinject + +extension Assembler { + func resolve(_ type: T.Type) -> T? { + resolver.resolve(T.self) + } +} diff --git a/Adamant/Helpers/Node+UI.swift b/Adamant/Helpers/Node+UI.swift new file mode 100644 index 000000000..f51c55c62 --- /dev/null +++ b/Adamant/Helpers/Node+UI.swift @@ -0,0 +1,117 @@ +// +// Node+UI.swift +// Adamant +// +// Created by Andrew G on 20.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import UIKit + +extension Node { + func statusString(showVersion: Bool) -> String? { + guard isEnabled else { return Strings.disabled } + + switch connectionStatus { + case .allowed: + return [ + pingString, + showVersion ? versionString : nil, + heightString + ] + .compactMap { $0 } + .joined(separator: " ") + case .synchronizing: + return [ + Strings.synchronizing, + showVersion ? versionString : nil, + heightString + ] + .compactMap { $0 } + .joined(separator: " ") + case .offline: + return Strings.offline + case .none: + return nil + } + } + + func indicatorString(isRest: Bool, isWs: Bool) -> String { + let connections = [ + isRest ? scheme.rawValue : nil, + isWs ? "ws" : nil + ].compactMap { $0 } + + return [ + "●", + connections.isEmpty + ? nil + : connections.joined(separator: ", ") + ] + .compactMap { $0 } + .joined(separator: " ") + } + + var indicatorColor: UIColor { + guard isEnabled else { return .adamant.inactive } + + switch connectionStatus { + case .allowed: + return .adamant.good + case .synchronizing: + return .adamant.alert + case .offline: + return .adamant.danger + case .none: + return .adamant.inactive + } + } +} + +private extension Node { + enum Strings { + static let ping = String.localized( + "NodesList.NodeCell.Ping", + comment: "NodesList.NodeCell: Node ping" + ) + + static let milliseconds = String.localized( + "NodesList.NodeCell.Milliseconds", + comment: "NodesList.NodeCell: Milliseconds" + ) + + static let synchronizing = String.localized( + "NodesList.NodeCell.Synchronizing", + comment: "NodesList.NodeCell: Node is synchronizing" + ) + + static let offline = String.localized( + "NodesList.NodeCell.Offline", + comment: "NodesList.NodeCell: Node is offline" + ) + + static let version = String.localized( + "NodesList.NodeCell.Version", + comment: "NodesList.NodeCell: Node version" + ) + + static let disabled = String.localized( + "NodesList.NodeCell.Disabled", + comment: "NodesList.NodeCell: Node is disabled" + ) + } + + var versionString: String? { + version.map { "(\(Strings.version): \($0))" } + } + + var pingString: String? { + guard let ping = ping else { return nil } + return "\(Strings.ping): \(Int(ping * 1000)) \(Strings.milliseconds)" + } + + var heightString: String? { + height.map { "▱ \($0)" } + } +} diff --git a/Adamant/Helpers/SelfRemovableHostingController.swift b/Adamant/Helpers/SelfRemovableHostingController.swift new file mode 100644 index 000000000..8436a5e2b --- /dev/null +++ b/Adamant/Helpers/SelfRemovableHostingController.swift @@ -0,0 +1,26 @@ +// +// SelfRemovableHostingController.swift +// Adamant +// +// Created by Andrew G on 22.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import SwiftUI + +class SelfRemovableHostingController: UIHostingController { + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(close) + ) + } + + @objc private func close() { + dismiss(animated: true) + } +} + diff --git a/Adamant/Helpers/ServerResponse+Resolver.swift b/Adamant/Helpers/ServerResponse+Resolver.swift new file mode 100644 index 000000000..fef3485a3 --- /dev/null +++ b/Adamant/Helpers/ServerResponse+Resolver.swift @@ -0,0 +1,60 @@ +// +// ServerModelResponse+Mapper.swift +// Adamant +// +// Created by Andrew G on 01.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit + +extension ServerModelResponse { + func resolved() -> ApiServiceResult { + if let model = model { + return .success(model) + } else { + return .failure(translateServerError(error)) + } + } +} + +extension ServerCollectionResponse { + func resolved() -> ApiServiceResult<[T]> { + if let collection = collection { + return .success(collection) + } else { + return .failure(translateServerError(error)) + } + } +} + +extension TransactionIdResponse { + func resolved() -> ApiServiceResult { + if let ransactionId = transactionId { + return .success(ransactionId) + } else { + return .failure(translateServerError(error)) + } + } +} + +extension GetPublicKeyResponse { + func resolved() -> ApiServiceResult { + if let publicKey = publicKey { + return .success(publicKey) + } else { + return .failure(translateServerError(error)) + } + } +} + +private func translateServerError(_ error: String?) -> ApiServiceError { + guard let error = error else { return .internalError(error: InternalAPIError.unknownError) } + + switch error { + case "Account not found": + return .accountNotFound + default: + return .serverError(error: error) + } +} diff --git a/Adamant/Helpers/String+adamant.swift b/Adamant/Helpers/String+adamant.swift index 836696c38..a69173cc3 100644 --- a/Adamant/Helpers/String+adamant.swift +++ b/Adamant/Helpers/String+adamant.swift @@ -60,7 +60,20 @@ extension String { } } } - + case .addressLegacy(address: let addr, params: let params): + address = addr + if let params = params { + for param in params { + switch param { + case .address: + break + case .label(let label): + name = label + case .message(let urlMessage): + message = urlMessage + } + } + } case .passphrase: address = nil } diff --git a/Adamant/Helpers/String+localized.swift b/Adamant/Helpers/String+localized.swift index a69593a1b..d3b940264 100644 --- a/Adamant/Helpers/String+localized.swift +++ b/Adamant/Helpers/String+localized.swift @@ -90,4 +90,9 @@ extension String.adamant { static let failedMessageError = String.localized("Reply.failedMessageError", comment: "Failed message reply error") static let pendingMessageError = String.localized("Reply.pendingMessageError", comment: "Pending message reply error") } + + enum partnerQR { + static let includePartnerName = String.localized("PartnerQR.includePartnerName", comment: "Include partner name") + static let includePartnerURL = String.localized("PartnerQR.includePartnerURL", comment: "Include partner url") + } } diff --git a/Adamant/Helpers/UITextField+adamant.swift b/Adamant/Helpers/UITextField+adamant.swift index daa3dd685..ec4846f7a 100644 --- a/Adamant/Helpers/UITextField+adamant.swift +++ b/Adamant/Helpers/UITextField+adamant.swift @@ -137,3 +137,33 @@ extension UITextField { self.defaultTextAttributes[.paragraphStyle] = style } } + +extension UITextField { + func enablePasswordToggle() { + let button = UIButton(type: .custom) + updatePasswordToggleImage(button) + button.addTarget(self, action: #selector(togglePasswordView(_:)), for: .touchUpInside) + + let contanerView = UIView() + contanerView.addSubview(button) + button.snp.makeConstraints { make in + make.directionalEdges.equalToSuperview().inset(3) + } + contanerView.snp.makeConstraints { make in + make.size.equalTo(28) + } + + rightView = contanerView + rightViewMode = .always + } + + private func updatePasswordToggleImage(_ button: UIButton) { + let imageName = isSecureTextEntry ? "eye_close" : "eye_open" + button.setImage(.asset(named: imageName), for: .normal) + } + + @objc private func togglePasswordView(_ sender: UIButton) { + isSecureTextEntry.toggle() + updatePasswordToggleImage(sender) + } +} diff --git a/Adamant/Models/APIParametersEncoding.swift b/Adamant/Models/APIParametersEncoding.swift new file mode 100644 index 000000000..545507ebd --- /dev/null +++ b/Adamant/Models/APIParametersEncoding.swift @@ -0,0 +1,30 @@ +// +// APIParametersEncoding.swift +// Adamant +// +// Created by Andrew G on 30.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Alamofire +import Foundation + +enum APIParametersEncoding { + case url + case json + case bodyString + case forceQueryItems([URLQueryItem]) + + var parametersEncoding: ParameterEncoding { + switch self { + case .url: + return URLEncoding.default + case .json: + return JSONEncoding.default + case .bodyString: + return BodyStringEncoding() + case let .forceQueryItems(items): + return ForceQueryItemsEncoding(queryItems: items) + } + } +} diff --git a/Adamant/Models/APIResponseModel.swift b/Adamant/Models/APIResponseModel.swift new file mode 100644 index 000000000..2fe7c288b --- /dev/null +++ b/Adamant/Models/APIResponseModel.swift @@ -0,0 +1,15 @@ +// +// APIResponseModel.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct APIResponseModel { + let result: ApiServiceResult + let data: Data? + let code: Int? +} diff --git a/Adamant/Models/AdamantVibroType.swift b/Adamant/Models/AdamantVibroType.swift new file mode 100644 index 000000000..0fcb8e64e --- /dev/null +++ b/Adamant/Models/AdamantVibroType.swift @@ -0,0 +1,21 @@ +// +// AdamantVibroType.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 07.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +enum AdamantVibroType: CaseIterable { + case light + case rigid + case heavy + case medium + case soft + case selection + case success + case warning + case error +} diff --git a/Adamant/Models/ApiServiceError.swift b/Adamant/Models/ApiServiceError.swift index b3ff326c7..8abc11831 100644 --- a/Adamant/Models/ApiServiceError.swift +++ b/Adamant/Models/ApiServiceError.swift @@ -17,6 +17,7 @@ enum ApiServiceError: LocalizedError, Error { case networkError(error: Error) case requestCancelled case commonError(message: String) + case noEndpointsAvailable(coin: String) var errorDescription: String? { switch self { @@ -41,8 +42,22 @@ enum ApiServiceError: LocalizedError, Error { case let .commonError(message): return String.adamant.sharedErrors.commonError(message) + + case let .noEndpointsAvailable(coin): + return + .localizedStringWithFormat( + .localized( + "ApiService.InternalError.NoNodesAvailable", + comment: "Serious internal error: No nodes available" + ), + coin + ).localized } } + + static func internalError(error: InternalAPIError) -> Self { + .internalError(message: error.localizedDescription, error: error) + } } extension ApiServiceError: RichError { @@ -52,7 +67,7 @@ extension ApiServiceError: RichError { var level: ErrorLevel { switch self { - case .accountNotFound, .notLogged, .networkError, .requestCancelled: + case .accountNotFound, .notLogged, .networkError, .requestCancelled, .noEndpointsAvailable: return .warning case .serverError, .commonError: @@ -65,7 +80,7 @@ extension ApiServiceError: RichError { var internalError: Error? { switch self { - case .accountNotFound, .notLogged, .serverError, .requestCancelled, .commonError: + case .accountNotFound, .notLogged, .serverError, .requestCancelled, .commonError, .noEndpointsAvailable: return nil case .internalError(_, let error): @@ -100,3 +115,27 @@ extension ApiServiceError: Equatable { } } } + +extension ApiServiceError: HealthCheckableError { + var isNetworkError: Bool { + switch self { + case .networkError: + return true + default: + return false + } + } + + var isRequestCancelledError: Bool { + switch self { + case .requestCancelled: + return true + default: + return false + } + } + + static func noEndpointsError(coin: String) -> ApiServiceError { + .noEndpointsAvailable(coin: coin) + } +} diff --git a/Adamant/Models/ApiServiceResult.swift b/Adamant/Models/ApiServiceResult.swift index 3d283c696..71c88ccd3 100644 --- a/Adamant/Models/ApiServiceResult.swift +++ b/Adamant/Models/ApiServiceResult.swift @@ -6,7 +6,4 @@ // Copyright © 2022 Adamant. All rights reserved. // -enum ApiServiceResult { - case success(T) - case failure(ApiServiceError) -} +typealias ApiServiceResult = Result diff --git a/Adamant/Models/BTCRawTransaction.swift b/Adamant/Models/BTCRawTransaction.swift index 7c1824e23..22fa94df3 100644 --- a/Adamant/Models/BTCRawTransaction.swift +++ b/Adamant/Models/BTCRawTransaction.swift @@ -35,7 +35,7 @@ struct BTCRawTransaction { transactionStatus = confirmations > 0 ? .success : .pending } else { confirmationsValue = nil - transactionStatus = .pending + transactionStatus = .notInitiated } // Transfers diff --git a/Adamant/Models/BodyStringEncoding.swift b/Adamant/Models/BodyStringEncoding.swift new file mode 100644 index 000000000..1b7242d61 --- /dev/null +++ b/Adamant/Models/BodyStringEncoding.swift @@ -0,0 +1,37 @@ +// +// BodyStringEncoding.swift +// Adamant +// +// Created by Andrew G on 30.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Alamofire +import Foundation + +struct BodyStringEncoding: ParameterEncoding { + func encode( + _ urlRequest: URLRequestConvertible, + with parameters: Parameters? + ) throws -> URLRequest { + var urlRequest = try urlRequest.asURLRequest() + + guard + let string = parameters?.first?.value as? String, + let data = string.data(using: .utf8) + else { + throw AFError.parameterEncodingFailed( + reason: .customEncodingFailed(error: AdamantError( + message: "String encoding problem" + )) + ) + } + + if parameters?.count != 1 { + assertionFailure("BodyStringEncoding uses just first parameter for encoding") + } + + urlRequest.httpBody = data + return urlRequest + } +} diff --git a/Adamant/CoreData/AdamantResources+CoreData.swift b/Adamant/Models/CoreData/AdamantResources+CoreData.swift similarity index 100% rename from Adamant/CoreData/AdamantResources+CoreData.swift rename to Adamant/Models/CoreData/AdamantResources+CoreData.swift diff --git a/Adamant/CoreData/BaseAccount+CoreDataClass.swift b/Adamant/Models/CoreData/BaseAccount+CoreDataClass.swift similarity index 100% rename from Adamant/CoreData/BaseAccount+CoreDataClass.swift rename to Adamant/Models/CoreData/BaseAccount+CoreDataClass.swift diff --git a/Adamant/CoreData/BaseAccount+CoreDataProperties.swift b/Adamant/Models/CoreData/BaseAccount+CoreDataProperties.swift similarity index 100% rename from Adamant/CoreData/BaseAccount+CoreDataProperties.swift rename to Adamant/Models/CoreData/BaseAccount+CoreDataProperties.swift diff --git a/Adamant/CoreData/BaseTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/BaseTransaction+CoreDataClass.swift similarity index 64% rename from Adamant/CoreData/BaseTransaction+CoreDataClass.swift rename to Adamant/Models/CoreData/BaseTransaction+CoreDataClass.swift index 084cd19fa..c99b70b3e 100644 --- a/Adamant/CoreData/BaseTransaction+CoreDataClass.swift +++ b/Adamant/Models/CoreData/BaseTransaction+CoreDataClass.swift @@ -11,8 +11,5 @@ import Foundation import CoreData @objc(BaseTransaction) -public class BaseTransaction: NSManagedObject { - var transactionStatus: TransactionStatus? { - return nil - } +public class BaseTransaction: CoinTransaction { } diff --git a/Adamant/Models/CoreData/BaseTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/BaseTransaction+CoreDataProperties.swift new file mode 100644 index 000000000..c2c2344b9 --- /dev/null +++ b/Adamant/Models/CoreData/BaseTransaction+CoreDataProperties.swift @@ -0,0 +1,23 @@ +// +// BaseTransaction+CoreDataProperties.swift +// Adamant +// +// Created by Anokhov Pavel on 02/02/2019. +// Copyright © 2019 Adamant. All rights reserved. +// +// + +import Foundation +import CoreData + +extension BaseTransaction { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "BaseTransaction") + } + + @NSManaged public var type: Int16 + @NSManaged public var partner: BaseAccount? + @NSManaged public var senderPublicKey: String? + +} diff --git a/Adamant/Models/CoreData/BaseTransaction+TransactionDetails.swift b/Adamant/Models/CoreData/BaseTransaction+TransactionDetails.swift new file mode 100644 index 000000000..e09933404 --- /dev/null +++ b/Adamant/Models/CoreData/BaseTransaction+TransactionDetails.swift @@ -0,0 +1,19 @@ +// +// BaseTransaction+TransactionDetails.swift +// Adamant +// +// Created by Anokhov Pavel on 10/11/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +extension BaseTransaction { + var block: UInt { + if let raw = blockId, let id = UInt(raw) { + return id + } else { + return 0 + } + } +} diff --git a/Adamant/CoreData/ChatTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/ChatTransaction+CoreDataClass.swift similarity index 85% rename from Adamant/CoreData/ChatTransaction+CoreDataClass.swift rename to Adamant/Models/CoreData/ChatTransaction+CoreDataClass.swift index c52f4a125..75780eeb0 100644 --- a/Adamant/CoreData/ChatTransaction+CoreDataClass.swift +++ b/Adamant/Models/CoreData/ChatTransaction+CoreDataClass.swift @@ -26,8 +26,11 @@ public class ChatTransaction: BaseTransaction { } override var transactionStatus: TransactionStatus? { - return confirmations > 0 - ? .success - : .pending + get { + return confirmations > 0 + ? .success + : .pending + } + set { } } } diff --git a/Adamant/CoreData/ChatTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/ChatTransaction+CoreDataProperties.swift similarity index 95% rename from Adamant/CoreData/ChatTransaction+CoreDataProperties.swift rename to Adamant/Models/CoreData/ChatTransaction+CoreDataProperties.swift index d5da8484a..accc3697f 100644 --- a/Adamant/CoreData/ChatTransaction+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/ChatTransaction+CoreDataProperties.swift @@ -24,5 +24,6 @@ extension ChatTransaction { @NSManaged public var status: Int16 @NSManaged public var chatroom: Chatroom? @NSManaged public var lastIn: Chatroom? + @NSManaged public var isFake: Bool } diff --git a/Adamant/CoreData/Chatroom+CoreDataClass.swift b/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift similarity index 100% rename from Adamant/CoreData/Chatroom+CoreDataClass.swift rename to Adamant/Models/CoreData/Chatroom+CoreDataClass.swift diff --git a/Adamant/CoreData/Chatroom+CoreDataProperties.swift b/Adamant/Models/CoreData/Chatroom+CoreDataProperties.swift similarity index 100% rename from Adamant/CoreData/Chatroom+CoreDataProperties.swift rename to Adamant/Models/CoreData/Chatroom+CoreDataProperties.swift diff --git a/Adamant/Models/CoreData/CoinTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/CoinTransaction+CoreDataClass.swift new file mode 100644 index 000000000..17785df15 --- /dev/null +++ b/Adamant/Models/CoreData/CoinTransaction+CoreDataClass.swift @@ -0,0 +1,27 @@ +// +// CoinTransaction+CoreDataClass.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 26.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// +// + +import Foundation +import CoreData + +@objc(CoinTransaction) +public class CoinTransaction: NSManagedObject { + static let entityCoinName = "CoinTransaction" + + var transactionStatus: TransactionStatus? { + get { + TransactionStatus(rawValue: transactionStatusRaw) + } + set { + let raw = newValue?.rawValue ?? .zero + guard raw != transactionStatusRaw else { return } + transactionStatusRaw = newValue?.rawValue ?? .zero + } + } +} diff --git a/Adamant/CoreData/BaseTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/CoinTransaction+CoreDataProperties.swift similarity index 58% rename from Adamant/CoreData/BaseTransaction+CoreDataProperties.swift rename to Adamant/Models/CoreData/CoinTransaction+CoreDataProperties.swift index bbb300c29..daa1fd6b2 100644 --- a/Adamant/CoreData/BaseTransaction+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/CoinTransaction+CoreDataProperties.swift @@ -1,34 +1,37 @@ // -// BaseTransaction+CoreDataProperties.swift +// CoinTransaction+CoreDataProperties.swift // Adamant // -// Created by Anokhov Pavel on 02/02/2019. -// Copyright © 2019 Adamant. All rights reserved. +// Created by Stanislav Jelezoglo on 26.09.2023. +// Copyright © 2023 Adamant. All rights reserved. // // import Foundation import CoreData -extension BaseTransaction { +extension CoinTransaction { - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "BaseTransaction") + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "CoinTransaction") } @NSManaged public var amount: NSDecimalNumber? - @NSManaged public var blockId: String? - @NSManaged public var confirmations: Int64 + @NSManaged public var transactionId: String + @NSManaged public var coinId: String? + @NSManaged public var senderId: String? + @NSManaged public var recipientId: String? @NSManaged public var date: NSDate? + @NSManaged public var isOutgoing: Bool + @NSManaged public var confirmations: Int64 @NSManaged public var fee: NSDecimalNumber? + @NSManaged public var blockId: String? @NSManaged public var height: Int64 @NSManaged public var isConfirmed: Bool - @NSManaged public var isOutgoing: Bool - @NSManaged public var recipientId: String? - @NSManaged public var senderId: String? - @NSManaged public var transactionId: String - @NSManaged public var type: Int16 - @NSManaged public var partner: BaseAccount? - @NSManaged public var senderPublicKey: String? + @NSManaged public var blockchainType: String + @NSManaged public var transactionStatusRaw: Int16 +} + +extension CoinTransaction : Identifiable { } diff --git a/Adamant/Models/CoreData/CoinTransaction+TransactionDetails.swift b/Adamant/Models/CoreData/CoinTransaction+TransactionDetails.swift new file mode 100644 index 000000000..7ae29eccd --- /dev/null +++ b/Adamant/Models/CoreData/CoinTransaction+TransactionDetails.swift @@ -0,0 +1,41 @@ +// +// CoinTransaction+TransactionDetails.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 04.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +extension CoinTransaction: TransactionDetails { + var defaultCurrencySymbol: String? { AdmWalletService.currencySymbol } + + var senderAddress: String { + senderId ?? "" + } + + var recipientAddress: String { + recipientId ?? "" + } + + var dateValue: Date? { + date as? Date + } + + var amountValue: Decimal? { + amount?.decimalValue + } + + var feeValue: Decimal? { fee?.decimalValue } + + var confirmationsValue: String? { return isConfirmed ? String(confirmations) : nil } + + var blockValue: String? { return isConfirmed ? blockId : nil } + + var txId: String { return transactionId } + + var blockHeight: UInt64? { + return nil + } +} diff --git a/Adamant/CoreData/CoreDataAccount+CoreDataClass.swift b/Adamant/Models/CoreData/CoreDataAccount+CoreDataClass.swift similarity index 100% rename from Adamant/CoreData/CoreDataAccount+CoreDataClass.swift rename to Adamant/Models/CoreData/CoreDataAccount+CoreDataClass.swift diff --git a/Adamant/CoreData/CoreDataAccount+CoreDataProperties.swift b/Adamant/Models/CoreData/CoreDataAccount+CoreDataProperties.swift similarity index 100% rename from Adamant/CoreData/CoreDataAccount+CoreDataProperties.swift rename to Adamant/Models/CoreData/CoreDataAccount+CoreDataProperties.swift diff --git a/Adamant/CoreData/DummyAccount+CoreDataClass.swift b/Adamant/Models/CoreData/DummyAccount+CoreDataClass.swift similarity index 100% rename from Adamant/CoreData/DummyAccount+CoreDataClass.swift rename to Adamant/Models/CoreData/DummyAccount+CoreDataClass.swift diff --git a/Adamant/CoreData/DummyAccount+CoreDataProperties.swift b/Adamant/Models/CoreData/DummyAccount+CoreDataProperties.swift similarity index 100% rename from Adamant/CoreData/DummyAccount+CoreDataProperties.swift rename to Adamant/Models/CoreData/DummyAccount+CoreDataProperties.swift diff --git a/Adamant/CoreData/MessageTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/MessageTransaction+CoreDataClass.swift similarity index 100% rename from Adamant/CoreData/MessageTransaction+CoreDataClass.swift rename to Adamant/Models/CoreData/MessageTransaction+CoreDataClass.swift diff --git a/Adamant/CoreData/MessageTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/MessageTransaction+CoreDataProperties.swift similarity index 100% rename from Adamant/CoreData/MessageTransaction+CoreDataProperties.swift rename to Adamant/Models/CoreData/MessageTransaction+CoreDataProperties.swift diff --git a/Adamant/CoreData/RichMessageTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/RichMessageTransaction+CoreDataClass.swift similarity index 67% rename from Adamant/CoreData/RichMessageTransaction+CoreDataClass.swift rename to Adamant/Models/CoreData/RichMessageTransaction+CoreDataClass.swift index 106987796..63d623a5a 100644 --- a/Adamant/CoreData/RichMessageTransaction+CoreDataClass.swift +++ b/Adamant/Models/CoreData/RichMessageTransaction+CoreDataClass.swift @@ -21,18 +21,12 @@ public class RichMessageTransaction: ChatTransaction { override var transactionStatus: TransactionStatus? { get { - if let raw = transferStatusRaw { - return TransactionStatus(rawValue: raw.int16Value) - } else { - return nil - } + TransactionStatus(rawValue: transactionStatusRaw) } set { - if let raw = newValue { - transferStatusRaw = raw.rawValue as NSNumber - } else { - transferStatusRaw = nil - } + let raw = newValue?.rawValue ?? .zero + guard raw != transactionStatusRaw else { return } + transactionStatusRaw = newValue?.rawValue ?? .zero } } diff --git a/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/RichMessageTransaction+CoreDataProperties.swift similarity index 100% rename from Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift rename to Adamant/Models/CoreData/RichMessageTransaction+CoreDataProperties.swift diff --git a/Adamant/CoreData/TransferTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/TransferTransaction+CoreDataClass.swift similarity index 100% rename from Adamant/CoreData/TransferTransaction+CoreDataClass.swift rename to Adamant/Models/CoreData/TransferTransaction+CoreDataClass.swift diff --git a/Adamant/CoreData/TransferTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/TransferTransaction+CoreDataProperties.swift similarity index 68% rename from Adamant/CoreData/TransferTransaction+CoreDataProperties.swift rename to Adamant/Models/CoreData/TransferTransaction+CoreDataProperties.swift index 5cc72637b..bbc9cfc2f 100644 --- a/Adamant/CoreData/TransferTransaction+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/TransferTransaction+CoreDataProperties.swift @@ -41,3 +41,25 @@ extension TransferTransaction { } } } + +extension TransferTransaction: AdamantTransactionDetails { + var partnerName: String? { + partner?.name + } + + var showToChat: Bool? { + guard let partner = partner as? CoreDataAccount, + let chatroom = partner.chatroom, + !chatroom.isReadonly + else { + return false + } + + return true + } + + var chatRoom: Chatroom? { + let partner = partner as? CoreDataAccount + return partner?.chatroom + } +} diff --git a/Adamant/Models/DashTransaction.swift b/Adamant/Models/DashTransaction.swift index b06362f3d..d0ec40287 100644 --- a/Adamant/Models/DashTransaction.swift +++ b/Adamant/Models/DashTransaction.swift @@ -9,7 +9,7 @@ import Foundation import BitcoinKit -class DashTransaction: BaseBtcTransaction { +final class DashTransaction: BaseBtcTransaction { override var defaultCurrencySymbol: String? { DashWalletService.currencySymbol } } diff --git a/Adamant/Models/Delegate.swift b/Adamant/Models/Delegate.swift index be2241248..4c1869d7d 100644 --- a/Adamant/Models/Delegate.swift +++ b/Adamant/Models/Delegate.swift @@ -9,7 +9,7 @@ import Foundation import CommonKit -class Delegate: Decodable { +final class Delegate: Decodable { let username: String let address: String let publicKey: String diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index 753e7242b..dbe88fbe6 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -23,7 +23,7 @@ extension String.adamant { } } -class DogeTransaction: BaseBtcTransaction { +final class DogeTransaction: BaseBtcTransaction { override var defaultCurrencySymbol: String? { DogeWalletService.currencySymbol } } diff --git a/Adamant/Models/ForceQueryItemsEncoding.swift b/Adamant/Models/ForceQueryItemsEncoding.swift new file mode 100644 index 000000000..44ae8296b --- /dev/null +++ b/Adamant/Models/ForceQueryItemsEncoding.swift @@ -0,0 +1,32 @@ +// +// ForceQueryItemsEncoding.swift +// Adamant +// +// Created by Andrew G on 18.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Alamofire +import Foundation + +struct ForceQueryItemsEncoding: ParameterEncoding { + let queryItems: [URLQueryItem] + + func encode( + _ urlRequest: URLRequestConvertible, + with parameters: Parameters? + ) throws -> URLRequest { + var urlRequest = try urlRequest.asURLRequest() + + guard + let url = urlRequest.url, + var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) + else { + throw AFError.parameterEncodingFailed(reason: .missingURL) + } + + urlComponents.queryItems = queryItems + urlRequest.url = urlComponents.url + return urlRequest + } +} diff --git a/Adamant/Models/InternalAPIError.swift b/Adamant/Models/InternalAPIError.swift new file mode 100644 index 000000000..c28ad55af --- /dev/null +++ b/Adamant/Models/InternalAPIError.swift @@ -0,0 +1,43 @@ +// +// InternalAPIError.swift +// Adamant +// +// Created by Andrew G on 30.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +enum InternalAPIError: LocalizedError { + case endpointBuildFailed + case signTransactionFailed + case parsingFailed + case unknownError + + func apiServiceErrorWith(error: Error) -> ApiServiceError { + .internalError(message: localizedDescription, error: error) + } + + var errorDescription: String? { + switch self { + case .endpointBuildFailed: + return .localized( + "ApiService.InternalError.EndpointBuildFailed", + comment: "Serious internal error: Failed to build endpoint url" + ) + case .signTransactionFailed: + return .localized( + "ApiService.InternalError.FailedTransactionSigning", + comment: "Serious internal error: Failed to sign transaction" + ) + case .parsingFailed: + return .localized( + "ApiService.InternalError.ParsingFailed", + comment: "Serious internal error: Error parsing response" + ) + case .unknownError: + return .adamant.sharedErrors.unknownError + } + } +} diff --git a/Adamant/Models/NodeStatusInfo.swift b/Adamant/Models/NodeStatusInfo.swift new file mode 100644 index 000000000..0046b8af4 --- /dev/null +++ b/Adamant/Models/NodeStatusInfo.swift @@ -0,0 +1,17 @@ +// +// NodeStatusInfo.swift +// Adamant +// +// Created by Andrew G on 01.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct NodeStatusInfo: Equatable { + let ping: TimeInterval + let height: Int + let wsEnabled: Bool + let wsPort: Int? + let version: String? +} diff --git a/Adamant/Models/NodeWithGroup.swift b/Adamant/Models/NodeWithGroup.swift new file mode 100644 index 000000000..a5f57100a --- /dev/null +++ b/Adamant/Models/NodeWithGroup.swift @@ -0,0 +1,36 @@ +// +// NodeWithGroup.swift +// Adamant +// +// Created by Andrew G on 20.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit + +struct NodeWithGroup: Codable, Equatable { + let group: NodeGroup + var node: Node +} + +extension NodeGroup { + var name: String { + switch self { + case .btc: + return BtcWalletService.tokenNetworkSymbol + case .eth: + return EthWalletService.tokenNetworkSymbol + case .lskNode: + return LskWalletService.tokenNetworkSymbol + case .lskService: + return LskWalletService.tokenNetworkSymbol + + " " + .adamant.coinsNodesList.serviceNode + case .doge: + return DogeWalletService.tokenNetworkSymbol + case .dash: + return DashWalletService.tokenNetworkSymbol + case .adm: + return AdmWalletService.tokenNetworkSymbol + } + } +} diff --git a/Adamant/ServerResponses/BTCRPCServerResponce.swift b/Adamant/Models/ServerResponses/BTCRPCServerResponce.swift similarity index 74% rename from Adamant/ServerResponses/BTCRPCServerResponce.swift rename to Adamant/Models/ServerResponses/BTCRPCServerResponce.swift index c4d3d2d6f..cc251bbe5 100644 --- a/Adamant/ServerResponses/BTCRPCServerResponce.swift +++ b/Adamant/Models/ServerResponses/BTCRPCServerResponce.swift @@ -8,13 +8,13 @@ import Foundation -class BTCRPCServerResponce: Decodable { +final class BTCRPCServerResponce: Decodable { let result: T? let error: BTCRPCError? let id: String? } -class BTCRPCError: Decodable { +final class BTCRPCError: Decodable { let code: Int let message: String } diff --git a/Adamant/ServerResponses/DogeGetTransactionsResponse.swift b/Adamant/Models/ServerResponses/DogeGetTransactionsResponse.swift similarity index 87% rename from Adamant/ServerResponses/DogeGetTransactionsResponse.swift rename to Adamant/Models/ServerResponses/DogeGetTransactionsResponse.swift index 6ed3a6109..460e3cf25 100644 --- a/Adamant/ServerResponses/DogeGetTransactionsResponse.swift +++ b/Adamant/Models/ServerResponses/DogeGetTransactionsResponse.swift @@ -8,7 +8,7 @@ import Foundation -class DogeGetTransactionsResponse: Decodable { +final class DogeGetTransactionsResponse: Decodable { let totalItems: Int let from: Int let to: Int diff --git a/Adamant/ServerResponses/GetPublicKeyResponse.swift b/Adamant/Models/ServerResponses/GetPublicKeyResponse.swift similarity index 100% rename from Adamant/ServerResponses/GetPublicKeyResponse.swift rename to Adamant/Models/ServerResponses/GetPublicKeyResponse.swift diff --git a/Adamant/ServerResponses/ServerResponseWithTimestamp.swift b/Adamant/Models/ServerResponses/ServerResponseWithTimestamp.swift similarity index 100% rename from Adamant/ServerResponses/ServerResponseWithTimestamp.swift rename to Adamant/Models/ServerResponses/ServerResponseWithTimestamp.swift diff --git a/Adamant/ServerResponses/TransactionIdResponse.swift b/Adamant/Models/ServerResponses/TransactionIdResponse.swift similarity index 100% rename from Adamant/ServerResponses/TransactionIdResponse.swift rename to Adamant/Models/ServerResponses/TransactionIdResponse.swift diff --git a/Adamant/Models/SimpleTransactionDetails.swift b/Adamant/Models/SimpleTransactionDetails.swift index c1ba0d997..7ab85b879 100644 --- a/Adamant/Models/SimpleTransactionDetails.swift +++ b/Adamant/Models/SimpleTransactionDetails.swift @@ -8,8 +8,8 @@ import Foundation -struct SimpleTransactionDetails: TransactionDetails { - var defaultCurrencySymbol: String? { nil } +struct SimpleTransactionDetails: AdamantTransactionDetails, Hashable { + var defaultCurrencySymbol: String? var txId: String @@ -34,4 +34,70 @@ struct SimpleTransactionDetails: TransactionDetails { var blockHeight: UInt64? { return nil } + + var partnerName: String? + var comment: String? + var showToChat: Bool? + var chatRoom: Chatroom? + + init( + defaultCurrencySymbol: String? = nil, + txId: String, + senderAddress: String, + recipientAddress: String, + dateValue: Date? = nil, + amountValue: Decimal? = nil, + feeValue: Decimal? = nil, + confirmationsValue: String? = nil, + blockValue: String? = nil, + isOutgoing: Bool, + transactionStatus: TransactionStatus? = nil, + partnerName: String? = nil + ) { + self.defaultCurrencySymbol = defaultCurrencySymbol + self.txId = txId + self.senderAddress = senderAddress + self.recipientAddress = recipientAddress + self.dateValue = dateValue + self.amountValue = amountValue + self.feeValue = feeValue + self.confirmationsValue = confirmationsValue + self.blockValue = blockValue + self.isOutgoing = isOutgoing + self.transactionStatus = transactionStatus + self.partnerName = partnerName + } + + init(_ transaction: TransactionDetails) { + self.defaultCurrencySymbol = transaction.defaultCurrencySymbol + self.txId = transaction.txId + self.senderAddress = transaction.senderAddress + self.recipientAddress = transaction.recipientAddress + self.dateValue = transaction.dateValue + self.amountValue = transaction.amountValue + self.feeValue = transaction.feeValue + self.confirmationsValue = transaction.confirmationsValue + self.blockValue = transaction.blockValue + self.isOutgoing = transaction.isOutgoing + self.transactionStatus = transaction.transactionStatus + } + + init(_ transaction: TransferTransaction) { + self.defaultCurrencySymbol = transaction.defaultCurrencySymbol + self.txId = transaction.txId + self.senderAddress = transaction.senderAddress + self.recipientAddress = transaction.recipientAddress + self.dateValue = transaction.dateValue + self.amountValue = transaction.amountValue + self.feeValue = transaction.feeValue + self.confirmationsValue = transaction.confirmationsValue + self.blockValue = transaction.blockValue + self.isOutgoing = transaction.isOutgoing + self.transactionStatus = transaction.transactionStatus + self.showToChat = transaction.showToChat + self.chatRoom = transaction.chatRoom + self.partnerName = transaction.partnerName + self.comment = transaction.comment + self.transactionStatus = transaction.transactionStatus + } } diff --git a/Adamant/Models/TransactionStatus.swift b/Adamant/Models/TransactionStatus.swift index 6a1ebeba9..a97a13dae 100644 --- a/Adamant/Models/TransactionStatus.swift +++ b/Adamant/Models/TransactionStatus.swift @@ -22,7 +22,7 @@ enum TransactionStatus: Int16 { var localized: String { switch self { case .notInitiated: - return .localized("TransactionStatus.Updating", comment: "Transaction status: updating in progress") + return "⏱" case .pending, .registered: return .localized("TransactionStatus.Pending", comment: "Transaction status: transaction is pending") case .success: diff --git a/Adamant/Models/UnregisteredTransaction.swift b/Adamant/Models/UnregisteredTransaction.swift index 593a0b084..e23543153 100644 --- a/Adamant/Models/UnregisteredTransaction.swift +++ b/Adamant/Models/UnregisteredTransaction.swift @@ -8,6 +8,7 @@ import Foundation import CommonKit +import BigInt struct UnregisteredTransaction: Hashable { let type: TransactionType @@ -18,6 +19,7 @@ struct UnregisteredTransaction: Hashable { let amount: Decimal let signature: String let asset: TransactionAsset + let requesterPublicKey: String? } extension UnregisteredTransaction: Codable { @@ -45,6 +47,7 @@ extension UnregisteredTransaction: Codable { let amount = try container.decode(Decimal.self, forKey: .amount) self.amount = amount.shiftedFromAdamant() + self.requesterPublicKey = "" } func encode(to encoder: Encoder) throws { diff --git a/Adamant/Modules/Account/AccountFactory.swift b/Adamant/Modules/Account/AccountFactory.swift new file mode 100644 index 000000000..dbc7a18a1 --- /dev/null +++ b/Adamant/Modules/Account/AccountFactory.swift @@ -0,0 +1,28 @@ +// +// AccountFactory.swift +// Adamant +// +// Created by Anokhov Pavel on 07.01.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Swinject +import UIKit + +struct AccountFactory { + let assembler: Assembler + + func makeViewController(screensFactory: ScreensFactory) -> UIViewController { + let c = AccountViewController() + c.accountService = assembler.resolve(AccountService.self) + c.dialogService = assembler.resolve(DialogService.self) + c.notificationsService = assembler.resolve(NotificationsService.self) + c.transfersProvider = assembler.resolve(TransfersProvider.self) + c.localAuth = assembler.resolve(LocalAuthentication.self) + c.avatarService = assembler.resolve(AvatarService.self) + c.currencyInfoService = assembler.resolve(CurrencyInfoService.self) + c.visibleWalletsService = assembler.resolve(VisibleWalletsService.self) + c.screensFactory = screensFactory + return c + } +} diff --git a/Adamant/Stories/Account/AccountFooter.xib b/Adamant/Modules/Account/AccountFooter.xib similarity index 100% rename from Adamant/Stories/Account/AccountFooter.xib rename to Adamant/Modules/Account/AccountFooter.xib diff --git a/Adamant/Stories/Account/AccountHeader.xib b/Adamant/Modules/Account/AccountHeader.xib similarity index 100% rename from Adamant/Stories/Account/AccountHeader.xib rename to Adamant/Modules/Account/AccountHeader.xib diff --git a/Adamant/Stories/Account/AccountHeaderView.swift b/Adamant/Modules/Account/AccountHeaderView.swift similarity index 93% rename from Adamant/Stories/Account/AccountHeaderView.swift rename to Adamant/Modules/Account/AccountHeaderView.swift index d8e0b166e..f1b4b3d35 100644 --- a/Adamant/Stories/Account/AccountHeaderView.swift +++ b/Adamant/Modules/Account/AccountHeaderView.swift @@ -12,7 +12,7 @@ protocol AccountHeaderViewDelegate: AnyObject { func addressLabelTapped(from: UIView) } -class AccountHeaderView: UIView { +final class AccountHeaderView: UIView { // MARK: - IBOutlets @IBOutlet weak var avatarImageView: UIImageView! diff --git a/Adamant/Stories/Account/AccountViewController+StayIn.swift b/Adamant/Modules/Account/AccountViewController+StayIn.swift similarity index 100% rename from Adamant/Stories/Account/AccountViewController+StayIn.swift rename to Adamant/Modules/Account/AccountViewController+StayIn.swift diff --git a/Adamant/Stories/Account/AccountViewController.swift b/Adamant/Modules/Account/AccountViewController.swift similarity index 83% rename from Adamant/Stories/Account/AccountViewController.swift rename to Adamant/Modules/Account/AccountViewController.swift index f26a820a1..30f23240d 100644 --- a/Adamant/Stories/Account/AccountViewController.swift +++ b/Adamant/Modules/Account/AccountViewController.swift @@ -13,6 +13,7 @@ import CoreData import Parchment import SnapKit import CommonKit +import Combine // MARK: - Localization extension String.adamant { @@ -31,7 +32,7 @@ extension String.adamant.alert { } // MARK: AccountViewController -class AccountViewController: FormViewController { +final class AccountViewController: FormViewController { // MARK: - Rows & Sections enum Sections { case wallet, application, delegates, actions, security @@ -59,7 +60,7 @@ class AccountViewController: FormViewController { enum Rows { case balance, sendTokens // Wallet - case security, nodes, theme, currency, about, visibleWallets, contribute // Application + case security, nodes, coinsNodes, theme, currency, about, visibleWallets, contribute, vibration // Application case voteForDelegates, generateQr, generatePk, logout // Actions case stayIn, biometry, notifications // Security @@ -81,6 +82,8 @@ class AccountViewController: FormViewController { case .notifications: return "notifications" case .visibleWallets: return "visibleWallets" case .contribute: return "contribute" + case .vibration: return "vibration" + case .coinsNodes: return "coinsNodes" } } @@ -102,28 +105,37 @@ class AccountViewController: FormViewController { case .notifications: return SecurityViewController.Rows.notificationsMode.localized case .visibleWallets: return .localized("VisibleWallets.Title", comment: "Visible Wallets page: scene title") case .contribute: return .localized("AccountTab.Row.Contribute", comment: "Account tab: 'Contribute' row") + case .vibration: return "Vibrations" + case .coinsNodes: return .adamant.coinsNodesList.title } } var image: UIImage? { + var image: UIImage? switch self { - case .security: return .asset(named: "row_security") - case .about: return .asset(named: "row_about") - case .theme: return .asset(named: "row_themes.png") - case .currency: return .asset(named: "row_currency") - case .nodes: return .asset(named: "row_nodes") - case .balance: return .asset(named: "row_balance") - case .voteForDelegates: return .asset(named: "row_vote-delegates") - case .logout: return .asset(named: "row_logout") - case .sendTokens: return nil - case .generateQr: return .asset(named: "row_QR.png") - case .generatePk: return .asset(named: "privateKey_row") - case .stayIn: return .asset(named: "row_security") - case .biometry: return nil // Determined by localAuth service - case .notifications: return .asset(named: "row_Notifications.png") - case .visibleWallets: return .asset(named: "row_balance") - case .contribute: return .asset(named: "row_contribute") + case .security: image = .asset(named: "row_security") + case .about: image = .asset(named: "row_about") + case .theme: image = .asset(named: "row_themes.png") + case .currency: image = .asset(named: "row_currency") + case .nodes: image = .asset(named: "row_nodes") + case .coinsNodes: image = .init(systemName: "server.rack") + case .balance: image = .asset(named: "row_balance") + case .voteForDelegates: image = .asset(named: "row_vote-delegates") + case .logout: image = .asset(named: "row_logout") + case .sendTokens: image = nil + case .generateQr: image = .asset(named: "row_QR.png") + case .generatePk: image = .asset(named: "privateKey_row") + case .stayIn: image = .asset(named: "row_security") + case .biometry: image = nil // Determined by localAuth service + case .notifications: image = .asset(named: "row_Notifications.png") + case .visibleWallets: image = .asset(named: "row_balance") + case .contribute: image = .asset(named: "row_contribute") + case .vibration: image = .asset(named: "row_contribute") } + + return image? + .imageResized(to: .init(squareSize: 24)) + .withTintColor(.adamant.tableRowIcons) } } @@ -131,7 +143,7 @@ class AccountViewController: FormViewController { var visibleWalletsService: VisibleWalletsService! var accountService: AccountService! var dialogService: DialogService! - var router: Router! + var screensFactory: ScreensFactory! var notificationsService: NotificationsService! var transfersProvider: TransfersProvider! var localAuth: LocalAuthentication! @@ -151,6 +163,7 @@ class AccountViewController: FormViewController { private var initiated = false private var walletViewControllers = [WalletViewController]() + private var notificationsSet: Set = [] // MARK: StayIn @@ -272,22 +285,21 @@ class AccountViewController: FormViewController { }.cellUpdate { (cell, _) in cell.accessoryType = .disclosureIndicator }.onCellSelection { [weak self] (_, _) in - guard let vc = self?.router.get(scene: AdamantScene.Settings.visibleWallets) else { - return - } + guard let self = self else { return } + let vc = screensFactory.makeVisibleWallets() - if let split = self?.splitViewController { + if let split = splitViewController { let details = UINavigationController(rootViewController:vc) details.definesPresentationContext = true split.showDetailViewController(details, sender: self) - } else if let nav = self?.navigationController { + } else if let nav = navigationController { nav.pushViewController(vc, animated: true) } else { vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) + present(vc, animated: true, completion: nil) } - self?.deselectWalletViewControllers() + deselectWalletViewControllers() } appSection.append(visibleWalletsRow) @@ -301,25 +313,51 @@ class AccountViewController: FormViewController { }.cellUpdate { (cell, _) in cell.accessoryType = .disclosureIndicator }.onCellSelection { [weak self] (_, _) in - guard let vc = self?.router.get(scene: AdamantScene.NodesEditor.nodesList) else { - return - } + guard let self = self else { return } + let vc = screensFactory.makeNodesList() - if let split = self?.splitViewController { + if let split = splitViewController { let details = UINavigationController(rootViewController:vc) split.showDetailViewController(details, sender: self) - } else if let nav = self?.navigationController { + } else if let nav = navigationController { nav.pushViewController(vc, animated: true) } else { vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) + present(vc, animated: true, completion: nil) } - self?.deselectWalletViewControllers() + deselectWalletViewControllers() } appSection.append(nodesRow) + // Coins nodes list + let coinsNodesRow = LabelRow { + $0.title = Rows.coinsNodes.localized + $0.tag = Rows.coinsNodes.tag + $0.cell.imageView?.image = Rows.coinsNodes.image + $0.cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, _) in + guard let self = self else { return } + let vc = screensFactory.makeCoinsNodesList(context: .menu) + + if let split = splitViewController { + let details = UINavigationController(rootViewController:vc) + split.showDetailViewController(details, sender: self) + } else if let nav = navigationController { + nav.pushViewController(vc, animated: true) + } else { + vc.modalPresentationStyle = .overFullScreen + present(vc, animated: true, completion: nil) + } + + deselectWalletViewControllers() + } + + appSection.append(coinsNodesRow) + // Currency select let currencyRow = ActionSheetRow { $0.title = Rows.currency.localized @@ -354,22 +392,20 @@ class AccountViewController: FormViewController { }.cellUpdate { (cell, _) in cell.accessoryType = .disclosureIndicator }.onCellSelection { [weak self] (_, _) in - guard let vc = self?.router.get(scene: AdamantScene.Settings.contribute) - else { - return - } + guard let self = self else { return } + let vc = screensFactory.makeContribute() - if let split = self?.splitViewController { - let details = UINavigationController(rootViewController:vc) + if let split = splitViewController { + let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) - } else if let nav = self?.navigationController { + } else if let nav = navigationController { nav.pushViewController(vc, animated: true) } else { vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) + present(vc, animated: true, completion: nil) } - self?.deselectWalletViewControllers() + deselectWalletViewControllers() } appSection.append(contributeRow) @@ -383,21 +419,20 @@ class AccountViewController: FormViewController { }.cellUpdate { (cell, _) in cell.accessoryType = .disclosureIndicator }.onCellSelection { [weak self] (_, _) in - guard let vc = self?.router.get(scene: AdamantScene.Settings.about) else { - return - } + guard let self = self else { return } + let vc = screensFactory.makeAbout() - if let split = self?.splitViewController { + if let split = splitViewController { let details = UINavigationController(rootViewController:vc) split.showDetailViewController(details, sender: self) - } else if let nav = self?.navigationController { + } else if let nav = navigationController { nav.pushViewController(vc, animated: true) } else { vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) + present(vc, animated: true, completion: nil) } - self?.deselectWalletViewControllers() + deselectWalletViewControllers() } appSection.append(aboutRow) @@ -416,22 +451,21 @@ class AccountViewController: FormViewController { }.cellUpdate { (cell, _) in cell.accessoryType = .disclosureIndicator }.onCellSelection { [weak self] (_, _) in - guard let vc = self?.router.get(scene: AdamantScene.Delegates.delegates) else { - return - } + guard let self = self else { return } + let vc = screensFactory.makeDelegatesList() - if let split = self?.splitViewController { + if let split = splitViewController { let details = UINavigationController(rootViewController:vc) details.definesPresentationContext = true split.showDetailViewController(details, sender: self) - } else if let nav = self?.navigationController { + } else if let nav = navigationController { nav.pushViewController(vc, animated: true) } else { vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) + present(vc, animated: true, completion: nil) } - self?.deselectWalletViewControllers() + deselectWalletViewControllers() } actionsSection.append(delegatesRow) @@ -445,21 +479,20 @@ class AccountViewController: FormViewController { }.cellUpdate { (cell, _) in cell.accessoryType = .disclosureIndicator }.onCellSelection { [weak self] (_, _) in - guard let vc = self?.router.get(scene: AdamantScene.Settings.qRGenerator) else { - return - } + guard let self = self else { return } + let vc = screensFactory.makeQRGenerator() - if let split = self?.splitViewController { + if let split = splitViewController { let details = UINavigationController(rootViewController:vc) split.showDetailViewController(details, sender: self) - } else if let nav = self?.navigationController { + } else if let nav = navigationController { nav.pushViewController(vc, animated: true) } else { vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) + present(vc, animated: true, completion: nil) } - self?.deselectWalletViewControllers() + deselectWalletViewControllers() } actionsSection.append(generateQrRow) @@ -473,21 +506,20 @@ class AccountViewController: FormViewController { }.cellUpdate { (cell, _) in cell.accessoryType = .disclosureIndicator }.onCellSelection { [weak self] (_, _) in - guard let vc = self?.router.get(scene: AdamantScene.Settings.pkGenerator) else { - return - } + guard let self = self else { return } + let vc = screensFactory.makePKGenerator() - if let split = self?.splitViewController { + if let split = splitViewController { let details = UINavigationController(rootViewController:vc) split.showDetailViewController(details, sender: self) - } else if let nav = self?.navigationController { + } else if let nav = navigationController { nav.pushViewController(vc, animated: true) } else { vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) + present(vc, animated: true, completion: nil) } - self?.deselectWalletViewControllers() + deselectWalletViewControllers() } actionsSection.append(generatePkRow) @@ -513,12 +545,15 @@ class AccountViewController: FormViewController { self?.tableView.deselectRow(at: indexPath, animated: true) } - let logout = UIAlertAction(title: String.adamant.alert.logoutButton, style: .default) { [weak self] _ in - self?.accountService.logout() - if let vc = self?.router.get(scene: AdamantScene.Login.login) { - vc.modalPresentationStyle = .overFullScreen - self?.dialogService.present(vc, animated: true, completion: nil) - } + let logout = UIAlertAction( + title: .adamant.alert.logoutButton, + style: .default + ) { [weak self] _ in + guard let self = self else { return } + accountService.logout() + let vc = screensFactory.makeLogin() + vc.modalPresentationStyle = .overFullScreen + dialogService.present(vc, animated: true, completion: nil) } alert.addAction(cancel) @@ -556,11 +591,12 @@ class AccountViewController: FormViewController { // Biometry let biometryRow = SwitchRow { [weak self] in + guard let self = self else { return } $0.tag = Rows.biometry.tag $0.title = localAuth.biometryType.localized $0.value = accountService.useBiometry - if let auth = self?.localAuth { + if let auth = localAuth { switch auth.biometryType { case .none: $0.cell.imageView?.image = nil case .touchID: $0.cell.imageView?.image = .asset(named: "row_touchid.png") @@ -601,21 +637,20 @@ class AccountViewController: FormViewController { }.cellUpdate { (cell, _) in cell.accessoryType = .disclosureIndicator }.onCellSelection { [weak self] (_, _) in - guard let vc = self?.router.get(scene: AdamantScene.Settings.notifications) else { - return - } + guard let self = self else { return } + let vc = screensFactory.makeNotifications() - if let split = self?.splitViewController { + if let split = splitViewController { let details = UINavigationController(rootViewController:vc) split.showDetailViewController(details, sender: self) - } else if let nav = self?.navigationController { + } else if let nav = navigationController { nav.pushViewController(vc, animated: true) } else { vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) + present(vc, animated: true, completion: nil) } - self?.deselectWalletViewControllers() + deselectWalletViewControllers() } securitySection.append(notificationsRow) @@ -793,13 +828,20 @@ class AccountViewController: FormViewController { queue: OperationQueue.main, using: callback) } + + NotificationCenter.default + .publisher(for: .AdamantVibroService.presentVibrationRow) + .sink { [weak self] _ in + self?.addVibrationRow() + } + .store(in: ¬ificationsSet) } private func setupWalletsVC() { walletViewControllers.removeAll() let availableServices: [WalletService] = visibleWalletsService.sorted(includeInvisible: false) availableServices.forEach { walletService in - walletViewControllers.append(walletService.walletViewController) + walletViewControllers.append(screensFactory.makeWalletVC(service: walletService)) } } @@ -898,6 +940,40 @@ class AccountViewController: FormViewController { self.accountService.reloadWallets() } } + + private func addVibrationRow() { + guard let appSection = form.sectionBy(tag: Sections.application.tag), + form.rowBy(tag: Rows.vibration.tag) == nil + else { return } + + let vibrationRow = LabelRow { + $0.title = Rows.vibration.localized + $0.tag = Rows.vibration.tag + $0.cell.imageView?.image = Rows.vibration.image + $0.cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, _) in + guard let vc = self?.screensFactory.makeVibrationSelection() + else { + return + } + + if let split = self?.splitViewController { + let details = UINavigationController(rootViewController:vc) + split.showDetailViewController(details, sender: self) + } else if let nav = self?.navigationController { + nav.pushViewController(vc, animated: true) + } else { + vc.modalPresentationStyle = .overFullScreen + self?.present(vc, animated: true, completion: nil) + } + + self?.deselectWalletViewControllers() + } + + appSection.append(vibrationRow) + } } // MARK: - AccountHeaderViewDelegate @@ -959,7 +1035,7 @@ extension AccountViewController: PagingViewControllerDataSource, PagingViewContr if ERC20Token.supportedTokens.contains(where: { token in return token.symbol == service.tokenSymbol }) { - network = service.tokenNetworkSymbol + network = type(of: service).tokenNetworkSymbol } let item = WalletPagingItem( diff --git a/Adamant/Stories/Account/WalletCollectionViewCell.swift b/Adamant/Modules/Account/WalletCollectionViewCell.swift similarity index 100% rename from Adamant/Stories/Account/WalletCollectionViewCell.swift rename to Adamant/Modules/Account/WalletCollectionViewCell.swift diff --git a/Adamant/Stories/Account/WalletCollectionViewCell.xib b/Adamant/Modules/Account/WalletCollectionViewCell.xib similarity index 100% rename from Adamant/Stories/Account/WalletCollectionViewCell.xib rename to Adamant/Modules/Account/WalletCollectionViewCell.xib diff --git a/Adamant/Stories/Account/WalletPagingItem.swift b/Adamant/Modules/Account/WalletPagingItem.swift similarity index 100% rename from Adamant/Stories/Account/WalletPagingItem.swift rename to Adamant/Modules/Account/WalletPagingItem.swift diff --git a/Adamant/Stories/Chat/ChatFactory.swift b/Adamant/Modules/Chat/ChatFactory.swift similarity index 78% rename from Adamant/Stories/Chat/ChatFactory.swift rename to Adamant/Modules/Chat/ChatFactory.swift index 4c29eeb2f..81d9d61f0 100644 --- a/Adamant/Stories/Chat/ChatFactory.swift +++ b/Adamant/Modules/Chat/ChatFactory.swift @@ -10,6 +10,7 @@ import UIKit import MessageKit import InputBarAccessoryView import Combine +import Swinject @MainActor struct ChatFactory { @@ -19,14 +20,26 @@ struct ChatFactory { let transferProvider: TransfersProvider let accountService: AccountService let accountProvider: AccountsProvider - let richTransactionStatusService: RichTransactionStatusService + let richTransactionStatusService: TransactionStatusService let addressBookService: AddressBookService let visibleWalletService: VisibleWalletsService let avatarService: AvatarService let emojiService: EmojiService - let router: Router - func makeViewController() -> UIViewController { + nonisolated init(assembler: Assembler) { + chatsProvider = assembler.resolve(ChatsProvider.self)! + dialogService = assembler.resolve(DialogService.self)! + transferProvider = assembler.resolve(TransfersProvider.self)! + accountService = assembler.resolve(AccountService.self)! + accountProvider = assembler.resolve(AccountsProvider.self)! + richTransactionStatusService = assembler.resolve(TransactionStatusService.self)! + addressBookService = assembler.resolve(AddressBookService.self)! + visibleWalletService = assembler.resolve(VisibleWalletsService.self)! + avatarService = assembler.resolve(AvatarService.self)! + emojiService = assembler.resolve(EmojiService.self)! + } + + func makeViewController(screensFactory: ScreensFactory) -> ChatViewController { let richMessageProviders = makeRichMessageProviders() let viewModel = makeViewModel(richMessageProviders: richMessageProviders) let delegates = makeDelegates(viewModel: viewModel) @@ -38,14 +51,18 @@ struct ChatFactory { let admService = accountService.wallets.first { wallet in return wallet is AdmWalletService - } as? AdmWalletService + } as! AdmWalletService let viewController = ChatViewController( viewModel: viewModel, richMessageProviders: richMessageProviders, storedObjects: delegates.asArray + [dialogManager], - sendTransaction: makeSendTransactionAction(viewModel: viewModel), - admService: admService + admService: admService, + screensFactory: screensFactory, + sendTransaction: makeSendTransactionAction( + viewModel: viewModel, + screensFactory: screensFactory + ) ) viewController.setupDelegates(delegates) @@ -114,10 +131,11 @@ private extension ChatFactory { } func makeSendTransactionAction( - viewModel: ChatViewModel + viewModel: ChatViewModel, + screensFactory: ScreensFactory ) -> ChatViewController.SendTransaction { - { [router, viewModel] parentVC, messageId in - guard let vc = router.get(scene: AdamantScene.Chats.complexTransfer) as? ComplexTransferViewController + { [screensFactory, viewModel] parentVC, messageId in + guard let vc = screensFactory.makeComplexTransfer() as? ComplexTransferViewController else { return } vc.partner = viewModel.chatroom?.partner diff --git a/Adamant/Stories/Chat/ChatLocalization.swift b/Adamant/Modules/Chat/ChatLocalization.swift similarity index 95% rename from Adamant/Stories/Chat/ChatLocalization.swift rename to Adamant/Modules/Chat/ChatLocalization.swift index f172487a9..441fad9c3 100644 --- a/Adamant/Stories/Chat/ChatLocalization.swift +++ b/Adamant/Modules/Chat/ChatLocalization.swift @@ -41,5 +41,6 @@ extension String.adamant { static let transactionSent = String.localized("ChatScene.Sent", comment: "Chat: 'Sent funds' bubble title") static let transactionReceived = String.localized("ChatScene.Received", comment: "Chat: 'Received funds' bubble title") static let messageWasDeleted = String.localized("ChatScene.Error.messageWasDeleted", comment: "Chat: Error scrolling to message, this message has been deleted and is no longer accessible") + static let messageIsTooBig = String.localized("ChatScene.Error.messageIsTooBig", comment: "Chat: Error message is too big") } } diff --git a/Adamant/Stories/Chat/View/ChatViewController.swift b/Adamant/Modules/Chat/View/ChatViewController.swift similarity index 90% rename from Adamant/Stories/Chat/View/ChatViewController.swift rename to Adamant/Modules/Chat/View/ChatViewController.swift index 3a40bf6ea..56ad90502 100644 --- a/Adamant/Stories/Chat/View/ChatViewController.swift +++ b/Adamant/Modules/Chat/View/ChatViewController.swift @@ -16,13 +16,17 @@ import CommonKit @MainActor final class ChatViewController: MessagesViewController { typealias SpinnerCell = MessageCellWrapper - typealias SendTransaction = ( _ parentVC: UIViewController & ComplexTransferViewControllerDelegate, _ replyToMessageId: String?) -> Void + typealias SendTransaction = @MainActor ( + _ parentVC: UIViewController & ComplexTransferViewControllerDelegate, + _ replyToMessageId: String? + ) -> Void // MARK: Dependencies private let storedObjects: [AnyObject] private let richMessageProviders: [String: RichMessageProvider] - private let admService: AdmWalletService? + private let admService: AdmWalletService + private let screensFactory: ScreensFactory let viewModel: ChatViewModel @@ -71,13 +75,15 @@ final class ChatViewController: MessagesViewController { viewModel: ChatViewModel, richMessageProviders: [String: RichMessageProvider], storedObjects: [AnyObject], - sendTransaction: @escaping SendTransaction, - admService: AdmWalletService? + admService: AdmWalletService, + screensFactory: ScreensFactory, + sendTransaction: @escaping SendTransaction ) { self.viewModel = viewModel self.storedObjects = storedObjects self.richMessageProviders = richMessageProviders self.admService = admService + self.screensFactory = screensFactory super.init(nibName: nil, bundle: nil) inputBar.onAttachmentButtonTap = { [weak self] in self.map { sendTransaction($0, viewModel.replyMessage?.id) } @@ -261,7 +267,14 @@ private extension ChatViewController { viewModel.$isSendingAvailable .removeDuplicates() - .assign(to: \.isEnabled, on: inputBar) + .sink(receiveValue: { [weak self] value in + self?.inputBar.isEnabled = value + if !value { + self?.navigationItem.rightBarButtonItem = nil + } else { + self?.configureHeaderRightButton() + } + }) .store(in: &subscriptions) viewModel.$fee @@ -345,6 +358,10 @@ private extension ChatViewController { viewModel.layoutIfNeeded .sink { [weak self] in self?.view.layoutIfNeeded() } .store(in: &subscriptions) + + viewModel.didTapPartnerQR + .sink { [weak self] in self?.didTapPartenerQR(partner: $0) } + .store(in: &subscriptions) } } @@ -368,6 +385,24 @@ private extension ChatViewController { func configureHeader() { navigationItem.titleView = updatingIndicatorView navigationItem.largeTitleDisplayMode = .never + + configureHeaderRightButton() + + let tapGesture = UITapGestureRecognizer( + target: self, + action: #selector(shortTapAction) + ) + + let longPressGesture = UILongPressGestureRecognizer( + target: self, + action: #selector(longTapAction(_:)) + ) + + navigationItem.titleView?.addGestureRecognizer(tapGesture) + navigationItem.titleView?.addGestureRecognizer(longPressGesture) + } + + func configureHeaderRightButton() { navigationItem.rightBarButtonItem = .init( title: "•••", style: .plain, @@ -406,6 +441,19 @@ private extension ChatViewController { } } +// MARK: Tap on title view + +private extension ChatViewController { + @objc func shortTapAction() { + viewModel.openPartnerQR() + } + + @objc func longTapAction(_ gestureRecognizer: UILongPressGestureRecognizer) { + guard gestureRecognizer.state == .began else { return } + viewModel.renamePartner() + } +} + // MARK: Content updating private extension ChatViewController { @@ -618,13 +666,20 @@ private extension ChatViewController { } func didTapTransferTransaction(_ transaction: TransferTransaction) { - admService?.richMessageTapped(for: transaction, in: self) + let vc = screensFactory.makeAdmTransactionDetails(transaction: transaction) + navigationController?.pushViewController(vc, animated: true) + } + + func didTapPartenerQR(partner: CoreDataAccount) { + let vc = screensFactory.makePartnerQR(partner: partner) + navigationController?.pushViewController(vc, animated: true) } func didTapRichMessageTransaction(_ transaction: RichMessageTransaction) { guard let type = transaction.richType, - let provider = richMessageProviders[type] + let provider = richMessageProviders[type], + let vc = screensFactory.makeDetailsVC(service: provider, transaction: transaction) else { return } switch transaction.transactionStatus { @@ -635,9 +690,9 @@ private extension ChatViewController { return } - provider.richMessageTapped(for: transaction, in: self) + navigationController?.pushViewController(vc, animated: true) case .notInitiated, .pending, .success, .none, .inconsistent, .registered, .noNetwork, .noNetworkFinal: - provider.richMessageTapped(for: transaction, in: self) + navigationController?.pushViewController(vc, animated: true) } } @@ -709,12 +764,10 @@ private extension ChatViewController { } func didTapAdmSend(to adm: AdamantAddress) { - guard let vc = admService?.transferViewController() else { return } - if let v = vc as? TransferViewControllerBase { - v.recipientAddress = adm.address - v.recipientName = adm.name - v.delegate = self - } + let vc = screensFactory.makeTransferVC(service: admService) + vc.recipientAddress = adm.address + vc.recipientName = adm.name + vc.delegate = self self.navigationController?.pushViewController(vc, animated: true) } } diff --git a/Adamant/Stories/Chat/View/Helpers/AdamantCellAnimation.swift b/Adamant/Modules/Chat/View/Helpers/AdamantCellAnimation.swift similarity index 100% rename from Adamant/Stories/Chat/View/Helpers/AdamantCellAnimation.swift rename to Adamant/Modules/Chat/View/Helpers/AdamantCellAnimation.swift diff --git a/Adamant/Stories/Chat/View/Helpers/ChatReactionsView.swift b/Adamant/Modules/Chat/View/Helpers/ChatReactionsView.swift similarity index 100% rename from Adamant/Stories/Chat/View/Helpers/ChatReactionsView.swift rename to Adamant/Modules/Chat/View/Helpers/ChatReactionsView.swift diff --git a/Adamant/Stories/Chat/View/Managers/ChatAction.swift b/Adamant/Modules/Chat/View/Managers/ChatAction.swift similarity index 100% rename from Adamant/Stories/Chat/View/Managers/ChatAction.swift rename to Adamant/Modules/Chat/View/Managers/ChatAction.swift diff --git a/Adamant/Stories/Chat/View/Managers/ChatCellManager.swift b/Adamant/Modules/Chat/View/Managers/ChatCellManager.swift similarity index 100% rename from Adamant/Stories/Chat/View/Managers/ChatCellManager.swift rename to Adamant/Modules/Chat/View/Managers/ChatCellManager.swift diff --git a/Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift similarity index 100% rename from Adamant/Stories/Chat/View/Managers/ChatDataSourceManager.swift rename to Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift diff --git a/Adamant/Stories/Chat/View/Managers/ChatDialogManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift similarity index 88% rename from Adamant/Stories/Chat/View/Managers/ChatDialogManager.swift rename to Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift index bb8a0a486..cc4126ef6 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatDialogManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift @@ -87,6 +87,7 @@ private extension ChatDialogManager { case let .failedMessageAlert(id, sender): showFailedMessageAlert(id: id, sender: sender) case let .presentMenu( + presentReactions, arg, didSelectEmojiDelegate, didSelectEmojiAction, @@ -94,6 +95,7 @@ private extension ChatDialogManager { didDismissMenuAction ): presentMenu( + presentReactions: presentReactions, arg: arg, didSelectEmojiDelegate: didSelectEmojiDelegate, didSelectEmojiAction: didSelectEmojiAction, @@ -102,6 +104,8 @@ private extension ChatDialogManager { ) case .dismissMenu: dismissMenu() + case .renameAlert: + showRenameAlert() } } @@ -134,23 +138,28 @@ private extension ChatDialogManager { } func showSystemPartnerMenu(sender: UIBarButtonItem) { - guard let address = address, let encodedAddress = encodedAddress else { return } + guard let address = address else { return } + + let didSelect: ((ShareType) -> Void)? = { [weak self] type in + guard case .partnerQR = type, + let partner = self?.viewModel.chatroom?.partner + else { return } + + self?.viewModel.didTapPartnerQR.send(partner) + } dialogService.presentShareAlertFor( string: address, types: [ .copyToPasteboard, .share, - .generateQr( - encodedContent: encodedAddress, - sharingTip: address, - withLogo: true - ) + .partnerQR ], excludedActivityTypes: ShareContentType.address.excludedActivityTypes, animated: true, from: sender, - completion: nil + completion: nil, + didSelect: didSelect ) } @@ -215,9 +224,16 @@ private extension ChatDialogManager { makeCancelSendingAction(id: id), makeCancelAction() ], - from: sender + from: nil ) } + + func showRenameAlert() { + guard let alert = makeRenameAlert() else { return } + dialogService.present(alert, animated: true) { [weak self] in + self?.dialogService.selectAllTextFields(in: alert) + } + } } // MARK: Alert actions @@ -254,10 +270,7 @@ private extension ChatDialogManager { title: .adamant.chat.rename, style: .default ) { [weak self] _ in - guard let alert = self?.makeRenameAlert() else { return } - self?.dialogService.present(alert, animated: true) { - self?.dialogService.selectAllTextFields(in: alert) - } + self?.showRenameAlert() } } @@ -302,25 +315,29 @@ private extension ChatDialogManager { ) { [weak self] _ in guard let self = self, - let address = self.address, - let encodedAddress = self.encodedAddress + let address = self.address else { return } + let didSelect: ((ShareType) -> Void)? = { [weak self] type in + guard case .partnerQR = type, + let partner = self?.viewModel.chatroom?.partner + else { return } + + self?.viewModel.didTapPartnerQR.send(partner) + } + self.dialogService.presentShareAlertFor( string: address, types: [ .copyToPasteboard, .share, - .generateQr( - encodedContent: encodedAddress, - sharingTip: address, - withLogo: true - ) + .partnerQR ], excludedActivityTypes: ShareContentType.address.excludedActivityTypes, animated: true, from: sender, - completion: nil + completion: nil, + didSelect: didSelect ) } } @@ -433,6 +450,7 @@ private extension ChatDialogManager { } func presentMenu( + presentReactions: Bool, arg: ChatContextMenuArguments, didSelectEmojiDelegate: ElegantEmojiPickerDelegate?, didSelectEmojiAction: DidSelectEmojiAction, @@ -442,15 +460,23 @@ private extension ChatDialogManager { contextMenu.didPresentMenuAction = didPresentMenuAction contextMenu.didDismissMenuAction = didDismissMenuAction + let reactionsContentView = !presentReactions + ? nil + : getUpperContentView( + messageId: arg.messageId, + selectedEmoji: arg.selectedEmoji, + didSelectEmojiAction: didSelectEmojiAction, + didSelectEmojiDelegate: didSelectEmojiDelegate + ) + + let reactionsContentViewSize: CGSize = !presentReactions + ? .zero + : getUpperContentViewSize() + contextMenu.presentMenu( arg: arg, - upperView: getUpperContentView( - messageId: arg.messageId, - selectedEmoji: arg.selectedEmoji, - didSelectEmojiAction: didSelectEmojiAction, - didSelectEmojiDelegate: didSelectEmojiDelegate - ), - upperViewSize: getUpperContentViewSize() + upperView: reactionsContentView, + upperViewSize: reactionsContentViewSize ) } diff --git a/Adamant/Stories/Chat/View/Managers/ChatDisplayManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDisplayManager.swift similarity index 100% rename from Adamant/Stories/Chat/View/Managers/ChatDisplayManager.swift rename to Adamant/Modules/Chat/View/Managers/ChatDisplayManager.swift diff --git a/Adamant/Stories/Chat/View/Managers/ChatInputBarManager.swift b/Adamant/Modules/Chat/View/Managers/ChatInputBarManager.swift similarity index 89% rename from Adamant/Stories/Chat/View/Managers/ChatInputBarManager.swift rename to Adamant/Modules/Chat/View/Managers/ChatInputBarManager.swift index 994e5c076..8e960067f 100644 --- a/Adamant/Stories/Chat/View/Managers/ChatInputBarManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatInputBarManager.swift @@ -18,6 +18,7 @@ final class ChatInputBarManager: InputBarAccessoryViewDelegate { } func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) { + guard viewModel.canSendMessage(withText: text) else { return } inputBar.inputTextView.text = "" viewModel.sendMessage(text: text) } diff --git a/Adamant/Stories/Chat/View/Managers/ChatKeyboardManager.swift b/Adamant/Modules/Chat/View/Managers/ChatKeyboardManager.swift similarity index 100% rename from Adamant/Stories/Chat/View/Managers/ChatKeyboardManager.swift rename to Adamant/Modules/Chat/View/Managers/ChatKeyboardManager.swift diff --git a/Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift b/Adamant/Modules/Chat/View/Managers/ChatLayoutManager.swift similarity index 100% rename from Adamant/Stories/Chat/View/Managers/ChatLayoutManager.swift rename to Adamant/Modules/Chat/View/Managers/ChatLayoutManager.swift diff --git a/Adamant/Stories/Chat/View/Managers/ChatMenuManager.swift b/Adamant/Modules/Chat/View/Managers/ChatMenuManager.swift similarity index 100% rename from Adamant/Stories/Chat/View/Managers/ChatMenuManager.swift rename to Adamant/Modules/Chat/View/Managers/ChatMenuManager.swift diff --git a/Adamant/Stories/Chat/View/Managers/FixedTextMessageSizeCalculator.swift b/Adamant/Modules/Chat/View/Managers/FixedTextMessageSizeCalculator.swift similarity index 100% rename from Adamant/Stories/Chat/View/Managers/FixedTextMessageSizeCalculator.swift rename to Adamant/Modules/Chat/View/Managers/FixedTextMessageSizeCalculator.swift diff --git a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift similarity index 94% rename from Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift rename to Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift index d5b4e4811..cb166e91a 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift @@ -17,6 +17,7 @@ extension ChatMessageCell { let reactions: Set? let address: String let opponentAddress: String + let isFake: Bool var isHidden: Bool static let `default` = Self( @@ -27,6 +28,7 @@ extension ChatMessageCell { reactions: nil, address: "", opponentAddress: "", + isFake: false, isHidden: false ) diff --git a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift similarity index 92% rename from Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift rename to Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift index 42b1d9e5b..080cbad54 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift @@ -391,6 +391,35 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { updateOwnReaction() updateOpponentReaction() } + + /// Handle tap gesture on contentView and its subviews. + override func handleTapGesture(_ gesture: UIGestureRecognizer) { + let touchLocation = gesture.location(in: self) + + let containerViewContains = containerView.frame.contains(touchLocation) + let canHandle = !cellContentView( + canHandle: convert(touchLocation, to: containerView) + ) + + switch true { + case containerViewContains && canHandle: + delegate?.didTapMessage(in: self) + case avatarView.frame.contains(touchLocation): + delegate?.didTapAvatar(in: self) + case cellTopLabel.frame.contains(touchLocation): + delegate?.didTapCellTopLabel(in: self) + case cellBottomLabel.frame.contains(touchLocation): + delegate?.didTapCellBottomLabel(in: self) + case messageTopLabel.frame.contains(touchLocation): + delegate?.didTapMessageTopLabel(in: self) + case messageBottomLabel.frame.contains(touchLocation): + delegate?.didTapMessageBottomLabel(in: self) + case accessoryView.frame.contains(touchLocation): + delegate?.didTapAccessoryView(in: self) + default: + delegate?.didTapBackground(in: self) + } + } } extension ChatMessageCell { @@ -424,6 +453,10 @@ extension ChatMessageCell { actionHandler(.copy(text: model.text.string)) } + guard !model.isFake else { + return AMenuSection([copy]) + } + return AMenuSection([reply, copy, report, remove]) } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatInputBar.swift b/Adamant/Modules/Chat/View/Subviews/ChatInputBar.swift similarity index 100% rename from Adamant/Stories/Chat/View/Subviews/ChatInputBar.swift rename to Adamant/Modules/Chat/View/Subviews/ChatInputBar.swift diff --git a/Adamant/Stories/Chat/View/Subviews/ChatMessagesCollection.swift b/Adamant/Modules/Chat/View/Subviews/ChatMessagesCollection.swift similarity index 100% rename from Adamant/Stories/Chat/View/Subviews/ChatMessagesCollection.swift rename to Adamant/Modules/Chat/View/Subviews/ChatMessagesCollection.swift diff --git a/Adamant/Stories/Chat/View/Subviews/ChatModelView.swift b/Adamant/Modules/Chat/View/Subviews/ChatModelView.swift similarity index 100% rename from Adamant/Stories/Chat/View/Subviews/ChatModelView.swift rename to Adamant/Modules/Chat/View/Subviews/ChatModelView.swift diff --git a/Adamant/Stories/Chat/View/Subviews/ChatRefreshMock.swift b/Adamant/Modules/Chat/View/Subviews/ChatRefreshMock.swift similarity index 100% rename from Adamant/Stories/Chat/View/Subviews/ChatRefreshMock.swift rename to Adamant/Modules/Chat/View/Subviews/ChatRefreshMock.swift diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift b/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift similarity index 100% rename from Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift rename to Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift diff --git a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift similarity index 95% rename from Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift rename to Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index 7defceb51..4d4c4ec3b 100644 --- a/Adamant/Stories/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -489,13 +489,33 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { messageLabel.handleGesture(touchPoint) } + /// Handle tap gesture on contentView and its subviews. override func handleTapGesture(_ gesture: UIGestureRecognizer) { - super.handleTapGesture(gesture) - let touchLocation = gesture.location(in: self) - if containerView.frame.contains(touchLocation) { + let containerViewContains = containerView.frame.contains(touchLocation) + let canHandle = !cellContentView( + canHandle: convert(touchLocation, to: containerView) + ) + + switch true { + case containerViewContains && canHandle: + delegate?.didTapMessage(in: self) actionHandler(.scrollTo(message: model)) + case avatarView.frame.contains(touchLocation): + delegate?.didTapAvatar(in: self) + case cellTopLabel.frame.contains(touchLocation): + delegate?.didTapCellTopLabel(in: self) + case cellBottomLabel.frame.contains(touchLocation): + delegate?.didTapCellBottomLabel(in: self) + case messageTopLabel.frame.contains(touchLocation): + delegate?.didTapMessageTopLabel(in: self) + case messageBottomLabel.frame.contains(touchLocation): + delegate?.didTapMessageBottomLabel(in: self) + case accessoryView.frame.contains(touchLocation): + delegate?.didTapAccessoryView(in: self) + default: + delegate?.didTapBackground(in: self) } } } diff --git a/Adamant/Stories/Chat/View/Subviews/ChatScrollDownButton.swift b/Adamant/Modules/Chat/View/Subviews/ChatScrollDownButton.swift similarity index 100% rename from Adamant/Stories/Chat/View/Subviews/ChatScrollDownButton.swift rename to Adamant/Modules/Chat/View/Subviews/ChatScrollDownButton.swift diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift similarity index 100% rename from Adamant/Stories/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift rename to Adamant/Modules/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift similarity index 100% rename from Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift rename to Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift similarity index 100% rename from Adamant/Stories/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift rename to Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift similarity index 100% rename from Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift rename to Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift diff --git a/Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift similarity index 100% rename from Adamant/Stories/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift rename to Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift diff --git a/Adamant/Stories/Chat/ViewModel/ChatCacheService.swift b/Adamant/Modules/Chat/ViewModel/ChatCacheService.swift similarity index 86% rename from Adamant/Stories/Chat/ViewModel/ChatCacheService.swift rename to Adamant/Modules/Chat/ViewModel/ChatCacheService.swift index 10c57483c..e1e940c0d 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatCacheService.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatCacheService.swift @@ -14,11 +14,8 @@ final class ChatCacheService { private var messages: [String: [ChatMessage]] = [:] private var subscriptions = Set() - init() { - NotificationCenter.default - .publisher(for: .AdamantAccountService.userLoggedOut) - .sink { [weak self] _ in self?.messages = .init() } - .store(in: &subscriptions) + nonisolated init() { + Task { await setup() } } func setMessages(address: String, messages: [ChatMessage]) { @@ -29,3 +26,12 @@ final class ChatCacheService { messages[address] } } + +private extension ChatCacheService { + func setup() { + NotificationCenter.default + .publisher(for: .AdamantAccountService.userLoggedOut) + .sink { [weak self] _ in self?.messages = .init() } + .store(in: &subscriptions) + } +} diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Modules/Chat/ViewModel/ChatMessageFactory.swift similarity index 99% rename from Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift rename to Adamant/Modules/Chat/ViewModel/ChatMessageFactory.swift index c2eb6f6ac..83e710273 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatMessageFactory.swift @@ -186,6 +186,7 @@ private extension ChatMessageFactory { reactions: reactions, address: address, opponentAddress: opponentAddress, + isFake: transaction.isFake, isHidden: false ) )) diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessagesListFactory.swift b/Adamant/Modules/Chat/ViewModel/ChatMessagesListFactory.swift similarity index 100% rename from Adamant/Stories/Chat/ViewModel/ChatMessagesListFactory.swift rename to Adamant/Modules/Chat/ViewModel/ChatMessagesListFactory.swift diff --git a/Adamant/Stories/Chat/ViewModel/ChatMessagesListViewModel.swift b/Adamant/Modules/Chat/ViewModel/ChatMessagesListViewModel.swift similarity index 100% rename from Adamant/Stories/Chat/ViewModel/ChatMessagesListViewModel.swift rename to Adamant/Modules/Chat/ViewModel/ChatMessagesListViewModel.swift diff --git a/Adamant/Stories/Chat/ViewModel/ChatPreservationDelegate.swift b/Adamant/Modules/Chat/ViewModel/ChatPreservationDelegate.swift similarity index 100% rename from Adamant/Stories/Chat/ViewModel/ChatPreservationDelegate.swift rename to Adamant/Modules/Chat/ViewModel/ChatPreservationDelegate.swift diff --git a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift similarity index 96% rename from Adamant/Stories/Chat/ViewModel/ChatViewModel.swift rename to Adamant/Modules/Chat/ViewModel/ChatViewModel.swift index c764ae4c3..913d2e863 100644 --- a/Adamant/Stories/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift @@ -26,7 +26,7 @@ final class ChatViewModel: NSObject { private let visibleWalletService: VisibleWalletsService private let accountService: AccountService private let accountProvider: AccountsProvider - private let richTransactionStatusService: RichTransactionStatusService + private let richTransactionStatusService: TransactionStatusService private let chatCacheService: ChatCacheService private let richMessageProviders: [String: RichMessageProvider] private let avatarService: AvatarService @@ -60,6 +60,7 @@ final class ChatViewModel: NSObject { private let minDiffCountForOffset = 5 private let minDiffCountForAnimateScroll = 20 private let partnerImageSize: CGFloat = 25 + private let maxMessageLenght: Int = 10000 private var previousArg: ChatContextMenuArguments? let minIndexForStartLoadNewMessages = 4 @@ -67,6 +68,7 @@ final class ChatViewModel: NSObject { var tempOffsets: [String] = [] var needToAnimateCellIndex: Int? + let didTapPartnerQR = ObservableSender() let didTapTransfer = ObservableSender() let dialog = ObservableSender() let didTapAdmChat = ObservableSender<(Chatroom, String?)>() @@ -127,7 +129,7 @@ final class ChatViewModel: NSObject { visibleWalletService: VisibleWalletsService, accountService: AccountService, accountProvider: AccountsProvider, - richTransactionStatusService: RichTransactionStatusService, + richTransactionStatusService: TransactionStatusService, chatCacheService: ChatCacheService, richMessageProviders: [String: RichMessageProvider], avatarService: AvatarService, @@ -451,6 +453,9 @@ final class ChatViewModel: NSObject { } func replyMessageIfNeeded(_ messageModel: MessageModel?) { + let tx = chatTransactions.first(where: { $0.txId == messageModel?.id }) + guard isSendingAvailable, tx?.isFake == false else { return } + let message = messages.first(where: { $0.messageId == messageModel?.id }) guard message?.status != .failed else { dialog.send(.warning(String.adamant.reply.failedMessageError)) @@ -559,8 +564,19 @@ final class ChatViewModel: NSObject { previousArg = arg + let tx = chatTransactions.first(where: { $0.txId == arg.messageId }) + guard tx?.statusEnum == .delivered else { return } + + let amount = tx?.amountValue ?? .zero + if !amount.isZero && !isSendingAvailable { + return + } + + let presentReactions = isSendingAvailable && tx?.isFake == false + dialog.send( .presentMenu( + presentReactions: presentReactions, arg: arg, didSelectEmojiDelegate: self, didSelectEmojiAction: didSelectEmojiAction, @@ -569,6 +585,15 @@ final class ChatViewModel: NSObject { ) ) } + + func canSendMessage(withText text: String) -> Bool { + guard text.count <= maxMessageLenght else { + dialog.send(.alert(.adamant.chat.messageIsTooBig)) + return false + } + + return true + } } extension ChatViewModel { @@ -598,6 +623,20 @@ extension ChatViewModel { tempOffsets.append(id) } + + func openPartnerQR() { + guard let partner = chatroom?.partner, + isSendingAvailable + else { return } + + didTapPartnerQR.send(partner) + } + + func renamePartner() { + guard isSendingAvailable else { return } + + dialog.send(.renameAlert) + } } extension ChatViewModel: NSFetchedResultsControllerDelegate { diff --git a/Adamant/Stories/Chat/ViewModel/Models/ChatDialog.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift similarity index 95% rename from Adamant/Stories/Chat/ViewModel/Models/ChatDialog.swift rename to Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift index 00d99d3c4..f65484731 100644 --- a/Adamant/Stories/Chat/ViewModel/Models/ChatDialog.swift +++ b/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift @@ -26,6 +26,7 @@ enum ChatDialog { case progress(Bool) case failedMessageAlert(id: String, sender: UIAlertController.SourceView) case presentMenu( + presentReactions: Bool, arg: ChatContextMenuArguments, didSelectEmojiDelegate: ElegantEmojiPickerDelegate?, didSelectEmojiAction: ChatDialogManager.DidSelectEmojiAction, @@ -33,4 +34,5 @@ enum ChatDialog { didDismissMenuAction: ChatDialogManager.ContextMenuAction ) case dismissMenu + case renameAlert } diff --git a/Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatMessage.swift similarity index 100% rename from Adamant/Stories/Chat/ViewModel/Models/ChatMessage.swift rename to Adamant/Modules/Chat/ViewModel/Models/ChatMessage.swift diff --git a/Adamant/Stories/Chat/ViewModel/Models/ChatMessageBackgroundColor.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatMessageBackgroundColor.swift similarity index 100% rename from Adamant/Stories/Chat/ViewModel/Models/ChatMessageBackgroundColor.swift rename to Adamant/Modules/Chat/ViewModel/Models/ChatMessageBackgroundColor.swift diff --git a/Adamant/Stories/Chat/ViewModel/Models/ChatSender.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatSender.swift similarity index 100% rename from Adamant/Stories/Chat/ViewModel/Models/ChatSender.swift rename to Adamant/Modules/Chat/ViewModel/Models/ChatSender.swift diff --git a/Adamant/Stories/Chat/ViewModel/Models/ChatStartPosition.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatStartPosition.swift similarity index 100% rename from Adamant/Stories/Chat/ViewModel/Models/ChatStartPosition.swift rename to Adamant/Modules/Chat/ViewModel/Models/ChatStartPosition.swift diff --git a/Adamant/Stories/Chat/ViewModel/Models/MessageModel.swift b/Adamant/Modules/Chat/ViewModel/Models/MessageModel.swift similarity index 100% rename from Adamant/Stories/Chat/ViewModel/Models/MessageModel.swift rename to Adamant/Modules/Chat/ViewModel/Models/MessageModel.swift diff --git a/Adamant/Modules/ChatsList/ChatListFactory.swift b/Adamant/Modules/ChatsList/ChatListFactory.swift new file mode 100644 index 000000000..5cb8d9d51 --- /dev/null +++ b/Adamant/Modules/ChatsList/ChatListFactory.swift @@ -0,0 +1,63 @@ +// +// ChatListFactory.swift +// Adamant +// +// Created by Anokhov Pavel on 12.01.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Swinject + +struct ChatListFactory { + let assembler: Assembler + + func makeChatListVC(screensFactory: ScreensFactory) -> UIViewController { + let c = ChatListViewController(nibName: "ChatListViewController", bundle: nil) + c.accountService = assembler.resolve(AccountService.self) + c.chatsProvider = assembler.resolve(ChatsProvider.self) + c.transfersProvider = assembler.resolve(TransfersProvider.self) + c.screensFactory = screensFactory + c.notificationsService = assembler.resolve(NotificationsService.self) + c.dialogService = assembler.resolve(DialogService.self) + c.addressBook = assembler.resolve(AddressBookService.self) + c.avatarService = assembler.resolve(AvatarService.self) + + // MARK: RichMessage handlers + // Transfer handlers from accountService' wallet services + if let accountService = assembler.resolve(AccountService.self) { + for case let provider as RichMessageProvider in accountService.wallets { + c.richMessageProviders[provider.dynamicRichMessageType] = provider + } + } + + return c + } + + func makeNewChatVC(screensFactory: ScreensFactory) -> NewChatViewController { + let c = NewChatViewController() + c.dialogService = assembler.resolve(DialogService.self) + c.accountService = assembler.resolve(AccountService.self) + c.accountsProvider = assembler.resolve(AccountsProvider.self) + c.screensFactory = screensFactory + return c + } + + func makeComplexTransferVC(screensFactory: ScreensFactory) -> UIViewController { + let c = ComplexTransferViewController() + c.accountService = assembler.resolve(AccountService.self) + c.visibleWalletsService = assembler.resolve(VisibleWalletsService.self) + c.addressBookService = assembler.resolve(AddressBookService.self) + c.screensFactory = screensFactory + return c + } + + func makeSearchResultsViewController(screensFactory: ScreensFactory) -> SearchResultsViewController { + SearchResultsViewController( + screensFactory: screensFactory, + avatarService: assembler.resolve(AvatarService.self)!, + addressBookService: assembler.resolve(AddressBookService.self)!, + accountsProvider: assembler.resolve(AccountsProvider.self)! + ) + } +} diff --git a/Adamant/Stories/ChatsList/ChatListViewController.swift b/Adamant/Modules/ChatsList/ChatListViewController.swift similarity index 94% rename from Adamant/Stories/ChatsList/ChatListViewController.swift rename to Adamant/Modules/ChatsList/ChatListViewController.swift index 691dfc6ee..f92838381 100644 --- a/Adamant/Stories/ChatsList/ChatListViewController.swift +++ b/Adamant/Modules/ChatsList/ChatListViewController.swift @@ -30,7 +30,7 @@ extension String.adamant { } } -class ChatListViewController: KeyboardObservingViewController { +final class ChatListViewController: KeyboardObservingViewController { typealias SpinnerCell = TableCellWrapper let cellIdentifier = "cell" @@ -41,7 +41,7 @@ class ChatListViewController: KeyboardObservingViewController { var accountService: AccountService! var chatsProvider: ChatsProvider! var transfersProvider: TransfersProvider! - var router: Router! + var screensFactory: ScreensFactory! var notificationsService: NotificationsService! var dialogService: DialogService! var addressBook: AddressBookService! @@ -58,6 +58,7 @@ class ChatListViewController: KeyboardObservingViewController { var unreadController: NSFetchedResultsController? var searchController: UISearchController? + private var transactionsRequiringBalanceUpdate: [String] = [] private var preservedMessagess = [String:String]() let defaultAvatar = UIImage.asset(named: "avatar-chat-placeholder") ?? .init() @@ -189,11 +190,7 @@ class ChatListViewController: KeyboardObservingViewController { } loadNewChatTask?.cancel() - - guard let searchResultController = router.get(scene: AdamantScene.Chats.searchResults) as? SearchResultsViewController else { - fatalError("Can't get SearchResultsViewController") - } - + let searchResultController = screensFactory.makeSearchResults() searchResultController.delegate = self searchController = UISearchController(searchResultsController: searchResultController) @@ -303,11 +300,8 @@ class ChatListViewController: KeyboardObservingViewController { // MARK: IB Actions @IBAction func newChat(sender: Any) { - let controller = router.get(scene: AdamantScene.Chats.newChat) - - if let c = controller as? NewChatViewController { - c.delegate = self - } + let controller = screensFactory.makeNewChat() + controller.delegate = self if let split = splitViewController { let nav = UINavigationController(rootViewController: controller) @@ -318,11 +312,11 @@ class ChatListViewController: KeyboardObservingViewController { } // MARK: Helpers - func chatViewController(for chatroom: Chatroom, with messageId: String? = nil) -> ChatViewController { - guard let vc = router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { - fatalError("Can't get ChatViewController") - } - + func chatViewController( + for chatroom: Chatroom, + with messageId: String? = nil + ) -> ChatViewController { + let vc = screensFactory.makeChat() vc.hidesBottomBarWhenPushed = true vc.viewModel.setup( account: accountService.account, @@ -676,21 +670,39 @@ extension ChatListViewController: NSFetchedResultsControllerDelegate { } // MARK: Unread controller + case let c where c == unreadController: - guard type == .insert else { - break + guard let transaction = anObject as? ChatTransaction else { break } + + if self.view.window == nil, + type == .insert { + showNotification(for: transaction) } - if let transaction = anObject as? ChatTransaction { - if self.view.window == nil { - showNotification(for: transaction) - } + let shouldForceUpdate = anObject is TransferTransaction + || anObject is RichMessageTransaction + + if shouldForceUpdate, type == .insert { + transactionsRequiringBalanceUpdate.append(transaction.txId) } - if let _ = anObject as? TransferTransaction { - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - NotificationCenter.default.post(name: .AdamantAccountService.forceUpdateBalance, object: nil) - } + + guard shouldForceUpdate, + let blockId = transaction.blockId, + !blockId.isEmpty, + transactionsRequiringBalanceUpdate.contains(transaction.txId) + else { + break + } + + if let index = transactionsRequiringBalanceUpdate.firstIndex(of: transaction.txId) { + transactionsRequiringBalanceUpdate.remove(at: index) } + + NotificationCenter.default.post( + name: .AdamantAccountService.forceUpdateBalance, + object: nil + ) + default: break } @@ -781,33 +793,45 @@ extension ChatListViewController { } // MARK: 1. Show notification only for incomming transactions - guard !transaction.silentNotification, !transaction.isOutgoing, - let chatroom = transaction.chatroom, chatroom != presentedChatroom(), !chatroom.isHidden, - let partner = chatroom.partner else { + guard !transaction.silentNotification, + !transaction.isOutgoing, + let chatroom = transaction.chatroom, + chatroom != presentedChatroom(), + !chatroom.isHidden, + let partner = chatroom.partner, + let address = partner.address + else { return } // MARK: 2. Prepare notification - let title = partner.name ?? partner.address + + let name: String? = partner.name ?? addressBook.getName(for: address) + let title = name ?? partner.address let text = shortDescription(for: transaction) let image: UIImage if let ava = partner.avatar, let img = UIImage.asset(named: ava) { image = img + } else if let publicKey = partner.publicKey { + image = avatarService.avatar(for: publicKey, size: 30) } else { image = defaultAvatar } // MARK: 4. Show notification with tap handler - dialogService.showNotification(title: title?.checkAndReplaceSystemWallets(), message: text?.string, image: image) { [weak self] in - DispatchQueue.main.async { - self?.presentChatroom(chatroom) - } + dialogService.showNotification( + title: title?.checkAndReplaceSystemWallets(), + message: text?.string, + image: image + ) { [weak self] in + self?.presentChatroom(chatroom) } } } - private func presentChatroom(_ chatroom: Chatroom, with message: MessageTransaction? = nil) { + @MainActor + func presentChatroom(_ chatroom: Chatroom, with message: MessageTransaction? = nil) { // MARK: 1. Create and config ViewController let vc = chatViewController(for: chatroom, with: message?.transactionId) diff --git a/Adamant/Stories/ChatsList/ChatListViewController.xib b/Adamant/Modules/ChatsList/ChatListViewController.xib similarity index 100% rename from Adamant/Stories/ChatsList/ChatListViewController.xib rename to Adamant/Modules/ChatsList/ChatListViewController.xib diff --git a/Adamant/Stories/ChatsList/ChatTableViewCell.swift b/Adamant/Modules/ChatsList/ChatTableViewCell.swift similarity index 97% rename from Adamant/Stories/ChatsList/ChatTableViewCell.swift rename to Adamant/Modules/ChatsList/ChatTableViewCell.swift index d1ca901c5..5119ace32 100644 --- a/Adamant/Stories/ChatsList/ChatTableViewCell.swift +++ b/Adamant/Modules/ChatsList/ChatTableViewCell.swift @@ -10,7 +10,7 @@ import UIKit import FreakingSimpleRoundImageView import CommonKit -class ChatTableViewCell: UITableViewCell { +final class ChatTableViewCell: UITableViewCell { static var defaultAvatar: UIImage = .asset(named: "avatar-chat-placeholder") ?? .init() static let shortDescriptionTextSize: CGFloat = 15.0 diff --git a/Adamant/Stories/ChatsList/ChatTableViewCell.xib b/Adamant/Modules/ChatsList/ChatTableViewCell.xib similarity index 100% rename from Adamant/Stories/ChatsList/ChatTableViewCell.xib rename to Adamant/Modules/ChatsList/ChatTableViewCell.xib diff --git a/Adamant/Stories/ChatsList/ComplexTransferViewController.swift b/Adamant/Modules/ChatsList/ComplexTransferViewController.swift similarity index 96% rename from Adamant/Stories/ChatsList/ComplexTransferViewController.swift rename to Adamant/Modules/ChatsList/ComplexTransferViewController.swift index 27576290f..879eec89f 100644 --- a/Adamant/Stories/ChatsList/ComplexTransferViewController.swift +++ b/Adamant/Modules/ChatsList/ComplexTransferViewController.swift @@ -16,12 +16,13 @@ protocol ComplexTransferViewControllerDelegate: AnyObject { func complexTransferViewController(_ viewController: ComplexTransferViewController, didFinishWithTransfer: TransactionDetails?, detailsViewController: UIViewController?) } -class ComplexTransferViewController: UIViewController { +final class ComplexTransferViewController: UIViewController { // MARK: - Dependencies var accountService: AccountService! var visibleWalletsService: VisibleWalletsService! var addressBookService: AddressBookService! + var screensFactory: ScreensFactory! // MARK: - Properties var pagingViewController: PagingViewController! @@ -96,7 +97,7 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { func pagingViewController(_ pagingViewController: PagingViewController, viewControllerAt index: Int) -> UIViewController { let service = services[index] - let vc = service.transferViewController() + let vc = screensFactory.makeTransferVC(service: service) guard let v = vc as? TransferViewControllerBase else { return vc } @@ -166,7 +167,7 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { if ERC20Token.supportedTokens.contains(where: { token in return token.symbol == service.tokenSymbol }) { - network = service.tokenNetworkSymbol + network = type(of: service).tokenNetworkSymbol } let item = WalletPagingItem( diff --git a/Adamant/Stories/ChatsList/NewChatViewController.swift b/Adamant/Modules/ChatsList/NewChatViewController.swift similarity index 96% rename from Adamant/Stories/ChatsList/NewChatViewController.swift rename to Adamant/Modules/ChatsList/NewChatViewController.swift index 4b040de96..5e41c6e96 100644 --- a/Adamant/Stories/ChatsList/NewChatViewController.swift +++ b/Adamant/Modules/ChatsList/NewChatViewController.swift @@ -43,7 +43,7 @@ protocol NewChatViewControllerDelegate: AnyObject { } // MARK: - -class NewChatViewController: FormViewController { +final class NewChatViewController: FormViewController { static let faqUrl = "https://medium.com/adamant-im/chats-and-uninitialized-accounts-in-adamant-5035438e2fcd" private enum Rows { @@ -77,7 +77,7 @@ class NewChatViewController: FormViewController { var dialogService: DialogService! var accountService: AccountService! var accountsProvider: AccountsProvider! - var router: Router! + var screensFactory: ScreensFactory! // MARK: Properties private var skipValueChange = false @@ -191,21 +191,27 @@ class NewChatViewController: FormViewController { }.cellUpdate { (cell, _) in cell.textLabel?.textColor = UIColor.adamant.primary }.onCellSelection { [weak self] (_, _) in - let encodedAddress = AdamantUriTools.encode(request: AdamantUri.address(address: address, params: nil)) + guard let self = self else { return } + let encodedAddress = AdamantUriTools.encode(request: AdamantUri.address( + address: address, + params: nil + )) + switch AdamantQRTools.generateQrFrom(string: encodedAddress, withLogo: true) { case .success(let qr): - guard let vc = self?.router.get(scene: AdamantScene.Shared.shareQr) as? ShareQrViewController else { - fatalError("Can't find ShareQrViewController") - } - + let vc = screensFactory.makeShareQr() vc.qrCode = qr vc.sharingTip = address vc.excludedActivityTypes = ShareContentType.address.excludedActivityTypes vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) + present(vc, animated: true, completion: nil) case .failure(error: let error): - self?.dialogService.showError(withMessage: error.localizedDescription, supportEmail: true, error: error) + dialogService.showError( + withMessage: error.localizedDescription, + supportEmail: true, + error: error + ) } } diff --git a/Adamant/Stories/ChatsList/SearchResultsViewController.swift b/Adamant/Modules/ChatsList/SearchResultsViewController.swift similarity index 98% rename from Adamant/Stories/ChatsList/SearchResultsViewController.swift rename to Adamant/Modules/ChatsList/SearchResultsViewController.swift index f796b0b44..76635c830 100644 --- a/Adamant/Stories/ChatsList/SearchResultsViewController.swift +++ b/Adamant/Modules/ChatsList/SearchResultsViewController.swift @@ -26,10 +26,10 @@ protocol SearchResultDelegate: AnyObject { func didSelected(_ account: CoreDataAccount) } -class SearchResultsViewController: UITableViewController { +final class SearchResultsViewController: UITableViewController { // MARK: - Dependencies - let router: Router + let screensFactory: ScreensFactory let avatarService: AvatarService let addressBookService: AddressBookService let accountsProvider: AccountsProvider @@ -47,12 +47,12 @@ class SearchResultsViewController: UITableViewController { // MARK: Init init( - router: Router, + screensFactory: ScreensFactory, avatarService: AvatarService, addressBookService: AddressBookService, accountsProvider: AccountsProvider ) { - self.router = router + self.screensFactory = screensFactory self.avatarService = avatarService self.addressBookService = addressBookService self.accountsProvider = accountsProvider diff --git a/Adamant/Stories/ChatsList/SearchResultsViewController.xib b/Adamant/Modules/ChatsList/SearchResultsViewController.xib similarity index 100% rename from Adamant/Stories/ChatsList/SearchResultsViewController.xib rename to Adamant/Modules/ChatsList/SearchResultsViewController.xib diff --git a/Adamant/Modules/CoinsNodesList/CoinsNodesListFactory.swift b/Adamant/Modules/CoinsNodesList/CoinsNodesListFactory.swift new file mode 100644 index 000000000..2e230fc37 --- /dev/null +++ b/Adamant/Modules/CoinsNodesList/CoinsNodesListFactory.swift @@ -0,0 +1,62 @@ +// +// CoinsNodesListFactory.swift +// Adamant +// +// Created by Andrew G on 20.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Swinject +import SwiftUI +import CommonKit + +enum CoinsNodesListContext { + case login + case menu +} + +struct CoinsNodesListFactory { + private let assembler: Assembler + + init(parent: Assembler) { + assembler = .init([CoinsNodesListAssembly()], parent: parent) + } + + func makeViewController(context: CoinsNodesListContext) -> UIViewController { + let viewModel = assembler.resolve(CoinsNodesListViewModel.self)! + let view = CoinsNodesListView(viewModel: viewModel) + + switch context { + case .login: + return SelfRemovableHostingController(rootView: view) + case .menu: + return UIHostingController(rootView: view) + } + } +} + +private struct CoinsNodesListAssembly: Assembly { + func assemble(container: Container) { + container.register(CoinsNodesListViewModel.self) { + let processedGroups = Set(NodeGroup.allCases).subtracting([.adm]) + + return .init( + mapper: .init(processedGroups: processedGroups), + nodesStorage: $0.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: $0.resolve( + NodesAdditionalParamsStorageProtocol.self + )!, + processedGroups: processedGroups, + apiServices: .init( + btc: $0.resolve(BtcApiService.self)!, + eth: $0.resolve(EthApiService.self)!, + lskNode: $0.resolve(LskNodeApiService.self)!, + lskService: $0.resolve(LskServiceApiService.self)!, + doge: $0.resolve(DogeApiService.self)!, + dash: $0.resolve(DashApiService.self)!, + adm: $0.resolve(ApiService.self)! + ) + ) + }.inObjectScope(.weak) + } +} diff --git a/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView+Row.swift b/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView+Row.swift new file mode 100644 index 000000000..00973fbb1 --- /dev/null +++ b/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView+Row.swift @@ -0,0 +1,73 @@ +// +// CoinsNodesListView+Row.swift +// Adamant +// +// Created by Andrew G on 20.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import SwiftUI +import CommonKit + +extension CoinsNodesListView { + struct Row: View { + let model: CoinsNodesListState.Section.Row + let setIsEnabled: (Bool) -> Void + + var body: some View { + HStack(spacing: 10) { + CheckmarkView( + isEnabled: model.isEnabled, + setIsEnabled: setIsEnabled + ) + .frame(squareSize: 24) + .animation(.easeInOut(duration: 0.1), value: model.isEnabled) + + VStack(alignment: .leading, spacing: 4) { + Text(model.title).font(titleFont).lineLimit(1) + + HStack(spacing: 6) { + Text(model.connectionStatus).font(captionFont) + + Text(model.description).font(subtitleFont) + .lineLimit(1) + .frame(height: 10) + } + } + }.padding(2) + } + } +} + +private extension CoinsNodesListView.Row { + struct CheckmarkView: View { + let isEnabled: Bool + let setIsEnabled: (Bool) -> Void + + var body: some View { + ZStack { + if isEnabled { + Image(uiImage: .asset(named: "status_success") ?? .strokedCheckmark) + .resizable() + .scaledToFit() + .transition(.scale) + } + + if !isEnabled { + Circle().strokeBorder( + Color(uiColor: .adamant.secondary), + lineWidth: 1 + ) + } + } + .contentShape(Rectangle()) + .onTapGesture { + setIsEnabled(!isEnabled) + } + } + } +} + +private let titleFont = Font.system(size: 17, weight: .regular) +private let subtitleFont = Font(UIFont.preferredFont(forTextStyle: .caption1)) +private let captionFont = Font.system(size: 12, weight: .regular) diff --git a/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView.swift b/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView.swift new file mode 100644 index 000000000..0f20bb046 --- /dev/null +++ b/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView.swift @@ -0,0 +1,80 @@ +// +// CoinsNodesListView.swift +// Adamant +// +// Created by Andrew G on 20.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import SwiftUI +import CommonKit + +struct CoinsNodesListView: View { + @StateObject private var viewModel: CoinsNodesListViewModel + + var body: some View { + List { + ForEach(viewModel.state.sections, content: makeSection) + makeFastestNodeModeSection() + makeResetSection() + } + .listStyle(.insetGrouped) + .withoutListBackground() + .background(Color(.adamant.secondBackgroundColor)) + .alert( + String.adamant.coinsNodesList.resetAlert, + isPresented: $viewModel.state.isAlertShown + ) { + Button(String.adamant.alert.cancel, role: .cancel) {} + Button(String.adamant.coinsNodesList.reset) { viewModel.reset() } + } + .navigationTitle(String.adamant.coinsNodesList.title) + } + + init(viewModel: CoinsNodesListViewModel) { + _viewModel = .init(wrappedValue: viewModel) + } +} + +private extension CoinsNodesListView { + func makeSection(_ model: CoinsNodesListState.Section) -> some View { + Section( + header: Text(model.title), + content: { + ForEach(model.rows) { row in + Row( + model: row, + setIsEnabled: { viewModel.setIsEnabled(id: row.id, value: $0) } + ).listRowBackground(Color(uiColor: .adamant.cellColor)) + } + } + ) + } + + func makeFastestNodeModeSection() -> some View { + Section( + content: { + Toggle( + String.adamant.coinsNodesList.preferTheFastestNode, + isOn: $viewModel.state.fastestNodeMode + ) + .listRowBackground(Color(uiColor: .adamant.cellColor)) + .tint(Color(uiColor: .adamant.active)) + }, + footer: { Text(String.adamant.coinsNodesList.fastestNodeTip) } + ) + } + + func makeResetSection() -> some View { + Section { + Button(action: showResetAlert) { + Text(String.adamant.coinsNodesList.reset) + .expanded(axes: .horizontal) + }.listRowBackground(Color(uiColor: .adamant.cellColor)) + } + } + + func showResetAlert() { + viewModel.state.isAlertShown = true + } +} diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListMapper.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListMapper.swift new file mode 100644 index 000000000..7d813038e --- /dev/null +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListMapper.swift @@ -0,0 +1,70 @@ +// +// CoinsNodesListMapper.swift +// Adamant +// +// Created by Andrew G on 20.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import SwiftUI + +struct CoinsNodesListMapper { + let processedGroups: Set + + func map(items: [NodeWithGroup], restNodeIds: [UUID]) -> [CoinsNodesListState.Section] { + var nodesDict = [NodeGroup: [Node]]() + + items.forEach { item in + guard processedGroups.contains(item.group) else { return } + + if nodesDict[item.group] == nil { + nodesDict[item.group] = [item.node] + } else { + nodesDict[item.group]?.append(item.node) + } + } + + return nodesDict.keys.map { + map( + group: $0, + nodes: nodesDict[$0] ?? .init(), + restNodeIds: restNodeIds + ) + }.sorted { $0.title < $1.title } + } +} + +private extension CoinsNodesListMapper { + func map( + group: NodeGroup, + nodes: [Node], + restNodeIds: [UUID] + ) -> CoinsNodesListState.Section { + .init( + id: group, + title: group.name, + rows: nodes.map { + map(node: $0, restNodeIds: restNodeIds) + } + ) + } + + func map(node: Node, restNodeIds: [UUID]) -> CoinsNodesListState.Section.Row { + let indicatorString = node.indicatorString( + isRest: restNodeIds.contains(node.id), + isWs: false + ) + + var indicatorAttrString = AttributedString(stringLiteral: indicatorString) + indicatorAttrString.foregroundColor = .init(uiColor: node.indicatorColor) + + return .init( + id: node.id, + isEnabled: node.isEnabled, + title: node.asString(), + connectionStatus: indicatorAttrString, + description: node.statusString(showVersion: false) ?? .empty + ) + } +} diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListState.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListState.swift new file mode 100644 index 000000000..9a868bbf3 --- /dev/null +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListState.swift @@ -0,0 +1,40 @@ +// +// CoinsNodesListState.swift +// Adamant +// +// Created by Andrew G on 20.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +struct CoinsNodesListState: Equatable { + var sections: [Section] + var fastestNodeMode: Bool + var isAlertShown: Bool + + static let `default` = Self( + sections: .init(), + fastestNodeMode: false, + isAlertShown: false + ) +} + +extension CoinsNodesListState { + struct Section: Equatable, Identifiable { + let id: NodeGroup + let title: String + let rows: [Row] + } +} + +extension CoinsNodesListState.Section { + struct Row: Equatable, Identifiable { + let id: UUID + let isEnabled: Bool + let title: String + let connectionStatus: AttributedString + let description: String + } +} diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListStrings.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListStrings.swift new file mode 100644 index 000000000..d65bb3fa0 --- /dev/null +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListStrings.swift @@ -0,0 +1,28 @@ +// +// CoinsNodesListStrings.swift +// Adamant +// +// Created by Andrew G on 20.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit + +extension String.adamant { + enum coinsNodesList { + static let title = String.localized("CoinsNodesList.Title", comment: .empty) + static let serviceNode = String.localized("CoinsNodesList.ServiceNode", comment: .empty) + static let reset = String.localized("NodesList.ResetButton", comment: .empty) + static let resetAlert = String.localized("NodesList.ResetNodeListAlert", comment: .empty) + + static let preferTheFastestNode = String.localized( + "NodesList.PreferTheFastestNode", + comment: .empty + ) + + static let fastestNodeTip = String.localized( + "NodesList.PreferTheFastestNode.Footer", + comment: .empty + ) + } +} diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel+ApiServices.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel+ApiServices.swift new file mode 100644 index 000000000..53bc5f309 --- /dev/null +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel+ApiServices.swift @@ -0,0 +1,42 @@ +// +// CoinsNodesListViewModel+Wallets.swift +// Adamant +// +// Created by Andrew G on 20.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit + +extension CoinsNodesListViewModel { + struct ApiServices { + let btc: WalletApiService + let eth: WalletApiService + let lskNode: WalletApiService + let lskService: WalletApiService + let doge: WalletApiService + let dash: WalletApiService + let adm: WalletApiService + } +} + +extension CoinsNodesListViewModel.ApiServices { + func getApiService(group: NodeGroup) -> WalletApiService { + switch group { + case .btc: + return btc + case .eth: + return eth + case .lskNode: + return lskNode + case .lskService: + return lskService + case .doge: + return doge + case .dash: + return dash + case .adm: + return adm + } + } +} diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel.swift new file mode 100644 index 000000000..d9f126b7b --- /dev/null +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel.swift @@ -0,0 +1,100 @@ +// +// CoinsNodesListViewModel.swift +// Adamant +// +// Created by Andrew G on 20.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import SwiftUI +import Combine +import CommonKit + +@MainActor +final class CoinsNodesListViewModel: ObservableObject { + @Published var state: CoinsNodesListState = .default + + private let mapper: CoinsNodesListMapper + private let nodesStorage: NodesStorageProtocol + private let nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol + private let processedGroups: Set + private let apiServices: ApiServices + private var subscriptions = Set() + + nonisolated init( + mapper: CoinsNodesListMapper, + nodesStorage: NodesStorageProtocol, + nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol, + processedGroups: Set, + apiServices: ApiServices + ) { + self.mapper = mapper + self.nodesStorage = nodesStorage + self.nodesAdditionalParamsStorage = nodesAdditionalParamsStorage + self.processedGroups = processedGroups + self.apiServices = apiServices + Task { @MainActor in setup() } + } + + func setIsEnabled(id: UUID, value: Bool) { + nodesStorage.updateNodeParams(id: id, isEnabled: value) + } + + func reset() { + processedGroups.forEach { + nodesStorage.resetNodes(group: $0) + } + } +} + +private extension CoinsNodesListViewModel { + func setup() { + state.fastestNodeMode = processedGroups + .map { nodesAdditionalParamsStorage.isFastestNodeMode(group: $0) } + .reduce(into: true) { $0 = $0 && $1 } + + $state + .map(\.fastestNodeMode) + .removeDuplicates() + .sink { [weak self] in self?.saveFastestNodeMode($0) } + .store(in: &subscriptions) + + guard let someGroup = processedGroups.first else { return } + + nodesStorage.nodesWithGroupsPublisher + .combineLatest(nodesAdditionalParamsStorage.fastestNodeMode(group: someGroup)) + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.updateSections(items: $0.0) } + .store(in: &subscriptions) + + Timer + .publish(every: someGroup.onScreenUpdateInterval, on: .main, in: .default) + .autoconnect() + .sink { [weak self] _ in self?.healthCheck() } + .store(in: &subscriptions) + + healthCheck() + } + + func updateSections(items: [NodeWithGroup]) { + state.sections = mapper.map( + items: items, + restNodeIds: processedGroups.flatMap { + apiServices.getApiService(group: $0).preferredNodeIds + } + ) + } + + func saveFastestNodeMode(_ value: Bool) { + nodesAdditionalParamsStorage.setFastestNodeMode( + groups: processedGroups, + value: value + ) + } + + func healthCheck() { + processedGroups.forEach { + apiServices.getApiService(group: $0).healthCheck() + } + } +} diff --git a/Adamant/Stories/Delegates/AdamantDelegateCell.swift b/Adamant/Modules/Delegates/AdamantDelegateCell.swift similarity index 98% rename from Adamant/Stories/Delegates/AdamantDelegateCell.swift rename to Adamant/Modules/Delegates/AdamantDelegateCell.swift index e06e87b75..4cadf52a5 100644 --- a/Adamant/Stories/Delegates/AdamantDelegateCell.swift +++ b/Adamant/Modules/Delegates/AdamantDelegateCell.swift @@ -15,7 +15,7 @@ protocol AdamantDelegateCellDelegate: AnyObject { } // MARK: - -class AdamantDelegateCell: UITableViewCell { +final class AdamantDelegateCell: UITableViewCell { private let checkmarkRowView = CheckmarkRowView() weak var delegate: AdamantDelegateCellDelegate? { diff --git a/Adamant/Stories/Delegates/DelegateDetailsViewController.swift b/Adamant/Modules/Delegates/DelegateDetailsViewController.swift similarity index 86% rename from Adamant/Stories/Delegates/DelegateDetailsViewController.swift rename to Adamant/Modules/Delegates/DelegateDetailsViewController.swift index e8cf20889..58d85dc2a 100644 --- a/Adamant/Stories/Delegates/DelegateDetailsViewController.swift +++ b/Adamant/Modules/Delegates/DelegateDetailsViewController.swift @@ -19,7 +19,7 @@ extension String.adamant { } // MARK: - -class DelegateDetailsViewController: UIViewController { +final class DelegateDetailsViewController: UIViewController { // MARK: - Rows fileprivate enum Row: Int { @@ -276,46 +276,42 @@ extension DelegateDetailsViewController { extension DelegateDetailsViewController { private func refreshData(with delegate: Delegate) { Task { - await apiService.getForgedByAccount(publicKey: delegate.publicKey) { [weak self] result in - switch result { - case .success(let details): - self?.forged = details.forged - - Task { @MainActor in - guard let tableView = self?.tableView else { - return - } - - let indexPath = Row.forged.indexPathFor(section: 0) - tableView.reloadRows(at: [indexPath], with: .none) - } - case .failure(let error): - self?.apiServiceFailed(with: error) + let result = await apiService.getForgedByAccount(publicKey: delegate.publicKey) + + switch result { + case .success(let details): + forged = details.forged + + guard let tableView = tableView else { + return } + + let indexPath = Row.forged.indexPathFor(section: 0) + tableView.reloadRows(at: [indexPath], with: .none) + case .failure(let error): + apiServiceFailed(with: error) } // Get forging time - await apiService.getForgingTime(for: delegate) { [weak self] result in - switch result { - case .success(let seconds): - if seconds >= 0 { - self?.forgingTime = TimeInterval(exactly: seconds) - } else { - self?.forgingTime = nil - } - - Task { @MainActor in - guard let tableView = self?.tableView else { - return - } - - let indexPath = Row.forgingTime.indexPathFor(section: 0) - tableView.reloadRows(at: [indexPath], with: .none) - } - - case .failure(let error): - self?.apiServiceFailed(with: error) + let forgingTimeResult = await apiService.getForgingTime(for: delegate) + + switch forgingTimeResult { + case .success(let seconds): + if seconds >= 0 { + forgingTime = TimeInterval(exactly: seconds) + } else { + forgingTime = nil } + + guard let tableView = tableView else { + return + } + + let indexPath = Row.forgingTime.indexPathFor(section: 0) + tableView.reloadRows(at: [indexPath], with: .none) + + case .failure(let error): + apiServiceFailed(with: error) } } } diff --git a/Adamant/Stories/Delegates/DelegateDetailsViewController.xib b/Adamant/Modules/Delegates/DelegateDetailsViewController.xib similarity index 100% rename from Adamant/Stories/Delegates/DelegateDetailsViewController.xib rename to Adamant/Modules/Delegates/DelegateDetailsViewController.xib diff --git a/Adamant/Stories/Delegates/DelegatesBottomPanel/DelegatesBottomPanel+Model.swift b/Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel+Model.swift similarity index 100% rename from Adamant/Stories/Delegates/DelegatesBottomPanel/DelegatesBottomPanel+Model.swift rename to Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel+Model.swift diff --git a/Adamant/Stories/Delegates/DelegatesBottomPanel/DelegatesBottomPanel.swift b/Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel.swift similarity index 100% rename from Adamant/Stories/Delegates/DelegatesBottomPanel/DelegatesBottomPanel.swift rename to Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel.swift diff --git a/Adamant/Modules/Delegates/DelegatesFactory.swift b/Adamant/Modules/Delegates/DelegatesFactory.swift new file mode 100644 index 000000000..f079897ab --- /dev/null +++ b/Adamant/Modules/Delegates/DelegatesFactory.swift @@ -0,0 +1,31 @@ +// +// DelegatesFactory.swift +// Adamant +// +// Created by Anton Boyarkin on 06/07/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Swinject + +struct DelegatesFactory { + let assembler: Assembler + + func makeDelegatesListVC(screensFactory: ScreensFactory) -> UIViewController { + DelegatesListViewController( + apiService: assembler.resolve(ApiService.self)!, + accountService: assembler.resolve(AccountService.self)!, + dialogService: assembler.resolve(DialogService.self)!, + screensFactory: screensFactory + ) + } + + func makeDelegateDetails() -> DelegateDetailsViewController { + let c = DelegateDetailsViewController(nibName: "DelegateDetailsViewController", bundle: nil) + c.apiService = assembler.resolve(ApiService.self) + c.accountService = assembler.resolve(AccountService.self) + c.dialogService = assembler.resolve(DialogService.self) + return c + } +} diff --git a/Adamant/Stories/Delegates/DelegatesListViewController.swift b/Adamant/Modules/Delegates/DelegatesListViewController.swift similarity index 84% rename from Adamant/Stories/Delegates/DelegatesListViewController.swift rename to Adamant/Modules/Delegates/DelegatesListViewController.swift index b3927d9ca..c6b1eb46d 100644 --- a/Adamant/Stories/Delegates/DelegatesListViewController.swift +++ b/Adamant/Modules/Delegates/DelegatesListViewController.swift @@ -42,7 +42,7 @@ final class DelegatesListViewController: KeyboardObservingViewController { private let apiService: ApiService private let accountService: AccountService private let dialogService: DialogService - private let router: Router + private let screensFactory: ScreensFactory // MARK: - Constants @@ -99,12 +99,12 @@ final class DelegatesListViewController: KeyboardObservingViewController { apiService: ApiService, accountService: AccountService, dialogService: DialogService, - router: Router + screensFactory: ScreensFactory ) { self.apiService = apiService self.accountService = accountService self.dialogService = dialogService - self.router = router + self.screensFactory = screensFactory super.init(nibName: nil, bundle: nil) } @@ -138,37 +138,33 @@ final class DelegatesListViewController: KeyboardObservingViewController { } Task { - await apiService.getDelegatesWithVotes( + let result = await apiService.getDelegatesWithVotes( for: address, limit: activeDelegates - ) { result in - Task { @MainActor [weak self] in - guard let self = self else { return } - - switch result { - case .success(let delegates): - let checkedNames = self.delegates - .filter { $0.isChecked } - .map { $0.delegate.username } - - let checkedDelegates = delegates.map { CheckedDelegate(delegate: $0) } - for name in checkedNames { - if let i = delegates.firstIndex(where: { $0.username == name }) { - checkedDelegates[i].isChecked = true - } - } - - self.delegates = checkedDelegates - self.tableView.reloadData() - case .failure(let error): - self.dialogService.showRichError(error: error) + ) + + switch result { + case .success(let delegates): + let checkedNames = self.delegates + .filter { $0.isChecked } + .map { $0.delegate.username } + + let checkedDelegates = delegates.map { CheckedDelegate(delegate: $0) } + for name in checkedNames { + if let i = delegates.firstIndex(where: { $0.username == name }) { + checkedDelegates[i].isChecked = true } - - refreshControl.endRefreshing() - self.updateVotePanel() - self.removeLoadingView() } + + self.delegates = checkedDelegates + self.tableView.reloadData() + case .failure(let error): + self.dialogService.showRichError(error: error) } + + refreshControl.endRefreshing() + self.updateVotePanel() + self.removeLoadingView() } } @@ -239,10 +235,7 @@ extension DelegatesListViewController: UITableViewDataSource, UITableViewDelegat } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let controller = router.get(scene: AdamantScene.Delegates.delegateDetails) as? DelegateDetailsViewController else { - return - } - + let controller = screensFactory.makeDelegateDetails() controller.delegate = checkedDelegateFor(indexPath: indexPath).delegate navigationController?.pushViewController(controller, animated: true) @@ -321,32 +314,30 @@ private extension DelegatesListViewController { dialogService.showProgress(withMessage: nil, userInteractionEnable: false) Task { - await apiService.voteForDelegates( + let result = await apiService.voteForDelegates( from: account.address, keypair: keypair, votes: votes - ) { result in - Task { @MainActor [weak self] in - self?.dialogService.dismissProgress() - - switch result { - case .success: - self?.dialogService.showSuccess(withMessage: String.adamant.delegates.success) - - checkedDelegates.forEach { - $1.isChecked = false - $1.delegate.voted = !$1.delegate.voted - $1.isUpdating = true - } - - self?.tableView.reloadData() - self?.updateVotePanel() - self?.scheduleUpdate() + ) + + dialogService.dismissProgress() + + switch result { + case .success: + dialogService.showSuccess(withMessage: String.adamant.delegates.success) - case .failure(let error): - self?.dialogService.showRichError(error: TransfersProviderError.serverError(error)) - } + checkedDelegates.forEach { + $1.isChecked = false + $1.delegate.voted = !$1.delegate.voted + $1.isUpdating = true } + + tableView.reloadData() + updateVotePanel() + scheduleUpdate() + + case .failure(let error): + dialogService.showRichError(error: TransfersProviderError.serverError(error)) } } } diff --git a/Adamant/Stories/Login/EurekaPassphraseRow.swift b/Adamant/Modules/Login/EurekaPassphraseRow.swift similarity index 100% rename from Adamant/Stories/Login/EurekaPassphraseRow.swift rename to Adamant/Modules/Login/EurekaPassphraseRow.swift diff --git a/Adamant/Modules/Login/LoginFactory.swift b/Adamant/Modules/Login/LoginFactory.swift new file mode 100644 index 000000000..dd1fb2c78 --- /dev/null +++ b/Adamant/Modules/Login/LoginFactory.swift @@ -0,0 +1,25 @@ +// +// LoginFactory.swift +// Adamant +// +// Created by Anokhov Pavel on 07.01.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Swinject + +struct LoginFactory { + let assembler: Assembler + + func makeViewController(screenFactory: ScreensFactory) -> LoginViewController { + LoginViewController( + accountService: assembler.resolve(AccountService.self)!, + adamantCore: assembler.resolve(AdamantCore.self)!, + dialogService: assembler.resolve(DialogService.self)!, + localAuth: assembler.resolve(LocalAuthentication.self)!, + screensFactory: screenFactory, + apiService: assembler.resolve(ApiService.self)! + ) + } +} diff --git a/Adamant/Stories/Login/LoginViewController+Pinpad.swift b/Adamant/Modules/Login/LoginViewController+Pinpad.swift similarity index 100% rename from Adamant/Stories/Login/LoginViewController+Pinpad.swift rename to Adamant/Modules/Login/LoginViewController+Pinpad.swift diff --git a/Adamant/Stories/Login/LoginViewController+QR.swift b/Adamant/Modules/Login/LoginViewController+QR.swift similarity index 100% rename from Adamant/Stories/Login/LoginViewController+QR.swift rename to Adamant/Modules/Login/LoginViewController+QR.swift diff --git a/Adamant/Stories/Login/LoginViewController.swift b/Adamant/Modules/Login/LoginViewController.swift similarity index 91% rename from Adamant/Stories/Login/LoginViewController.swift rename to Adamant/Modules/Login/LoginViewController.swift index d859130a5..8453cdcdd 100644 --- a/Adamant/Stories/Login/LoginViewController.swift +++ b/Adamant/Modules/Login/LoginViewController.swift @@ -34,7 +34,7 @@ extension String.adamant { } // MARK: - ViewController -class LoginViewController: FormViewController { +final class LoginViewController: FormViewController { // MARK: Rows & Sections @@ -70,6 +70,7 @@ class LoginViewController: FormViewController { case tapToSaveHint case generateNewPassphraseButton case nodes + case coinsNodes var localized: String { switch self { @@ -98,11 +99,14 @@ class LoginViewController: FormViewController { return "" case .nodes: - return String.adamant.nodesList.nodesListButton + return .adamant.nodesList.nodesListButton + + case .coinsNodes: + return .adamant.coinsNodesList.title } } - var tag:String { + var tag: String { switch self { case .passphrase: return "pass" case .loginButton: return "login" @@ -113,6 +117,7 @@ class LoginViewController: FormViewController { case .generateNewPassphraseButton: return "generate" case .tapToSaveHint: return "hint" case .nodes: return "nodes" + case .coinsNodes: return "coinsNodes" } } } @@ -122,7 +127,7 @@ class LoginViewController: FormViewController { let accountService: AccountService let adamantCore: AdamantCore let localAuth: LocalAuthentication - let router: Router + let screensFactory: ScreensFactory let apiService: ApiService let dialogService: DialogService @@ -141,14 +146,14 @@ class LoginViewController: FormViewController { adamantCore: AdamantCore, dialogService: DialogService, localAuth: LocalAuthentication, - router: Router, + screensFactory: ScreensFactory, apiService: ApiService ) { self.accountService = accountService self.adamantCore = adamantCore self.dialogService = dialogService self.localAuth = localAuth - self.router = router + self.screensFactory = screensFactory self.apiService = apiService super.init(nibName: nil, bundle: nil) @@ -218,6 +223,7 @@ class LoginViewController: FormViewController { $0.tag = Rows.passphrase.tag $0.placeholder = Rows.passphrase.localized $0.placeholderColor = UIColor.adamant.secondary + $0.cell.textField.enablePasswordToggle() $0.keyboardReturnType = KeyboardReturnTypeConfiguration(nextKeyboardType: .go, defaultKeyboardType: .go) } @@ -317,13 +323,27 @@ class LoginViewController: FormViewController { }.cellUpdate { (cell, _) in cell.textLabel?.textColor = UIColor.adamant.primary }.onCellSelection { [weak self] (_, _) in - guard let vc = self?.router.get(scene: AdamantScene.NodesEditor.nodesList) else { - return - } - + guard let self = self else { return } + let vc = screensFactory.makeNodesList() + let nav = UINavigationController(rootViewController: vc) + nav.modalPresentationStyle = .overFullScreen + present(nav, animated: true, completion: nil) + } + + // MARK: Coins nodes list settings + <<< ButtonRow { + $0.title = Rows.coinsNodes.localized + $0.tag = Rows.coinsNodes.tag + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = UIColor.adamant.primary + }.onCellSelection { [weak self] (_, _) in + guard let self = self else { return } + let vc = screensFactory.makeCoinsNodesList(context: .login) let nav = UINavigationController(rootViewController: vc) nav.modalPresentationStyle = .overFullScreen - self?.present(nav, animated: true, completion: nil) + present(nav, animated: true, completion: nil) } // MARK: tableView position tuning @@ -375,14 +395,14 @@ extension LoginViewController { dialogService.showProgress(withMessage: String.adamant.login.loggingInProgressMessage, userInteractionEnable: false) Task { - await apiService.getAccount(byPassphrase: passphrase) { [weak self] result in - switch result { - case .success: - self?.loginIntoExistingAccount(passphrase: passphrase) - - case .failure(let error): - self?.dialogService.showRichError(error: error) - } + let result = await apiService.getAccount(byPassphrase: passphrase) + + switch result { + case .success: + loginIntoExistingAccount(passphrase: passphrase) + + case .failure(let error): + dialogService.showRichError(error: error) } } } diff --git a/Adamant/Stories/Login/PassphraseCell.xib b/Adamant/Modules/Login/PassphraseCell.xib similarity index 100% rename from Adamant/Stories/Login/PassphraseCell.xib rename to Adamant/Modules/Login/PassphraseCell.xib diff --git a/Adamant/Modules/NodesEditor/NodeCell/NodeCell+Model.swift b/Adamant/Modules/NodesEditor/NodeCell/NodeCell+Model.swift new file mode 100644 index 000000000..28342922b --- /dev/null +++ b/Adamant/Modules/NodesEditor/NodeCell/NodeCell+Model.swift @@ -0,0 +1,34 @@ +// +// EurekaNodeRow+Model.swift +// Adamant +// +// Created by Andrew G on 19.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit +import CommonKit + +extension NodeCell { + typealias NodeUpdateAction = (_ isEnabled: Bool) -> Void + + struct Model: Equatable { + let id: UUID + let title: String + let indicatorString: String + let indicatorColor: UIColor + let statusString: String + let isEnabled: Bool + let nodeUpdateAction: IDWrapper + + static let `default` = Self( + id: .init(), + title: .empty, + indicatorString: .init(), + indicatorColor: .adamant.inactive, + statusString: .empty, + isEnabled: false, + nodeUpdateAction: .init(id: .empty) { _ in } + ) + } +} diff --git a/Adamant/Modules/NodesEditor/NodeCell/NodeCell.swift b/Adamant/Modules/NodesEditor/NodeCell/NodeCell.swift new file mode 100644 index 000000000..c1e426fd4 --- /dev/null +++ b/Adamant/Modules/NodesEditor/NodeCell/NodeCell.swift @@ -0,0 +1,73 @@ +// +// NodeCell.swift +// Adamant +// +// Created by Anokhov Pavel on 20.06.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import SnapKit +import Eureka +import CommonKit +import Combine + +final class NodeCell: Cell, CellType { + private let checkmarkRowView = CheckmarkRowView() + private var subscription: AnyCancellable? + + private var model: Model = .default { + didSet { + guard model != oldValue else { return } + baseRow.baseValue = model + update() + } + } + + required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + override func update() { + checkmarkRowView.setIsChecked(model.isEnabled, animated: true) + checkmarkRowView.title = model.title + checkmarkRowView.captionColor = model.indicatorColor + checkmarkRowView.caption = model.indicatorString + checkmarkRowView.subtitle = model.statusString + } + + func subscribe>(_ publisher: P) { + subscription = publisher + .removeDuplicates() + .sink { [weak self] in self?.model = $0 } + } +} + +private extension NodeCell { + func setupView() { + contentView.addSubview(checkmarkRowView) + checkmarkRowView.snp.makeConstraints { + $0.directionalEdges.equalToSuperview() + } + + checkmarkRowView.checkmarkImage = .asset(named: "status_success") + checkmarkRowView.onCheckmarkTap = { [weak self] in self?.onCheckmarkTap() } + } + + func onCheckmarkTap() { + model.nodeUpdateAction.value(!checkmarkRowView.isChecked) + } +} + +final class NodeRow: Row, RowType { + required public init(tag: String?) { + super.init(tag: tag) + cellProvider = .init() + } +} diff --git a/Adamant/Stories/NodesEditor/NodeEditorViewController.swift b/Adamant/Modules/NodesEditor/NodeEditorViewController.swift similarity index 88% rename from Adamant/Stories/NodesEditor/NodeEditorViewController.swift rename to Adamant/Modules/NodesEditor/NodeEditorViewController.swift index 978776fa9..5c0e8216f 100644 --- a/Adamant/Stories/NodesEditor/NodeEditorViewController.swift +++ b/Adamant/Modules/NodesEditor/NodeEditorViewController.swift @@ -12,12 +12,10 @@ import CommonKit // MARK: - Localization extension String.adamant { - struct nodesEditor { + enum nodesEditor { static let newNodeTitle = String.localized("NodesEditor.NewNodeTitle", comment: "NodesEditor: New node scene title") static let deleteNodeAlert = String.localized("NodesEditor.DeleteNodeAlert", comment: "NodesEditor: Delete node confirmation message") static let failedToBuildURL = String.localized("NodesEditor.FailedToBuildURL", comment: "NodesEditor: Failed to build URL alert") - - private init() {} } } @@ -34,7 +32,7 @@ protocol NodeEditorDelegate: AnyObject { } // MARK: - NodeEditorViewController -class NodeEditorViewController: FormViewController { +final class NodeEditorViewController: FormViewController { // MARK: - Rows private enum Rows { @@ -90,6 +88,7 @@ class NodeEditorViewController: FormViewController { // MARK: - Dependencies var dialogService: DialogService! var apiService: ApiService! + var nodesStorage: NodesStorageProtocol! // MARK: - Properties var node: Node? @@ -138,15 +137,15 @@ class NodeEditorViewController: FormViewController { $0.value = node.port $0.placeholder = String(node.scheme.defaultPort) } else { - $0.placeholder = String(URLScheme.default.defaultPort) + $0.placeholder = String(Node.URLScheme.default.defaultPort) } } // Scheme - <<< PickerInlineRow { + <<< PickerInlineRow { $0.title = Rows.scheme.localized $0.tag = Rows.scheme.tag - $0.value = node?.scheme ?? URLScheme.default + $0.value = node?.scheme ?? Node.URLScheme.default $0.options = [.https, .http] $0.baseCell.detailTextLabel?.textColor = .adamant.textColor }.onExpandInlineRow { (cell, _, inlineRow) in @@ -156,7 +155,7 @@ class NodeEditorViewController: FormViewController { if let scheme = row.value { portRow.placeholder = String(scheme.defaultPort) } else { - portRow.placeholder = String(URLScheme.default.defaultPort) + portRow.placeholder = String(Node.URLScheme.default.defaultPort) } portRow.updateCell() @@ -165,7 +164,7 @@ class NodeEditorViewController: FormViewController { // MARK: - WebSockets - if let wsEnabled = node?.status?.wsEnabled { + if let wsEnabled = node?.wsEnabled { form +++ Section() <<< LabelRow { @@ -218,12 +217,15 @@ extension NodeEditorViewController { } let host = rawUrl.trimmingCharacters(in: .whitespaces) + let scheme: Node.URLScheme - let scheme: URLScheme - if let row = form.rowBy(tag: Rows.scheme.tag), let value = row.baseValue as? URLScheme { + if + let row = form.rowBy(tag: Rows.scheme.tag), + let value = row.baseValue as? Node.URLScheme + { scheme = value } else { - scheme = URLScheme.default + scheme = .default } let port: Int? @@ -235,12 +237,22 @@ extension NodeEditorViewController { let result: NodeEditorResult if let node = node { - node.scheme = scheme - node.host = host - node.port = port + nodesStorage.updateNodeParams( + id: node.id, + scheme: scheme, + host: host, + port: port + ) + result = .nodeUpdated } else { - result = .new(node: Node(scheme: scheme, host: host, port: port)) + result = .new(node: Node( + scheme: scheme, + host: host, + isEnabled: true, + wsEnabled: false, + port: port + )) } didCallDelegate = true diff --git a/Adamant/Modules/NodesEditor/NodesEditorFactory.swift b/Adamant/Modules/NodesEditor/NodesEditorFactory.swift new file mode 100644 index 000000000..8d6dffdfd --- /dev/null +++ b/Adamant/Modules/NodesEditor/NodesEditorFactory.swift @@ -0,0 +1,35 @@ +// +// NodesEditorFactory.swift +// Adamant +// +// Created by Anokhov Pavel on 20.06.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Swinject +import CommonKit + +struct NodesEditorFactory { + let assembler: Assembler + + func makeNodesListVC(screensFactory: ScreensFactory) -> UIViewController { + NodesListViewController( + dialogService: assembler.resolve(DialogService.self)!, + securedStore: assembler.resolve(SecuredStore.self)!, + screensFactory: screensFactory, + nodesStorage: assembler.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: assembler.resolve(NodesAdditionalParamsStorageProtocol.self)!, + apiService: assembler.resolve(ApiService.self)!, + socketService: assembler.resolve(SocketService.self)! + ) + } + + func makeNodeEditorVC() -> NodeEditorViewController { + let c = NodeEditorViewController() + c.dialogService = assembler.resolve(DialogService.self) + c.apiService = assembler.resolve(ApiService.self) + c.nodesStorage = assembler.resolve(NodesStorageProtocol.self) + return c + } +} diff --git a/Adamant/Stories/NodesEditor/NodesListViewController.swift b/Adamant/Modules/NodesEditor/NodesListViewController.swift similarity index 55% rename from Adamant/Stories/NodesEditor/NodesListViewController.swift rename to Adamant/Modules/NodesEditor/NodesListViewController.swift index be7260038..358859a83 100644 --- a/Adamant/Stories/NodesEditor/NodesListViewController.swift +++ b/Adamant/Modules/NodesEditor/NodesListViewController.swift @@ -9,10 +9,11 @@ import UIKit import Eureka import CommonKit +import Combine // MARK: - Localization extension String.adamant { - struct nodesList { + enum nodesList { static let title = String.localized("NodesList.Title", comment: "NodesList: scene title") static let nodesListButton = String.localized("NodesList.NodesList", comment: "NodesList: Button label") @@ -20,12 +21,15 @@ extension String.adamant { static let resetAlertTitle = String.localized("NodesList.ResetNodeListAlert", comment: "NodesList: Reset nodes alert title") - private init() {} + static let fastestNodeModeTip = String.localized( + "NodesList.PreferTheFastestNode.Footer", + comment: .empty + ) } } // MARK: - NodesListViewController -class NodesListViewController: FormViewController { +final class NodesListViewController: FormViewController { // Rows & Sections private enum Sections { @@ -67,94 +71,54 @@ class NodesListViewController: FormViewController { // MARK: Dependencies - var dialogService: DialogService! - var securedStore: SecuredStore! - var router: Router! - var nodesSource: NodesSource! - - var apiService: ApiService! { - didSet { - Task { - currentRestNode = await apiService.currentNodes.first - } - } - } - - var socketService: SocketService! { - didSet { - currentSocketsNode = socketService.currentNode - } - } + private let dialogService: DialogService + private let securedStore: SecuredStore + private let screensFactory: ScreensFactory + private let nodesStorage: NodesStorageProtocol + private let nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol + private let apiService: ApiService + private let socketService: SocketService // Properties - private var timer: Timer? + @ObservableValue private var nodesList = [Node]() + @ObservableValue private var currentSocketsNodeId: UUID? + @ObservableValue private var currentRestNodesIds = [UUID]() - private var currentSocketsNode: Node? { - didSet { - updateNodesRows() - } - } - - private var currentRestNode: Node? { - didSet { - updateNodesRows() - } - } + private var nodesHaveBeenDisplayed = false + private var timerSubsctiption: AnyCancellable? + private var subscriptions = Set() // MARK: - Lifecycle - init() { + init( + dialogService: DialogService, + securedStore: SecuredStore, + screensFactory: ScreensFactory, + nodesStorage: NodesStorageProtocol, + nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol, + apiService: ApiService, + socketService: SocketService + ) { + self.dialogService = dialogService + self.securedStore = securedStore + self.screensFactory = screensFactory + self.nodesStorage = nodesStorage + self.nodesAdditionalParamsStorage = nodesAdditionalParamsStorage + self.apiService = apiService + self.socketService = socketService super.init(nibName: nil, bundle: nil) - setup() } required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } - - deinit { - timer?.invalidate() - } - - private func setup() { - NotificationCenter.default.addObserver( - forName: Notification.Name.NodesSource.nodesUpdate, - object: nil, - queue: nil - ) { [weak self] _ in - DispatchQueue.onMainAsync { - self?.updateNodesRows() - } - } - - NotificationCenter.default.addObserver( - forName: Notification.Name.ApiService.currentNodeUpdate, - object: nil, - queue: nil - ) { [weak self] _ in - DispatchQueue.onMainAsync { - self?.currentRestNode = self?.apiService.currentNodes.first - } - } - - NotificationCenter.default.addObserver( - forName: Notification.Name.SocketService.currentNodeUpdate, - object: nil, - queue: nil - ) { [weak self] _ in - DispatchQueue.onMainAsync { - self?.currentSocketsNode = self?.socketService.currentNode - } - } + fatalError("Isn't implemented") } override func viewDidLoad() { super.viewDidLoad() navigationItem.title = String.adamant.nodesList.title navigationOptions = .Disabled - navigationItem.largeTitleDisplayMode = .always + navigationItem.largeTitleDisplayMode = .never if splitViewController == nil, navigationController?.viewControllers.count == 1 { let done = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(NodesListViewController.close)) @@ -171,13 +135,19 @@ class NodesListViewController: FormViewController { +++ Section { $0.tag = Sections.preferTheFastestNode.tag + $0.footer = HeaderFooterView(stringLiteral: .adamant.nodesList.fastestNodeModeTip) } - <<< SwitchRow { [preferTheFastestNode = nodesSource.preferTheFastestNode] in + <<< SwitchRow { [nodesAdditionalParamsStorage] in $0.title = Rows.preferTheFastestNode.localized - $0.value = preferTheFastestNode - }.onChange { [weak nodesSource] in - nodesSource?.preferTheFastestNode = $0.value ?? true + $0.value = nodesAdditionalParamsStorage.isFastestNodeMode( + group: nodeGroup + ) + }.onChange { [nodesAdditionalParamsStorage] in + nodesAdditionalParamsStorage.setFastestNodeMode( + group: nodeGroup, + value: $0.value ?? true + ) }.cellUpdate { cell, _ in cell.switchControl.onTintColor = .adamant.active } @@ -205,26 +175,54 @@ class NodesListViewController: FormViewController { } setColors() + setupObservers() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - updateNodesRows() - nodesSource.healthCheck() + apiService.healthCheck() setHealthCheckTimer() } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - timer?.invalidate() - } - // MARK: - Other private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear } + + private func setupObservers() { + nodesStorage.getNodesPublisher(group: nodeGroup) + .combineLatest(nodesAdditionalParamsStorage.fastestNodeMode(group: nodeGroup)) + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.setNewNodesList($0.0) } + .store(in: &subscriptions) + + NotificationCenter.default + .publisher(for: .SocketService.currentNodeUpdate, object: nil) + .receive(on: DispatchQueue.main) + .map { [weak self] _ in self?.socketService.currentNode?.id } + .removeDuplicates() + .assign(to: _currentSocketsNodeId) + .store(in: &subscriptions) + + currentSocketsNodeId = socketService.currentNode?.id + } + + private func setNewNodesList(_ newNodes: [Node]) { + nodesList = newNodes + currentRestNodesIds = apiService.preferredNodeIds + + if !nodesHaveBeenDisplayed { + UIView.performWithoutAnimation { + remakeNodesRows() + } + } else { + remakeNodesRows() + } + + nodesHaveBeenDisplayed = true + } } // MARK: - Manipulating node list @@ -234,28 +232,28 @@ extension NodesListViewController { } func addNode(node: Node) { - getNodesSection()?.append(createRowFor(node: node, tag: generateRandomTag())) - nodesSource.nodes.append(node) + getNodesSection()?.append(createRowFor(nodeId: node.id, tag: generateRandomTag())) + nodesStorage.addNode(node, group: nodeGroup) } - func removeNode(node: Node) { - guard let index = getNodeIndex(node: node) else { return } + func removeNode(nodeId: UUID) { + guard let index = getNodeIndex(nodeId: nodeId) else { return } getNodesSection()?.remove(at: index) - nodesSource.nodes.remove(at: index) + nodesStorage.removeNode(id: nodeId) } - func getNodeIndex(node: Node) -> Int? { - nodesSource.nodes.firstIndex { $0 === node } + func getNodeIndex(nodeId: UUID) -> Int? { + displayedNodesIds.firstIndex { $0 == nodeId } } func getNodesSection() -> Section? { form.sectionBy(tag: Sections.nodes.tag) } - var displayedNodes: [Node] { + var displayedNodesIds: [UUID] { getNodesSection()?.allRows.compactMap { - ($0.baseValue as? NodeCell.Model)?.node + ($0.baseValue as? NodeCell.Model)?.id } ?? [] } @@ -273,7 +271,7 @@ extension NodesListViewController { func resetToDefault(silent: Bool = false) { if silent { - nodesSource.setDefaultNodes() + nodesStorage.resetNodes(group: nodeGroup) return } @@ -282,24 +280,22 @@ extension NodesListViewController { alert.addAction(UIAlertAction( title: Rows.reset.localized, style: .destructive, - handler: { [weak self] _ in self?.nodesSource.setDefaultNodes() } + handler: { [weak self] _ in self?.nodesStorage.resetNodes(group: nodeGroup) } )) alert.modalPresentationStyle = .overFullScreen present(alert, animated: true, completion: nil) } - func updateNodesRows() { - guard let nodesSection = getNodesSection() else { return } - - guard !displayedNodes.hasTheSameReferences(as: nodesSource.nodes) else { - nodesSection.allRows.forEach { $0.updateCell() } - return - } + func remakeNodesRows() { + guard + let nodesSection = getNodesSection(), + displayedNodesIds != nodesList.map({ $0.id }) + else { return } nodesSection.removeAll() - for node in nodesSource.nodes { - let row = createRowFor(node: node, tag: generateRandomTag()) + for node in nodesList { + let row = createRowFor(nodeId: node.id, tag: generateRandomTag()) nodesSection.append(row) } } @@ -311,23 +307,16 @@ extension NodesListViewController: NodeEditorDelegate { switch result { case .new(let node): addNode(node: node) - case .delete(let editorNode): - removeNode(node: editorNode) - - case .nodeUpdated: - nodesSource.nodesUpdate() - - case .cancel: + removeNode(nodeId: editorNode.id) + case .nodeUpdated, .cancel: break } - DispatchQueue.main.async { - if UIScreen.main.traitCollection.userInterfaceIdiom == .pad { - self.navigationController?.popToViewController(self, animated: true) - } else { - self.dismiss(animated: true, completion: nil) - } + if UIScreen.main.traitCollection.userInterfaceIdiom == .pad { + navigationController?.popToViewController(self, animated: true) + } else { + dismiss(animated: true, completion: nil) } } } @@ -336,7 +325,7 @@ extension NodesListViewController: NodeEditorDelegate { extension NodesListViewController { func loadDefaultNodes(showAlert: Bool) { - nodesSource.setDefaultNodes() + nodesStorage.resetNodes(group: nodeGroup) if showAlert { dialogService.showSuccess(withMessage: String.adamant.nodesList.defaultNodesWasLoaded) @@ -346,9 +335,9 @@ extension NodesListViewController { // MARK: - Tools extension NodesListViewController { - private func createRowFor(node: Node, tag: String) -> BaseRow { + private func createRowFor(nodeId: UUID, tag: String) -> BaseRow { let row = NodeRow { - $0.value = makeNodeCellModel(node: node) + $0.cell.subscribe(makeNodeCellPublisher(nodeId: nodeId)) $0.tag = tag let deleteAction = SwipeAction( @@ -358,7 +347,7 @@ extension NodesListViewController { defer { completionHandler?(true) } guard let model = row.baseValue as? NodeCell.Model else { return } - self?.removeNode(node: model.node) + self?.removeNode(nodeId: model.id) } $0.trailingSwipe.actions = [deleteAction] @@ -369,20 +358,20 @@ extension NodesListViewController { } }.onCellSelection { [weak self] (_, row) in defer { row.deselect(animated: true) } - guard let node = row.value?.node else { - return - } - self?.editNode(node) + guard + let self = self, + let node = self.nodesList.first(where: { $0.id == row.value?.id }) + else { return } + + self.editNode(node) } return row } private func presentEditor(forNode node: Node?) { - guard let editor = router.get(scene: AdamantScene.NodesEditor.nodeEditor) as? NodeEditorViewController else { - fatalError("Failed to get editor") - } + let editor = screensFactory.makeNodeEditor() editor.delegate = self editor.node = node @@ -408,35 +397,48 @@ extension NodesListViewController { } private func setHealthCheckTimer() { - timer = Timer.scheduledTimer( - withTimeInterval: regularHealthCheckTimeInteval, - repeats: true - ) { [weak nodesSource] _ in - nodesSource?.healthCheck() - } + timerSubsctiption = Timer + .publish(every: nodeGroup.onScreenUpdateInterval, on: .main, in: .default) + .autoconnect() + .sink { [apiService] _ in apiService.healthCheck() } } private func makeNodeCellModel(node: Node) -> NodeCell.Model { - NodeCell.Model( - node: node, - nodeUpdate: { [weak nodesSource] in - nodesSource?.nodesUpdate() - }, - nodeActivity: { [weak self] node in - var activities = Set() - - if self?.currentRestNode === node { - activities.insert(.rest) - } - - if self?.currentSocketsNode === node { - activities.insert(.webSockets) - } - - return activities + let connectionStatus = node.isEnabled + ? node.connectionStatus + : .none + + return .init( + id: node.id, + title: node.asString(), + indicatorString: node.indicatorString( + isRest: currentRestNodesIds.contains(node.id), + isWs: currentSocketsNodeId == node.id + ), + indicatorColor: node.indicatorColor, + statusString: node.statusString(showVersion: true) ?? .empty, + isEnabled: node.isEnabled, + nodeUpdateAction: .init(id: node.id.uuidString) { [nodesStorage] isEnabled in + nodesStorage.updateNodeParams(id: node.id, isEnabled: isEnabled) } ) } + + private func makeNodeCellPublisher(nodeId: UUID) -> some Observable { + $nodesList.combineLatest( + $currentSocketsNodeId, + $currentRestNodesIds + ).compactMap { [weak self] tuple in + let nodes = tuple.0 + + guard + let self = self, + let node = nodes.first(where: { $0.id == nodeId }) + else { return nil } + + return self.makeNodeCellModel(node: node) + } + } } -private let regularHealthCheckTimeInteval: TimeInterval = 10 +private let nodeGroup: NodeGroup = .adm diff --git a/Adamant/Stories/Onboard/EulaViewController.swift b/Adamant/Modules/Onboard/EulaViewController.swift similarity index 96% rename from Adamant/Stories/Onboard/EulaViewController.swift rename to Adamant/Modules/Onboard/EulaViewController.swift index eeac0b792..a67c939ce 100644 --- a/Adamant/Stories/Onboard/EulaViewController.swift +++ b/Adamant/Modules/Onboard/EulaViewController.swift @@ -8,7 +8,7 @@ import UIKit -class EulaViewController: UIViewController { +final class EulaViewController: UIViewController { // MARK: Outlets @IBOutlet weak var eulaTextView: UITextView! diff --git a/Adamant/Stories/Onboard/EulaViewController.xib b/Adamant/Modules/Onboard/EulaViewController.xib similarity index 100% rename from Adamant/Stories/Onboard/EulaViewController.xib rename to Adamant/Modules/Onboard/EulaViewController.xib diff --git a/Adamant/Modules/Onboard/OnboardFactory.swift b/Adamant/Modules/Onboard/OnboardFactory.swift new file mode 100644 index 000000000..e05814cf1 --- /dev/null +++ b/Adamant/Modules/Onboard/OnboardFactory.swift @@ -0,0 +1,20 @@ +// +// OnboardFactory.swift +// Adamant +// +// Created by Anokhov Pavel on 18/01/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit +import Swinject + +struct OnboardFactory { + func makeOnboardVC() -> UIViewController { + OnboardViewController(nibName: "OnboardViewController", bundle: nil) + } + + func makeEulaVC() -> UIViewController { + EulaViewController(nibName: "EulaViewController", bundle: nil) + } +} diff --git a/Adamant/Stories/Onboard/OnboardOverlay.swift b/Adamant/Modules/Onboard/OnboardOverlay.swift similarity index 97% rename from Adamant/Stories/Onboard/OnboardOverlay.swift rename to Adamant/Modules/Onboard/OnboardOverlay.swift index 4f391d91f..434e95bb0 100644 --- a/Adamant/Stories/Onboard/OnboardOverlay.swift +++ b/Adamant/Modules/Onboard/OnboardOverlay.swift @@ -9,7 +9,7 @@ import UIKit import CommonKit -class OnboardOverlay: SwiftyOnboardOverlay { +final class OnboardOverlay: SwiftyOnboardOverlay { lazy var agreeSwitch: UISwitch = { let view = UISwitch() diff --git a/Adamant/Stories/Onboard/OnboardPage.swift b/Adamant/Modules/Onboard/OnboardPage.swift similarity index 98% rename from Adamant/Stories/Onboard/OnboardPage.swift rename to Adamant/Modules/Onboard/OnboardPage.swift index 89f2c2ea9..88fcd2679 100644 --- a/Adamant/Stories/Onboard/OnboardPage.swift +++ b/Adamant/Modules/Onboard/OnboardPage.swift @@ -10,7 +10,7 @@ import UIKit import MarkdownKit import CommonKit -class OnboardPage: SwiftyOnboardPage { +final class OnboardPage: SwiftyOnboardPage { @IBOutlet weak var image: UIImageView! @IBOutlet weak var text: UITextView! diff --git a/Adamant/Stories/Onboard/OnboardPage.xib b/Adamant/Modules/Onboard/OnboardPage.xib similarity index 100% rename from Adamant/Stories/Onboard/OnboardPage.xib rename to Adamant/Modules/Onboard/OnboardPage.xib diff --git a/Adamant/Stories/Onboard/OnboardViewController.swift b/Adamant/Modules/Onboard/OnboardViewController.swift similarity index 99% rename from Adamant/Stories/Onboard/OnboardViewController.swift rename to Adamant/Modules/Onboard/OnboardViewController.swift index 33cad5a6d..e9347e293 100644 --- a/Adamant/Stories/Onboard/OnboardViewController.swift +++ b/Adamant/Modules/Onboard/OnboardViewController.swift @@ -30,7 +30,7 @@ fileprivate extension String.adamant { } } -class OnboardViewController: UIViewController { +final class OnboardViewController: UIViewController { // MARK: Constants private static let titleFont = UIFont.adamantPrimary(ofSize: 18) diff --git a/Adamant/Stories/Onboard/OnboardViewController.xib b/Adamant/Modules/Onboard/OnboardViewController.xib similarity index 100% rename from Adamant/Stories/Onboard/OnboardViewController.xib rename to Adamant/Modules/Onboard/OnboardViewController.xib diff --git a/Adamant/Stories/Onboard/ReadonlyTextView.swift b/Adamant/Modules/Onboard/ReadonlyTextView.swift similarity index 86% rename from Adamant/Stories/Onboard/ReadonlyTextView.swift rename to Adamant/Modules/Onboard/ReadonlyTextView.swift index 4baa31817..af7084085 100644 --- a/Adamant/Stories/Onboard/ReadonlyTextView.swift +++ b/Adamant/Modules/Onboard/ReadonlyTextView.swift @@ -8,7 +8,7 @@ import UIKit -class ReadonlyTextView: UITextView { +final class ReadonlyTextView: UITextView { override var selectedTextRange: UITextRange? { get { return nil diff --git a/Adamant/Modules/PartnerQR/PartnerQRFactory.swift b/Adamant/Modules/PartnerQR/PartnerQRFactory.swift new file mode 100644 index 000000000..27801872d --- /dev/null +++ b/Adamant/Modules/PartnerQR/PartnerQRFactory.swift @@ -0,0 +1,52 @@ +// +// PartnerQRFactory.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 27.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Swinject +import SwiftUI +import CommonKit + +struct PartnerQRFactory { + private let assembler: Assembler + + init(parent: Assembler) { + assembler = .init([PartnerQRAssembly()], parent: parent) + } + + @MainActor + func makeViewController(partner: CoreDataAccount) -> UIViewController { + let viewModel = assembler.resolve(PartnerQRViewModel.self)! + viewModel.setup(partner: partner) + + let view = PartnerQRView( + viewModel: viewModel + ) + + return UIHostingController( + rootView: view + ) + } +} + +private struct PartnerQRAssembly: Assembly { + func assemble(container: Container) { + container.register(PartnerQRService.self) { r in + AdamantPartnerQRService( + securedStore: r.resolve(SecuredStore.self)! + ) + }.inObjectScope(.container) + + container.register(PartnerQRViewModel.self) { + PartnerQRViewModel( + dialogService: $0.resolve(DialogService.self)!, + addressBookService: $0.resolve(AddressBookService.self)!, + avatarService: $0.resolve(AvatarService.self)!, + partnerQRService: $0.resolve(PartnerQRService.self)! + ) + }.inObjectScope(.weak) + } +} diff --git a/Adamant/Modules/PartnerQR/PartnerQRView.swift b/Adamant/Modules/PartnerQR/PartnerQRView.swift new file mode 100644 index 000000000..706f18929 --- /dev/null +++ b/Adamant/Modules/PartnerQR/PartnerQRView.swift @@ -0,0 +1,95 @@ +// +// PartnerQRView.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 27.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import SwiftUI + +struct PartnerQRView: View { + @ObservedObject var viewModel: PartnerQRViewModel + + var body: some View { + Form { + infoSection() + toggleSection() + buttonSection() + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + toolbar() + } + } + } +} + +private extension PartnerQRView { + func toolbar() -> some View { + HStack { + if let uiImage = viewModel.partnerImage { + Image(uiImage: uiImage) + .resizable() + .frame(squareSize: viewModel.partnerImageSize) + } + Text(viewModel.partnerName).font(.headline) + } + } + + func infoSection() -> some View { + Section { + if let uiImage = viewModel.image { + HStack { + Spacer() + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 250) + Spacer() + } + } + + HStack { + Spacer() + Button(action: { + viewModel.copyToPasteboard() + }, label: { + Text(viewModel.title) + .padding() + }) + Spacer() + } + } + } + + func toggleSection() -> some View { + Section { + Toggle(String.adamant.partnerQR.includePartnerName, isOn: $viewModel.includeContactsName) + .disabled(!viewModel.includeContactsNameEnabled) + .tint(.init(uiColor: .adamant.active)) + .onChange(of: viewModel.includeContactsName) { _ in + viewModel.didToggle() + } + + Toggle(String.adamant.partnerQR.includePartnerURL, isOn: $viewModel.includeWebAppLink) + .tint(.init(uiColor: .adamant.active)) + .onChange(of: viewModel.includeWebAppLink) { _ in + viewModel.didToggle() + } + } + } + + func buttonSection() -> some View { + Section { + Button(String.adamant.alert.saveToPhotolibrary) { + viewModel.saveToPhotos() + } + + Button(String.adamant.alert.share) { + viewModel.share() + } + } + } +} diff --git a/Adamant/Modules/PartnerQR/PartnerQRViewModel.swift b/Adamant/Modules/PartnerQR/PartnerQRViewModel.swift new file mode 100644 index 000000000..acb043846 --- /dev/null +++ b/Adamant/Modules/PartnerQR/PartnerQRViewModel.swift @@ -0,0 +1,206 @@ +// +// PartnerQRViewModel.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 27.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import SwiftUI +import Combine +import CommonKit +import Photos + +@MainActor +final class PartnerQRViewModel: NSObject, ObservableObject { + @Published var includeContactsNameEnabled = true + @Published var image: UIImage? + @Published var partnerImage: UIImage? + @Published var partnerName: String = "" + @Published var includeWebAppLink = false + @Published var includeContactsName = false + + private var partner: CoreDataAccount? + private let dialogService: DialogService + private let addressBookService: AddressBookService + private let avatarService: AvatarService + private let partnerQRService: PartnerQRService + private var subscriptions = Set() + + let partnerImageSize: CGFloat = 25 + + var title: String { + partner?.address ?? "" + } + + nonisolated init( + dialogService: DialogService, + addressBookService: AddressBookService, + avatarService: AvatarService, + partnerQRService: PartnerQRService + ) { + self.dialogService = dialogService + self.addressBookService = addressBookService + self.avatarService = avatarService + self.partnerQRService = partnerQRService + } + + func setup(partner: CoreDataAccount) { + self.partner = partner + updatePartnerInfo() + generateQR() + } + + func saveToPhotos() { + guard let qrCode = image else { return } + + switch PHPhotoLibrary.authorizationStatus() { + case .authorized, .limited: + UIImageWriteToSavedPhotosAlbum( + qrCode, + self, + #selector(image(_: didFinishSavingWithError: contextInfo:)), + nil + ) + + case .notDetermined: + UIImageWriteToSavedPhotosAlbum( + qrCode, + self, + #selector(image(_: didFinishSavingWithError: contextInfo:)), + nil + ) + + case .restricted, .denied: + dialogService.presentGoToSettingsAlert( + title: nil, + message: String.adamant.shared.photolibraryNotAuthorized + ) + @unknown default: + break + } + } + + func share() { + guard let qrCode = image else { return } + + let vc = UIActivityViewController( + activityItems: [qrCode], + applicationActivities: nil + ) + + vc.completionWithItemsHandler = { [weak self] (_: UIActivity.ActivityType?, completed: Bool, _, error: Error?) in + guard completed else { return } + + if let error = error { + self?.dialogService.showWarning(withMessage: error.localizedDescription) + } else { + self?.dialogService.showSuccess(withMessage: String.adamant.alert.done) + } + } + vc.modalPresentationStyle = .overFullScreen + dialogService.present(vc, animated: true, completion: nil) + } + + func didToggle() { + partnerQRService.setIncludeURLEnabled(includeWebAppLink) + partnerQRService.setIncludeNameEnabled(includeContactsName) + generateQR() + } + + func copyToPasteboard() { + UIPasteboard.general.string = title + dialogService.showToastMessage(.adamant.alert.copiedToPasteboardNotification) + } +} + +private extension PartnerQRViewModel { + func updatePartnerInfo() { + guard let publicKey = partner?.publicKey, + let address = partner?.address + else { + includeContactsNameEnabled = false + includeContactsName = false + includeWebAppLink = false + return + } + + let name = addressBookService.getName(for: partner) + + if let name = name { + partnerName = name + includeContactsNameEnabled = true + includeContactsName = partnerQRService.isIncludeNameEnabled() + } else { + partnerName = address + includeContactsNameEnabled = false + includeContactsName = false + } + + includeWebAppLink = partnerQRService.isIncludeURLEnabled() + + guard let avatarName = partner?.avatar, + let avatar = UIImage.asset(named: avatarName) + else { + partnerImage = avatarService.avatar( + for: publicKey, + size: partnerImageSize + ) + return + } + + partnerImage = avatar + } + + func generateQR() { + guard let address = partner?.address else { return } + + var params: [AdamantAddressParam] = [] + + let name = addressBookService.getName(for: partner) + + if includeContactsName, + let name = name { + params.append(.label(name)) + } + + var data: String = address + + if includeWebAppLink { + data = AdamantUriTools.encode(request: AdamantUri.address( + address: address, + params: params + )) + } else { + data = AdamantUriTools.encode(request: AdamantUri.addressLegacy( + address: address, + params: params + )) + } + + let qr = AdamantQRTools.generateQrFrom( + string: data, + withLogo: true + ) + + switch qr { + case .success(let uIImage): + image = uIImage + case .failure(let error): + dialogService.showError(withMessage: "", supportEmail: false, error: error) + } + } + + @objc private func image( + _ image: UIImage, + didFinishSavingWithError error: NSError?, + contextInfo: UnsafeRawPointer + ) { + guard error == nil else { + dialogService.presentGoToSettingsAlert(title: String.adamant.shared.photolibraryNotAuthorized, message: nil) + return + } + + dialogService.showSuccess(withMessage: String.adamant.alert.done) + } +} diff --git a/Adamant/Modules/ScreensFactory/AdamantScreensFactory.swift b/Adamant/Modules/ScreensFactory/AdamantScreensFactory.swift new file mode 100644 index 000000000..f4e734ee2 --- /dev/null +++ b/Adamant/Modules/ScreensFactory/AdamantScreensFactory.swift @@ -0,0 +1,184 @@ +// +// AdamantScreensFactory.swift +// Adamant +// +// Created by Andrew G on 10.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit +import Swinject + +@MainActor +struct AdamantScreensFactory: ScreensFactory { + private let walletFactoryCompose: WalletFactoryCompose + private let admWalletFactory: AdmWalletFactory + private let chatListFactory: ChatListFactory + private let chatFactory: ChatFactory + private let nodesEditorFactory: NodesEditorFactory + private let delegatesFactory: DelegatesFactory + private let settingsFactory: SettingsFactory + private let contributeFactory: ContributeFactory + private let loginFactory: LoginFactory + private let onboardFactory: OnboardFactory + private let shareQRFactory: ShareQRFactory + private let accountFactory: AccountFactory + private let vibrationSelectionFactory: VibrationSelectionFactory + private let partnerQRFactory: PartnerQRFactory + private let coinsNodesListFactory: CoinsNodesListFactory + + init(assembler: Assembler) { + admWalletFactory = .init(assembler: assembler) + chatListFactory = .init(assembler: assembler) + chatFactory = .init(assembler: assembler) + nodesEditorFactory = .init(assembler: assembler) + delegatesFactory = .init(assembler: assembler) + settingsFactory = .init(assembler: assembler) + contributeFactory = .init(parent: assembler) + loginFactory = .init(assembler: assembler) + onboardFactory = .init() + shareQRFactory = .init(assembler: assembler) + accountFactory = .init(assembler: assembler) + vibrationSelectionFactory = .init(parent: assembler) + partnerQRFactory = .init(parent: assembler) + coinsNodesListFactory = .init(parent: assembler) + + walletFactoryCompose = AdamantWalletFactoryCompose( + lskWalletFactory: .init(assembler: assembler), + dogeWalletFactory: .init(assembler: assembler), + dashWalletFactory: .init(assembler: assembler), + btcWalletFactory: .init(assembler: assembler), + ethWalletFactory: .init(assembler: assembler), + erc20WalletFactory: .init(assembler: assembler), + admWalletFactory: admWalletFactory + ) + } + + func makeWalletVC(service: WalletService) -> WalletViewController { + walletFactoryCompose.makeWalletVC(service: service, screensFactory: self) + } + + func makeTransferListVC(service: WalletService) -> UIViewController { + walletFactoryCompose.makeTransferListVC(service: service, screenFactory: self) + } + + func makeTransferVC(service: WalletService) -> TransferViewControllerBase { + walletFactoryCompose.makeTransferVC(service: service, screenFactory: self) + } + + func makeDetailsVC(service: WalletService) -> TransactionDetailsViewControllerBase { + walletFactoryCompose.makeDetailsVC(service: service) + } + + func makeDetailsVC(service: WalletService, transaction: RichMessageTransaction) -> UIViewController? { + walletFactoryCompose.makeDetailsVC(service: service, transaction: transaction) + } + + func makeAdmTransactionDetails(transaction: TransferTransaction) -> UIViewController { + admWalletFactory.makeDetailsVC(transaction: transaction, screensFactory: self) + } + + func makeAdmTransactionDetails() -> AdmTransactionDetailsViewController { + admWalletFactory.makeDetailsVC(screensFactory: self) + } + + func makeBuyAndSell() -> UIViewController { + admWalletFactory.makeBuyAndSellVC() + } + + func makeChatList() -> UIViewController { + chatListFactory.makeChatListVC(screensFactory: self) + } + + func makeChat() -> ChatViewController { + chatFactory.makeViewController(screensFactory: self) + } + + func makeNewChat() -> NewChatViewController { + chatListFactory.makeNewChatVC(screensFactory: self) + } + + func makeDelegatesList() -> UIViewController { + delegatesFactory.makeDelegatesListVC(screensFactory: self) + } + + func makeDelegateDetails() -> DelegateDetailsViewController { + delegatesFactory.makeDelegateDetails() + } + + func makeNodesList() -> UIViewController { + nodesEditorFactory.makeNodesListVC(screensFactory: self) + } + + func makeNodeEditor() -> NodeEditorViewController { + nodesEditorFactory.makeNodeEditorVC() + } + + func makeEula() -> UIViewController { + onboardFactory.makeEulaVC() + } + + func makeOnboard() -> UIViewController { + onboardFactory.makeOnboardVC() + } + + func makeShareQr() -> ShareQrViewController { + shareQRFactory.makeViewController() + } + + func makeAccount() -> UIViewController { + accountFactory.makeViewController(screensFactory: self) + } + + func makeComplexTransfer() -> UIViewController { + chatListFactory.makeComplexTransferVC(screensFactory: self) + } + + func makeSearchResults() -> SearchResultsViewController { + chatListFactory.makeSearchResultsViewController(screensFactory: self) + } + + func makeSecurity() -> UIViewController { + settingsFactory.makeSecurityVC(screensFactory: self) + } + + func makeQRGenerator() -> UIViewController { + settingsFactory.makeQRGeneratorVC() + } + + func makePKGenerator() -> UIViewController { + settingsFactory.makePKGeneratorVC() + } + + func makeAbout() -> UIViewController { + settingsFactory.makeAboutVC(screensFactory: self) + } + + func makeNotifications() -> UIViewController { + settingsFactory.makeNotificationsVC() + } + + func makeVisibleWallets() -> UIViewController { + settingsFactory.makeVisibleWalletsVC() + } + + func makeContribute() -> UIViewController { + contributeFactory.makeViewController() + } + + func makeLogin() -> LoginViewController { + loginFactory.makeViewController(screenFactory: self) + } + + func makeVibrationSelection() -> UIViewController { + vibrationSelectionFactory.makeViewController() + } + + func makePartnerQR(partner: CoreDataAccount) -> UIViewController { + partnerQRFactory.makeViewController(partner: partner) + } + + func makeCoinsNodesList(context: CoinsNodesListContext) -> UIViewController { + coinsNodesListFactory.makeViewController(context: context) + } +} diff --git a/Adamant/Modules/ScreensFactory/ScreensFactory.swift b/Adamant/Modules/ScreensFactory/ScreensFactory.swift new file mode 100644 index 000000000..628db804a --- /dev/null +++ b/Adamant/Modules/ScreensFactory/ScreensFactory.swift @@ -0,0 +1,64 @@ +// +// ScreensFactory.swift +// Adamant +// +// Created by Andrew G on 10.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit + +@MainActor +protocol ScreensFactory { + // MARK: Wallets + + func makeWalletVC(service: WalletService) -> WalletViewController + func makeTransferListVC(service: WalletService) -> UIViewController + func makeTransferVC(service: WalletService) -> TransferViewControllerBase + func makeDetailsVC(service: WalletService) -> TransactionDetailsViewControllerBase + + func makeDetailsVC( + service: WalletService, + transaction: RichMessageTransaction + ) -> UIViewController? + + func makeAdmTransactionDetails(transaction: TransferTransaction) -> UIViewController + func makeAdmTransactionDetails() -> AdmTransactionDetailsViewController + func makeBuyAndSell() -> UIViewController + + // MARK: Chats + + func makeChat() -> ChatViewController + func makeChatList() -> UIViewController + func makeNewChat() -> NewChatViewController + func makeComplexTransfer() -> UIViewController + func makeSearchResults() -> SearchResultsViewController + + // MARK: Delegates + + func makeDelegatesList() -> UIViewController + func makeDelegateDetails() -> DelegateDetailsViewController + + // MARK: Nodes + + func makeNodesList() -> UIViewController + func makeNodeEditor() -> NodeEditorViewController + func makeCoinsNodesList(context: CoinsNodesListContext) -> UIViewController + + // MARK: Other + + func makeEula() -> UIViewController + func makeOnboard() -> UIViewController + func makeShareQr() -> ShareQrViewController + func makeAccount() -> UIViewController + func makeSecurity() -> UIViewController + func makeQRGenerator() -> UIViewController + func makePKGenerator() -> UIViewController + func makeAbout() -> UIViewController + func makeNotifications() -> UIViewController + func makeVisibleWallets() -> UIViewController + func makeContribute() -> UIViewController + func makeLogin() -> LoginViewController + func makeVibrationSelection() -> UIViewController + func makePartnerQR(partner: CoreDataAccount) -> UIViewController +} diff --git a/Adamant/Stories/Settings/AboutViewController.swift b/Adamant/Modules/Settings/AboutViewController.swift similarity index 91% rename from Adamant/Stories/Settings/AboutViewController.swift rename to Adamant/Modules/Settings/AboutViewController.swift index 31839b4b3..79b6796b0 100644 --- a/Adamant/Stories/Settings/AboutViewController.swift +++ b/Adamant/Modules/Settings/AboutViewController.swift @@ -22,7 +22,7 @@ extension String.adamant { } // MARK: - AboutViewController -class AboutViewController: FormViewController { +final class AboutViewController: FormViewController { // MARK: Section & Rows enum Sections { @@ -105,10 +105,12 @@ class AboutViewController: FormViewController { var accountService: AccountService! var accountsProvider: AccountsProvider! var dialogService: DialogService! - var router: Router! + var screensFactory: ScreensFactory! // MARK: Properties private var storedIOSSupportMessage: String? + private var numerOfTap = 0 + private let maxNumerOfTap = 10 // MARK: Lifecycle @@ -120,6 +122,13 @@ class AboutViewController: FormViewController { // MARK: Header & Footer if let header = UINib(nibName: "LogoFullHeader", bundle: nil).instantiate(withOwner: nil, options: nil).first as? UIView { + + let tapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(tapAction) + ) + header.addGestureRecognizer(tapGestureRecognizer) + tableView.tableHeaderView = header if let label = header.viewWithTag(888) as? UILabel { @@ -186,14 +195,10 @@ class AboutViewController: FormViewController { }.cellUpdate { (cell, _) in cell.accessoryType = .disclosureIndicator }.onCellSelection { [weak self] (_, _) in - guard let vc = self?.router.get(scene: AdamantScene.Onboard.welcome) else { - if let tableView = self?.tableView, let indexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: indexPath, animated: true) - } - return - } + guard let self = self else { return } + let vc = self.screensFactory.makeOnboard() vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) + self.present(vc, animated: true, completion: nil) } // MARK: Contact @@ -261,13 +266,13 @@ class AboutViewController: FormViewController { do { let account = try await accountsProvider.getAccount(byAddress: AdamantContacts.adamantSupport.address) - guard let chatroom = account.chatroom, - let nav = self.navigationController, - let account = self.accountService.account, - let chat = router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { - return - } + guard + let chatroom = account.chatroom, + let nav = navigationController, + let account = accountService.account + else { return } + let chat = screensFactory.makeChat() chat.hidesBottomBarWhenPushed = true chat.viewModel.setup( account: account, @@ -361,3 +366,18 @@ extension AboutViewController: ChatPreservationDelegate { } } } + +private extension AboutViewController { + @objc func tapAction() { + numerOfTap += 1 + + guard numerOfTap == maxNumerOfTap else { + return + } + + NotificationCenter.default.post( + name: .AdamantVibroService.presentVibrationRow, + object: nil + ) + } +} diff --git a/Adamant/Modules/Settings/Contribute/ContributeFactory.swift b/Adamant/Modules/Settings/Contribute/ContributeFactory.swift new file mode 100644 index 000000000..a8c13a4ec --- /dev/null +++ b/Adamant/Modules/Settings/Contribute/ContributeFactory.swift @@ -0,0 +1,36 @@ +// +// ContributeFactory.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 14.06.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Swinject +import SwiftUI + +struct ContributeFactory { + private let assembler: Assembler + + init(parent: Assembler) { + assembler = .init([ContributeAssembly()], parent: parent) + } + + func makeViewController() -> UIViewController { + UIHostingController( + rootView: ContributeView( + viewModel: assembler.resolve(ContributeViewModel.self)! + ) + ) + } +} + +private struct ContributeAssembly: Assembly { + func assemble(container: Container) { + container.register(ContributeViewModel.self) { + ContributeViewModel( + crashliticsService: $0.resolve(CrashlyticsService.self)! + ) + }.inObjectScope(.weak) + } +} diff --git a/Adamant/Stories/Settings/Contribute/ContributeState.swift b/Adamant/Modules/Settings/Contribute/ContributeState.swift similarity index 98% rename from Adamant/Stories/Settings/Contribute/ContributeState.swift rename to Adamant/Modules/Settings/Contribute/ContributeState.swift index fd26de230..cc78b0eea 100644 --- a/Adamant/Stories/Settings/Contribute/ContributeState.swift +++ b/Adamant/Modules/Settings/Contribute/ContributeState.swift @@ -12,7 +12,7 @@ import CommonKit struct ContributeState { var isCrashlyticsOn: Bool var isCrashButtonOn: Bool - var safariURL: IdentifiableContainer? + var safariURL: IDWrapper? let name: String let crashliticsRowImage: UIImage diff --git a/Adamant/Stories/Settings/Contribute/ContributeView.swift b/Adamant/Modules/Settings/Contribute/ContributeView.swift similarity index 100% rename from Adamant/Stories/Settings/Contribute/ContributeView.swift rename to Adamant/Modules/Settings/Contribute/ContributeView.swift diff --git a/Adamant/Stories/Settings/Contribute/ContributeViewModel.swift b/Adamant/Modules/Settings/Contribute/ContributeViewModel.swift similarity index 80% rename from Adamant/Stories/Settings/Contribute/ContributeViewModel.swift rename to Adamant/Modules/Settings/Contribute/ContributeViewModel.swift index 32e042772..ec6be779d 100644 --- a/Adamant/Stories/Settings/Contribute/ContributeViewModel.swift +++ b/Adamant/Modules/Settings/Contribute/ContributeViewModel.swift @@ -17,14 +17,9 @@ final class ContributeViewModel: ObservableObject { @Published var state: ContributeState = .initial - init(crashliticsService: CrashlyticsService) { + nonisolated init(crashliticsService: CrashlyticsService) { self.crashliticsService = crashliticsService - state.isCrashlyticsOn = crashliticsService.isCrashlyticsEnabled() - - $state.map(\.isCrashlyticsOn) - .removeDuplicates() - .sink { [weak crashliticsService] in crashliticsService?.setCrashlyticsEnabled($0) } - .store(in: &subscriptions) + Task { await setup() } } func enableCrashButton() { @@ -34,10 +29,21 @@ final class ContributeViewModel: ObservableObject { } func openLink(row: ContributeState.LinkRow) { - state.safariURL = row.link.map { .init(value: $0) } + state.safariURL = row.link.map { .init(id: $0.absoluteString, value: $0) } } func simulateCrash() { fatalError("Test crash") } } + +private extension ContributeViewModel { + func setup() { + state.isCrashlyticsOn = crashliticsService.isCrashlyticsEnabled() + + $state.map(\.isCrashlyticsOn) + .removeDuplicates() + .sink { [weak crashliticsService] in crashliticsService?.setCrashlyticsEnabled($0) } + .store(in: &subscriptions) + } +} diff --git a/Adamant/Stories/Settings/NotificationSoundsViewController.swift b/Adamant/Modules/Settings/NotificationSoundsViewController.swift similarity index 98% rename from Adamant/Stories/Settings/NotificationSoundsViewController.swift rename to Adamant/Modules/Settings/NotificationSoundsViewController.swift index 2aca3ba84..4d5f1e2b8 100644 --- a/Adamant/Stories/Settings/NotificationSoundsViewController.swift +++ b/Adamant/Modules/Settings/NotificationSoundsViewController.swift @@ -11,7 +11,7 @@ import Eureka import AudioToolbox import AVFoundation -class NotificationSoundsViewController: FormViewController { +final class NotificationSoundsViewController: FormViewController { // MARK: Sections & Rows enum Sections { diff --git a/Adamant/Stories/Settings/NotificationsViewController.swift b/Adamant/Modules/Settings/NotificationsViewController.swift similarity index 99% rename from Adamant/Stories/Settings/NotificationsViewController.swift rename to Adamant/Modules/Settings/NotificationsViewController.swift index e6d100d66..66d1b2e58 100644 --- a/Adamant/Stories/Settings/NotificationsViewController.swift +++ b/Adamant/Modules/Settings/NotificationsViewController.swift @@ -13,7 +13,7 @@ import MarkdownKit import ProcedureKit import CommonKit -class NotificationsViewController: FormViewController { +final class NotificationsViewController: FormViewController { // MARK: Sections & Rows enum Sections { diff --git a/Adamant/Stories/Settings/PKGeneratorViewController.swift b/Adamant/Modules/Settings/PKGeneratorViewController.swift similarity index 93% rename from Adamant/Stories/Settings/PKGeneratorViewController.swift rename to Adamant/Modules/Settings/PKGeneratorViewController.swift index 53ad3aa19..34476f29a 100644 --- a/Adamant/Stories/Settings/PKGeneratorViewController.swift +++ b/Adamant/Modules/Settings/PKGeneratorViewController.swift @@ -25,7 +25,7 @@ extension String.adamant { } // MARK: - -class PKGeneratorViewController: FormViewController { +final class PKGeneratorViewController: FormViewController { // MARK: Dependencies var accountService: AccountService! @@ -105,17 +105,21 @@ class PKGeneratorViewController: FormViewController { cell.textView.attributedText = mutableText } - let passphraseRow = TextAreaRow { + let passphraseRow = PasswordRow { $0.placeholder = String.adamant.qrGenerator.passphrasePlaceholder $0.tag = Rows.passphrase.tag - $0.textAreaHeight = .dynamic(initialTextViewHeight: 28.0) // 28 for textView and 8+8 for insets + $0.cell.textField.enablePasswordToggle() } let generateButton = ButtonRow { $0.title = String.adamant.pkGenerator.generateButton $0.tag = Rows.generateButton.tag }.onCellSelection { [weak self] (_, row) in - guard let row: TextAreaRow = self?.form.rowBy(tag: Rows.passphrase.tag), let passphrase = row.value else { + guard let row: PasswordRow = self?.form.rowBy(tag: Rows.passphrase.tag), + let passphrase = row.value, + AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase) + else { + self?.dialogService.showToastMessage(String.adamant.qrGenerator.wrongPassphraseError) return } diff --git a/Adamant/Stories/Settings/PrivateKeyGenerator.swift b/Adamant/Modules/Settings/PrivateKeyGenerator.swift similarity index 100% rename from Adamant/Stories/Settings/PrivateKeyGenerator.swift rename to Adamant/Modules/Settings/PrivateKeyGenerator.swift diff --git a/Adamant/Stories/Settings/QRGeneratorViewController.swift b/Adamant/Modules/Settings/QRGeneratorViewController.swift similarity index 93% rename from Adamant/Stories/Settings/QRGeneratorViewController.swift rename to Adamant/Modules/Settings/QRGeneratorViewController.swift index 21474685c..4a5438b00 100644 --- a/Adamant/Stories/Settings/QRGeneratorViewController.swift +++ b/Adamant/Modules/Settings/QRGeneratorViewController.swift @@ -28,7 +28,7 @@ extension String.adamant { } // MARK: - -class QRGeneratorViewController: FormViewController { +final class QRGeneratorViewController: FormViewController { // MARK: Dependencies var dialogService: DialogService! @@ -137,10 +137,10 @@ class QRGeneratorViewController: FormViewController { // MARK: Passphrase section form +++ Section { $0.tag = Sections.passphrase.tag } - <<< TextAreaRow { + <<< PasswordRow { $0.placeholder = String.adamant.qrGenerator.passphrasePlaceholder + $0.cell.textField.enablePasswordToggle() $0.tag = Rows.passphrase.tag - $0.textAreaHeight = .dynamic(initialTextViewHeight: 28.0) // 28 for textView and 8+8 for insets } <<< ButtonRow { @@ -180,10 +180,11 @@ class QRGeneratorViewController: FormViewController { // MARK: - QR Tools extension QRGeneratorViewController { func generateQr() { - guard let row: TextAreaRow = form.rowBy(tag: Rows.passphrase.tag), - let passphrase = row.value?.lowercased(), // Lowercased! - AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase) else { - dialogService.showToastMessage(String.adamant.qrGenerator.wrongPassphraseError) + guard let row: PasswordRow = form.rowBy(tag: Rows.passphrase.tag), + let passphrase = row.value?.lowercased(), + AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase) + else { + dialogService.showToastMessage(String.adamant.qrGenerator.wrongPassphraseError) return } diff --git a/Adamant/Stories/Settings/SecurityViewController+StayIn.swift b/Adamant/Modules/Settings/SecurityViewController+StayIn.swift similarity index 100% rename from Adamant/Stories/Settings/SecurityViewController+StayIn.swift rename to Adamant/Modules/Settings/SecurityViewController+StayIn.swift diff --git a/Adamant/Stories/Settings/SecurityViewController+notifications.swift b/Adamant/Modules/Settings/SecurityViewController+notifications.swift similarity index 100% rename from Adamant/Stories/Settings/SecurityViewController+notifications.swift rename to Adamant/Modules/Settings/SecurityViewController+notifications.swift diff --git a/Adamant/Stories/Settings/SecurityViewController.swift b/Adamant/Modules/Settings/SecurityViewController.swift similarity index 97% rename from Adamant/Stories/Settings/SecurityViewController.swift rename to Adamant/Modules/Settings/SecurityViewController.swift index ddf9928f5..914aeb37b 100644 --- a/Adamant/Stories/Settings/SecurityViewController.swift +++ b/Adamant/Modules/Settings/SecurityViewController.swift @@ -33,7 +33,7 @@ extension NotificationsMode: CustomStringConvertible { } // MARK: - SecurityViewController -class SecurityViewController: FormViewController { +final class SecurityViewController: FormViewController { enum PinpadRequest { case createPin @@ -101,7 +101,7 @@ class SecurityViewController: FormViewController { var dialogService: DialogService! var notificationsService: NotificationsService! var localAuth: LocalAuthentication! - var router: Router! + var screensFactory: ScreensFactory! // MARK: - Properties var showLoggedInOptions: Bool { @@ -137,11 +137,8 @@ class SecurityViewController: FormViewController { }.cellUpdate { (cell, _) in cell.accessoryType = .disclosureIndicator }.onCellSelection { [weak self] (_, _) in - guard let nav = self?.navigationController, let vc = self?.router.get(scene: AdamantScene.Settings.qRGenerator) else { - return - } - - nav.pushViewController(vc, animated: true) + guard let vc = self?.screensFactory.makeQRGenerator() else { return } + self?.navigationController?.pushViewController(vc, animated: true) } // Stay logged in diff --git a/Adamant/Modules/Settings/SettingsFactory.swift b/Adamant/Modules/Settings/SettingsFactory.swift new file mode 100644 index 000000000..bf74ee0e5 --- /dev/null +++ b/Adamant/Modules/Settings/SettingsFactory.swift @@ -0,0 +1,61 @@ +// +// SettingsFactory.swift +// Adamant +// +// Created by Anokhov Pavel on 01.02.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import CommonKit +import Swinject + +struct SettingsFactory { + let assembler: Assembler + + func makeSecurityVC(screensFactory: ScreensFactory) -> UIViewController { + let c = SecurityViewController() + c.accountService = assembler.resolve(AccountService.self) + c.dialogService = assembler.resolve(DialogService.self) + c.notificationsService = assembler.resolve(NotificationsService.self) + c.localAuth = assembler.resolve(LocalAuthentication.self) + c.screensFactory = screensFactory + return c + } + + func makeQRGeneratorVC() -> UIViewController { + let c = QRGeneratorViewController() + c.dialogService = assembler.resolve(DialogService.self) + return c + } + + func makePKGeneratorVC() -> UIViewController { + let c = PKGeneratorViewController() + c.dialogService = assembler.resolve(DialogService.self) + c.accountService = assembler.resolve(AccountService.self) + return c + } + + func makeAboutVC(screensFactory: ScreensFactory) -> UIViewController { + let c = AboutViewController() + c.accountService = assembler.resolve(AccountService.self) + c.accountsProvider = assembler.resolve(AccountsProvider.self) + c.dialogService = assembler.resolve(DialogService.self) + c.screensFactory = screensFactory + return c + } + + func makeNotificationsVC() -> UIViewController { + let c = NotificationsViewController() + c.notificationsService = assembler.resolve(NotificationsService.self) + c.dialogService = assembler.resolve(DialogService.self) + return c + } + + func makeVisibleWalletsVC() -> UIViewController { + VisibleWalletsViewController( + visibleWalletsService: assembler.resolve(VisibleWalletsService.self)!, + accountService: assembler.resolve(AccountService.self)! + ) + } +} diff --git a/Adamant/Stories/Settings/VisibleWallets/VisibleWalletsCheckmarkView.swift b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsCheckmarkView.swift similarity index 100% rename from Adamant/Stories/Settings/VisibleWallets/VisibleWalletsCheckmarkView.swift rename to Adamant/Modules/Settings/VisibleWallets/VisibleWalletsCheckmarkView.swift diff --git a/Adamant/Stories/Settings/VisibleWallets/VisibleWalletsResetTableViewCell.swift b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsResetTableViewCell.swift similarity index 93% rename from Adamant/Stories/Settings/VisibleWallets/VisibleWalletsResetTableViewCell.swift rename to Adamant/Modules/Settings/VisibleWallets/VisibleWalletsResetTableViewCell.swift index d54024c40..f4527162b 100644 --- a/Adamant/Stories/Settings/VisibleWallets/VisibleWalletsResetTableViewCell.swift +++ b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsResetTableViewCell.swift @@ -9,7 +9,7 @@ import UIKit import CommonKit -class VisibleWalletsResetTableViewCell: UITableViewCell { +final class VisibleWalletsResetTableViewCell: UITableViewCell { private lazy var titleLabel: UILabel = { let label = UILabel() diff --git a/Adamant/Stories/Settings/VisibleWallets/VisibleWalletsTableViewCell.swift b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsTableViewCell.swift similarity index 97% rename from Adamant/Stories/Settings/VisibleWallets/VisibleWalletsTableViewCell.swift rename to Adamant/Modules/Settings/VisibleWallets/VisibleWalletsTableViewCell.swift index 1d6eba394..0e5490cd5 100644 --- a/Adamant/Stories/Settings/VisibleWallets/VisibleWalletsTableViewCell.swift +++ b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsTableViewCell.swift @@ -15,7 +15,7 @@ protocol AdamantVisibleWalletsCellDelegate: AnyObject { } // MARK: - Cell -class VisibleWalletsTableViewCell: UITableViewCell { +final class VisibleWalletsTableViewCell: UITableViewCell { private let checkmarkRowView = VisibleWalletsCheckmarkRowView() weak var delegate: AdamantVisibleWalletsCellDelegate? { diff --git a/Adamant/Stories/Settings/VisibleWallets/VisibleWalletsViewController.swift b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsViewController.swift similarity index 99% rename from Adamant/Stories/Settings/VisibleWallets/VisibleWalletsViewController.swift rename to Adamant/Modules/Settings/VisibleWallets/VisibleWalletsViewController.swift index 6665e9a3e..8003142d6 100644 --- a/Adamant/Stories/Settings/VisibleWallets/VisibleWalletsViewController.swift +++ b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsViewController.swift @@ -229,7 +229,7 @@ extension VisibleWalletsViewController: UITableViewDataSource, UITableViewDelega cell.backgroundColor = UIColor.adamant.cellColor cell.title = wallet.tokenName - cell.caption = !isToken ? "Blockchain" : wallet.tokenNetworkSymbol + cell.caption = !isToken ? "Blockchain" : type(of: wallet).tokenNetworkSymbol cell.subtitle = wallet.tokenSymbol cell.logoImage = wallet.tokenLogo cell.balance = wallet.wallet?.balance diff --git a/Adamant/Modules/ShareQR/ShareQRFactory.swift b/Adamant/Modules/ShareQR/ShareQRFactory.swift new file mode 100644 index 000000000..eb30dd85b --- /dev/null +++ b/Adamant/Modules/ShareQR/ShareQRFactory.swift @@ -0,0 +1,18 @@ +// +// ShareQRFactory.swift +// Adamant +// +// Created by Anokhov Pavel on 17.03.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Swinject +import UIKit + +struct ShareQRFactory { + let assembler: Assembler + + func makeViewController() -> ShareQrViewController { + ShareQrViewController(dialogService: assembler.resolve(DialogService.self)!) + } +} diff --git a/Adamant/Stories/Shared/ShareQrViewController.swift b/Adamant/Modules/ShareQR/ShareQrViewController.swift similarity index 94% rename from Adamant/Stories/Shared/ShareQrViewController.swift rename to Adamant/Modules/ShareQR/ShareQrViewController.swift index bb47803de..7eab26f34 100644 --- a/Adamant/Stories/Shared/ShareQrViewController.swift +++ b/Adamant/Modules/ShareQR/ShareQrViewController.swift @@ -15,9 +15,9 @@ extension String.adamant.shared { static let photolibraryNotAuthorized = String.localized("ShareQR.photolibraryNotAuthorized", comment: "ShareQR scene: User had not authorized access to write images to photolibrary") } -class ShareQrViewController: FormViewController { +final class ShareQrViewController: FormViewController { // MARK: - Dependencies - var dialogService: DialogService! + private let dialogService: DialogService // MARK: - Rows private enum Rows { @@ -79,6 +79,15 @@ class ShareQrViewController: FormViewController { var excludedActivityTypes: [UIActivity.ActivityType]? + init(dialogService: DialogService) { + self.dialogService = dialogService + super.init(nibName: "ShareQrViewController", bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() diff --git a/Adamant/Stories/Shared/ShareQrViewController.xib b/Adamant/Modules/ShareQR/ShareQrViewController.xib similarity index 100% rename from Adamant/Stories/Shared/ShareQrViewController.xib rename to Adamant/Modules/ShareQR/ShareQrViewController.xib diff --git a/Adamant/Stories/SwiftyOnboard/SwiftyOnboard.swift b/Adamant/Modules/SwiftyOnboard/SwiftyOnboard.swift similarity index 100% rename from Adamant/Stories/SwiftyOnboard/SwiftyOnboard.swift rename to Adamant/Modules/SwiftyOnboard/SwiftyOnboard.swift diff --git a/Adamant/Stories/SwiftyOnboard/SwiftyOnboardOverlay.swift b/Adamant/Modules/SwiftyOnboard/SwiftyOnboardOverlay.swift similarity index 100% rename from Adamant/Stories/SwiftyOnboard/SwiftyOnboardOverlay.swift rename to Adamant/Modules/SwiftyOnboard/SwiftyOnboardOverlay.swift diff --git a/Adamant/Stories/SwiftyOnboard/SwiftyOnboardPage.swift b/Adamant/Modules/SwiftyOnboard/SwiftyOnboardPage.swift similarity index 100% rename from Adamant/Stories/SwiftyOnboard/SwiftyOnboardPage.swift rename to Adamant/Modules/SwiftyOnboard/SwiftyOnboardPage.swift diff --git a/Adamant/Modules/TestVibration/VibrationSelectionFactory.swift b/Adamant/Modules/TestVibration/VibrationSelectionFactory.swift new file mode 100644 index 000000000..3c3e87b05 --- /dev/null +++ b/Adamant/Modules/TestVibration/VibrationSelectionFactory.swift @@ -0,0 +1,36 @@ +// +// VibrationSelectionFactory.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 07.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Swinject +import SwiftUI + +struct VibrationSelectionFactory { + private let assembler: Assembler + + init(parent: Assembler) { + assembler = .init([VibrationSelectionAssembly()], parent: parent) + } + + func makeViewController() -> UIViewController { + UIHostingController( + rootView: VibrationSelectionView( + viewModel: assembler.resolve(VibrationSelectionViewModel.self)! + ) + ) + } +} + +private struct VibrationSelectionAssembly: Assembly { + func assemble(container: Container) { + container.register(VibrationSelectionViewModel.self) { + VibrationSelectionViewModel( + vibroService: $0.resolve(VibroService.self)! + ) + }.inObjectScope(.weak) + } +} diff --git a/Adamant/Modules/TestVibration/VibrationSelectionView.swift b/Adamant/Modules/TestVibration/VibrationSelectionView.swift new file mode 100644 index 000000000..cebabd5df --- /dev/null +++ b/Adamant/Modules/TestVibration/VibrationSelectionView.swift @@ -0,0 +1,56 @@ +// +// VibrationSelectionView.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 07.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import SwiftUI +import CommonKit + +struct VibrationSelectionView: View { + @StateObject var viewModel: VibrationSelectionViewModel + + init(viewModel: VibrationSelectionViewModel) { + _viewModel = .init(wrappedValue: viewModel) + } + + var body: some View { + List { + ForEach(AdamantVibroType.allCases, id: \.self) { type in + Button { + viewModel.type = type + } label: { + Text(vibrationTypeDescription(type)) + } + } + } + .withoutListBackground() + .background(Color(.adamant.secondBackgroundColor)) + .navigationTitle("Vibrations") + } + + private func vibrationTypeDescription(_ type: AdamantVibroType) -> String { + switch type { + case .light: + return "Single-Short-Light (1SL Vibartion)" + case .rigid: + return "Single-Short-Rigid (1SR Vibartion)" + case .heavy: + return "Single-Long-Rigid (1LR Vibartion)" + case .medium: + return "Single-Short-Medium (1SM Vibartion)" + case .soft: + return "Single-Long-Soft (1LS Vibartion)" + case .selection: + return "Single-Short-Soft (1SS Vibartion)" + case .success: + return "Double-Short-Medium (2SM Vibartion)" + case .warning: + return "Double-Long-Medium (2LM Vibartion)" + case .error: + return "Tripple-Long-Medium (3LM Vibartion)" + } + } +} diff --git a/Adamant/Modules/TestVibration/VibrationSelectionViewModel.swift b/Adamant/Modules/TestVibration/VibrationSelectionViewModel.swift new file mode 100644 index 000000000..795fab61a --- /dev/null +++ b/Adamant/Modules/TestVibration/VibrationSelectionViewModel.swift @@ -0,0 +1,35 @@ +// +// VibrationSelectionViewModel.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 07.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import SwiftUI +import Combine +import CommonKit + +@MainActor +final class VibrationSelectionViewModel: ObservableObject { + private let vibroService: VibroService + private var subscriptions = Set() + + @Published var type: AdamantVibroType? + + nonisolated init(vibroService: VibroService) { + self.vibroService = vibroService + Task { + await self.setup() + } + } +} + +private extension VibrationSelectionViewModel { + func setup() { + $type + .compactMap { $0 } + .sink { [weak vibroService] in vibroService?.applyVibration($0) } + .store(in: &subscriptions) + } +} diff --git a/Adamant/Wallets/Adamant/AdmTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Adamant/AdmTransactionDetailsViewController.swift similarity index 64% rename from Adamant/Wallets/Adamant/AdmTransactionDetailsViewController.swift rename to Adamant/Modules/Wallets/Adamant/AdmTransactionDetailsViewController.swift index cc0d2d236..072bee533 100644 --- a/Adamant/Wallets/Adamant/AdmTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmTransactionDetailsViewController.swift @@ -10,12 +10,12 @@ import UIKit import Eureka import CommonKit -class AdmTransactionDetailsViewController: TransactionDetailsViewControllerBase { +final class AdmTransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies let transfersProvider: TransfersProvider - let router: Router + let screensFactory: ScreensFactory // MARK: - Properties private let autoupdateInterval: TimeInterval = 5.0 @@ -31,24 +31,35 @@ class AdmTransactionDetailsViewController: TransactionDetailsViewControllerBase return control }() + override var transaction: TransactionDetails? { + get { super.transaction } + set { assertionFailure("Use adamant transaction") } + } + + var adamantTransaction: AdamantTransactionDetails? { + get { super.transaction as? AdamantTransactionDetails } + set { super.transaction = newValue } + } + // MARK: - Lifecycle init( accountService: AccountService, transfersProvider: TransfersProvider, - router: Router, + screensFactory: ScreensFactory, dialogService: DialogService, currencyInfo: CurrencyInfoService, addressBookService: AddressBookService ) { self.transfersProvider = transfersProvider - self.router = router + self.screensFactory = screensFactory super.init( dialogService: dialogService, currencyInfo: currencyInfo, addressBookService: addressBookService, - accountService: accountService + accountService: accountService, + walletService: nil ) } @@ -61,37 +72,25 @@ class AdmTransactionDetailsViewController: TransactionDetailsViewControllerBase super.viewDidLoad() - if showToChat { - let haveChatroom: Bool - - if let transfer = transaction as? TransferTransaction, let partner = transfer.partner as? CoreDataAccount, let chatroom = partner.chatroom, let transactions = chatroom.transactions { - let messeges = transactions.first(where: { (object) -> Bool in - return !(object is TransferTransaction) - }) - - haveChatroom = messeges != nil - } else { - haveChatroom = false - } - - let chatLabel = haveChatroom ? String.adamant.transactionList.toChat : String.adamant.transactionList.startChat + if showToChat, + adamantTransaction?.chatRoom != nil, + let section = form.sectionBy(tag: Sections.actions.tag) { + let chatLabel = String.adamant.transactionList.toChat // MARK: Open chat - if let trs = transaction as? TransferTransaction, trs.chatroom != nil, let section = form.sectionBy(tag: Sections.actions.tag) { - let row = LabelRow { - $0.tag = Rows.openChat.tag - $0.title = chatLabel - $0.cell.imageView?.image = Rows.openChat.image - }.cellSetup { (cell, _) in - cell.selectionStyle = .gray - }.cellUpdate { (cell, _) in - cell.accessoryType = .disclosureIndicator - }.onCellSelection { [weak self] (_, _) in - self?.goToChat() - } - - section.append(row) + let row = LabelRow { + $0.tag = Rows.openChat.tag + $0.title = chatLabel + $0.cell.imageView?.image = Rows.openChat.image + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, _) in + self?.goToChat() } + + section.append(row) } tableView.refreshControl = refreshControl @@ -114,16 +113,7 @@ class AdmTransactionDetailsViewController: TransactionDetailsViewControllerBase } func goToChat() { - guard let transfer = transaction as? TransferTransaction else { - return - } - - guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { - dialogService.showError(withMessage: "AdmTransactionDetailsViewController: Failed to get ChatViewController", supportEmail: true, error: nil) - return - } - - guard let chatroom = transfer.chatroom else { + guard let chatroom = adamantTransaction?.chatRoom else { dialogService.showError(withMessage: "AdmTransactionDetailsViewController: Failed to get chatroom for transaction.", supportEmail: true, error: nil) return } @@ -133,6 +123,7 @@ class AdmTransactionDetailsViewController: TransactionDetailsViewControllerBase return } + let vc = screensFactory.makeChat() vc.hidesBottomBarWhenPushed = true vc.viewModel.setup( account: account, @@ -158,6 +149,7 @@ class AdmTransactionDetailsViewController: TransactionDetailsViewControllerBase do { try await transfersProvider.refreshTransfer(id: id) + adamantTransaction = await transfersProvider.getTransfer(id: id) refreshControl.endRefreshing() tableView.reloadData() } catch { diff --git a/Adamant/Modules/Wallets/Adamant/AdmTransactionsViewController.swift b/Adamant/Modules/Wallets/Adamant/AdmTransactionsViewController.swift new file mode 100644 index 000000000..35c42d1f2 --- /dev/null +++ b/Adamant/Modules/Wallets/Adamant/AdmTransactionsViewController.swift @@ -0,0 +1,331 @@ +// +// AdmTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 26/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import CoreData +import CommonKit + +final class AdmTransactionsViewController: TransactionsListViewControllerBase { + // MARK: - Dependencies + + let accountService: AccountService + let transfersProvider: TransfersProvider + let chatsProvider: ChatsProvider + let stack: CoreDataStack + let screensFactory: ScreensFactory + let addressBookService: AddressBookService + let admService: AdmWalletService + + // MARK: - Properties + + var controller: NSFetchedResultsController? + + /* + In SplitViewController on iPhones, viewController can still present in memory, but not on screen. + In this cases not visible viewController will still mark messages isUnread = false + */ + /// ViewController currently is ontop of the screen. + private var isOnTop = false + private let transactionsPerRequest = 100 + + // MARK: - Lifecycle + + init( + nibName nibNameOrNil: String?, + bundle nibBundleOrNil: Bundle?, + accountService: AccountService, + transfersProvider: TransfersProvider, + chatsProvider: ChatsProvider, + dialogService: DialogService, + stack: CoreDataStack, + screensFactory: ScreensFactory, + addressBookService: AddressBookService, + admService: AdmWalletService + ) { + self.accountService = accountService + self.transfersProvider = transfersProvider + self.chatsProvider = chatsProvider + self.stack = stack + self.screensFactory = screensFactory + self.addressBookService = addressBookService + self.admService = admService + + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + self.dialogService = dialogService + self.walletService = admService + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + if accountService.account != nil { + reloadData() + } + + setupObserver() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + isOnTop = true + markTransfersAsRead() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + isOnTop = false + } + + // MARK: - Overrides + + @MainActor + override func reloadData() { + Task { + controller = await transfersProvider.transfersController() + + do { + try controller?.performFetch() + let transactions: [SimpleTransactionDetails] = controller?.fetchedObjects?.compactMap { + getTransactionDetails(by: $0) + } ?? [] + + update(transactions) + } catch { + dialogService.showError(withMessage: "Failed to get transactions. Please, report a bug", supportEmail: true, error: error) + controller = nil + } + + isBusy = false + } + } + + @MainActor + override func handleRefresh() { + Task { + self.isBusy = true + self.emptyLabel.isHidden = true + + let result = await self.transfersProvider.update() + + guard let result = result else { + refreshControl.endRefreshing() + return + } + + switch result { + case .success: + refreshControl.endRefreshing() + tableView.reloadData() + + case .failure(let error): + refreshControl.endRefreshing() + + dialogService.showRichError(error: error) + } + + self.isBusy = false + }.stored(in: taskManager) + } + + override func loadData(silent: Bool) { + isBusy = true + emptyLabel.isHidden = true + + guard let address = accountService.account?.address else { + return + } + + Task { @MainActor in + do { + let count = try await transfersProvider.getTransactions( + forAccount: address, + type: .send, + offset: transfersProvider.offsetTransactions, + limit: transactionsPerRequest, + orderByTime: true + ) + + if count > 0 { + await transfersProvider.updateOffsetTransactions( + transfersProvider.offsetTransactions + transactionsPerRequest + ) + } + + isNeedToLoadMoore = count >= transactionsPerRequest + } catch { + isNeedToLoadMoore = false + + if !silent { + dialogService.showRichError(error: error) + } + } + + isBusy = false + emptyLabel.isHidden = !transactions.isEmpty + refreshControl.endRefreshing() + stopBottomIndicator() + }.stored(in: taskManager) + } + + private func markTransfersAsRead() { + let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + privateContext.parent = self.stack.container.viewContext + + let request = NSFetchRequest(entityName: TransferTransaction.entityName) + request.predicate = NSPredicate(format: "isUnread == true") + request.sortDescriptors = [NSSortDescriptor(key: "transactionId", ascending: false)] + + if let result = try? privateContext.fetch(request) { + result.forEach { $0.isUnread = false } + + if privateContext.hasChanges { + try? privateContext.save() + } + } + } + + func getTransactionDetails(by transaction: TransferTransaction) -> SimpleTransactionDetails { + let partnerId = ( + transaction.isOutgoing + ? transaction.recipientId + : transaction.senderId + ) ?? "" + + var simple = SimpleTransactionDetails(transaction) + simple.partnerName = getPartnerName(for: partnerId) + return simple + } + + func getPartnerName(for partnerId: String) -> String? { + var partnerName = addressBookService.getName(for: partnerId) + + if let address = accountService.account?.address, + partnerId == address { + partnerName = String.adamant.transactionDetails.yourAddress + } + + return partnerName + } + + // MARK: - UITableView + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let transaction = transactions[safe: indexPath.row] else { return } + + let controller = screensFactory.makeAdmTransactionDetails() + controller.adamantTransaction = transaction + controller.comment = transaction.comment + controller.showToChat = transaction.showToChat ?? false + + if let address = accountService.account?.address { + let partnerName = transaction.partnerName + + if address == transaction.senderAddress { + controller.senderName = String.adamant.transactionDetails.yourAddress + } else { + controller.senderName = partnerName + } + + if address == transaction.recipientAddress { + controller.recipientName = String.adamant.transactionDetails.yourAddress + } else { + controller.recipientName = partnerName + } + } + + navigationController?.pushViewController(controller, animated: true) + } + + func tableView( + _ tableView: UITableView, + trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath + ) -> UISwipeActionsConfiguration? { + guard let transaction = transactions[safe: indexPath.row], + transaction.showToChat == true, + let chatroom = transaction.chatRoom + else { + return nil + } + + let toChat = UIContextualAction(style: .normal, title: "") { [weak self] (_, _, _) in + guard + let self = self, + let account = accountService.account + else { return } + + let vc = screensFactory.makeChat() + vc.hidesBottomBarWhenPushed = true + vc.viewModel.setup( + account: account, + chatroom: chatroom, + messageIdToShow: nil, + preservationDelegate: nil + ) + + if let nav = self.navigationController { + nav.pushViewController(vc, animated: true) + } else { + vc.modalPresentationStyle = .overFullScreen + present(vc, animated: true) + } + } + + toChat.image = .asset(named: "chats_tab") + toChat.backgroundColor = UIColor.adamant.primary + return UISwipeActionsConfiguration(actions: [toChat]) + } + + private func toShowChat(for transaction: TransferTransaction) -> Bool { + guard let partner = transaction.partner as? CoreDataAccount, let chatroom = partner.chatroom, !chatroom.isReadonly else { + return false + } + + return true + } +} + +private extension AdmTransactionsViewController { + func setupObserver() { + NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, + object: stack.container.viewContext + ) + .sink { [weak self] notification in + guard let self = self else { return } + + let changes = notification.managedObjectContextChanges(of: TransferTransaction.self) + + if let inserted = changes.inserted, !inserted.isEmpty { + let maped: [SimpleTransactionDetails] = inserted.map { + self.getTransactionDetails(by: $0) + } + + var transactions = self.transactions + transactions.append(contentsOf: maped) + self.update(transactions) + } + + if let updated = changes.updated, !updated.isEmpty { + updated.forEach { transaction in + guard let index = self.transactions.firstIndex(where: { + $0.txId == transaction.txId + }) + else { return } + var transactions: [SimpleTransactionDetails] = self.transactions + transactions[index] = self.getTransactionDetails(by: transaction) + self.update(transactions) + } + } + } + .store(in: &subscriptions) + } +} diff --git a/Adamant/Wallets/Adamant/AdmTransferViewController.swift b/Adamant/Modules/Wallets/Adamant/AdmTransferViewController.swift similarity index 95% rename from Adamant/Wallets/Adamant/AdmTransferViewController.swift rename to Adamant/Modules/Wallets/Adamant/AdmTransferViewController.swift index f41195e05..c2fe5999c 100644 --- a/Adamant/Wallets/Adamant/AdmTransferViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmTransferViewController.swift @@ -128,26 +128,31 @@ final class AdmTransferViewController: TransferViewControllerBase { } } - private func openDetailVC(result: TransactionDetails, vc: AdmTransferViewController, recipient: String, comments: String) { - let detailsVC = router.get(scene: AdamantScene.Wallets.Adamant.transactionDetails) as? AdmTransactionDetailsViewController - detailsVC?.transaction = result + private func openDetailVC( + result: AdamantTransactionDetails, + vc: AdmTransferViewController, + recipient: String, + comments: String + ) { + let detailsVC = screensFactory.makeAdmTransactionDetails() + detailsVC.adamantTransaction = result if comments.count > 0 { - detailsVC?.comment = comments + detailsVC.comment = comments } // MARK: Sender, you - detailsVC?.senderName = String.adamant.transactionDetails.yourAddress + detailsVC.senderName = String.adamant.transactionDetails.yourAddress // MARK: Get recipient if let recipientName = recipientName { - detailsVC?.recipientName = recipientName + detailsVC.recipientName = recipientName vc.delegate?.transferViewController(vc, didFinishWithTransfer: result, detailsViewController: detailsVC) } else { - Task { + Task { do { let account = try await accountsProvider.getAccount(byAddress: recipient) - detailsVC?.recipientName = account.name + detailsVC.recipientName = account.name vc.delegate?.transferViewController(vc, didFinishWithTransfer: result, detailsViewController: detailsVC) } catch { vc.delegate?.transferViewController(vc, didFinishWithTransfer: result, detailsViewController: detailsVC) diff --git a/Adamant/Wallets/Adamant/AdmWallet.swift b/Adamant/Modules/Wallets/Adamant/AdmWallet.swift similarity index 91% rename from Adamant/Wallets/Adamant/AdmWallet.swift rename to Adamant/Modules/Wallets/Adamant/AdmWallet.swift index 53a7e412f..804a31084 100644 --- a/Adamant/Wallets/Adamant/AdmWallet.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWallet.swift @@ -8,7 +8,7 @@ import Foundation -class AdmWallet: WalletAccount { +final class AdmWallet: WalletAccount { let address: String var balance: Decimal = 0 var notifications: Int = 0 diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift new file mode 100644 index 000000000..82772bc15 --- /dev/null +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift @@ -0,0 +1,96 @@ +// +// AdmWalletFactory.swift +// Adamant +// +// Created by Anokhov Pavel on 28.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Swinject +import UIKit + +struct AdmWalletFactory: WalletFactory { + typealias Service = AdmWalletService + + let assembler: Assembler + + func makeWalletVC(service: AdmWalletService, screensFactory: ScreensFactory) -> WalletViewController { + let c = AdmWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) + c.dialogService = assembler.resolve(DialogService.self) + c.currencyInfoService = assembler.resolve(CurrencyInfoService.self) + c.accountService = assembler.resolve(AccountService.self) + c.service = service + c.screensFactory = screensFactory + return c + } + + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { + AdmTransactionsViewController( + nibName: "TransactionsListViewControllerBase", + bundle: nil, + accountService: assembler.resolve(AccountService.self)!, + transfersProvider: assembler.resolve(TransfersProvider.self)!, + chatsProvider: assembler.resolve(ChatsProvider.self)!, + dialogService: assembler.resolve(DialogService.self)!, + stack: assembler.resolve(CoreDataStack.self)!, + screensFactory: screensFactory, + addressBookService: assembler.resolve(AddressBookService.self)!, + admService: service + ) + } + + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { + let vc = AdmTransferViewController( + chatsProvider: assembler.resolve(ChatsProvider.self)!, + accountService: assembler.resolve(AccountService.self)!, + accountsProvider: assembler.resolve(AccountsProvider.self)!, + dialogService: assembler.resolve(DialogService.self)!, + screensFactory: screensFactory, + currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, + vibroService: assembler.resolve(VibroService.self)! + ) + + vc.service = service + return vc + } + + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { nil } + + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { + fatalError("ScreensFactory in necessary for AdmTransactionDetailsViewController") + } + + func makeDetailsVC(screensFactory: ScreensFactory) -> AdmTransactionDetailsViewController { + makeTransactionDetailsVC(screensFactory: screensFactory) + } + + func makeDetailsVC(transaction: TransferTransaction, screensFactory: ScreensFactory) -> UIViewController { + let controller = makeTransactionDetailsVC(screensFactory: screensFactory) + controller.adamantTransaction = transaction + controller.comment = transaction.comment + controller.senderId = transaction.senderId + controller.recipientId = transaction.recipientId + return controller + } + + func makeBuyAndSellVC() -> UIViewController { + let c = BuyAndSellViewController() + c.accountService = assembler.resolve(AccountService.self) + c.dialogService = assembler.resolve(DialogService.self) + return c + } +} + +private extension AdmWalletFactory { + func makeTransactionDetailsVC(screensFactory: ScreensFactory) -> AdmTransactionDetailsViewController { + AdmTransactionDetailsViewController( + accountService: assembler.resolve(AccountService.self)!, + transfersProvider: assembler.resolve(TransfersProvider.self)!, + screensFactory: screensFactory, + dialogService: assembler.resolve(DialogService.self)!, + currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + addressBookService: assembler.resolve(AddressBookService.self)! + ) + } +} diff --git a/Adamant/Wallets/Adamant/AdmWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift similarity index 84% rename from Adamant/Wallets/Adamant/AdmWalletService+DynamicConstants.swift rename to Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift index e40e79e01..a98528bcb 100644 --- a/Adamant/Wallets/Adamant/AdmWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift @@ -45,16 +45,20 @@ extension AdmWalletService { 0 } + var minNodeVersion: String? { + "0.7.0" + } + static let explorerAddress = "https://explorer.adamant.im/tx/" static var nodes: [Node] { [ Node(url: URL(string: "https://clown.adamant.im")!), Node(url: URL(string: "https://lake.adamant.im")!), -Node(url: URL(string: "https://endless.adamant.im")!), +Node(url: URL(string: "https://endless.adamant.im")!, altUrl: URL(string: "http://149.102.157.15:36666")), Node(url: URL(string: "https://bid.adamant.im")!), Node(url: URL(string: "https://unusual.adamant.im")!), -Node(url: URL(string: "https://debate.adamant.im")!), +Node(url: URL(string: "https://debate.adamant.im")!, altUrl: URL(string: "http://95.216.161.113:36666")), Node(url: URL(string: "http://78.47.205.206:36666")!), Node(url: URL(string: "http://5.161.53.74:36666")!), Node(url: URL(string: "http://184.94.215.92:45555")!), diff --git a/Adamant/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift similarity index 73% rename from Adamant/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift rename to Adamant/Modules/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift index 48b0bb480..3d8cd67c2 100644 --- a/Adamant/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift @@ -42,24 +42,6 @@ extension AdmWalletService: RichMessageProvider { return } - func richMessageTapped(for transaction: TransferTransaction, in chat: ChatViewController) { - guard let controller = router.get(scene: AdamantScene.Wallets.Adamant.transactionDetails) as? TransactionDetailsViewControllerBase else { - fatalError("Can't get TransactionDetails scene") - } - - controller.transaction = transaction - controller.comment = transaction.comment - controller.senderId = transaction.senderId - controller.recipientId = transaction.recipientId - - if let nav = chat.navigationController { - nav.pushViewController(controller, animated: true) - } else { - controller.modalPresentationStyle = .overFullScreen - chat.present(controller, animated: true, completion: nil) - } - } - // MARK: Short description private static var formatter: NumberFormatter = { return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) diff --git a/Adamant/Wallets/Adamant/AdmWalletService+Send.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+Send.swift similarity index 77% rename from Adamant/Wallets/Adamant/AdmWalletService+Send.swift rename to Adamant/Modules/Wallets/Adamant/AdmWalletService+Send.swift index 82c32bc9d..e8db50d6e 100644 --- a/Adamant/Wallets/Adamant/AdmWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService+Send.swift @@ -13,21 +13,12 @@ extension AdmWalletService: WalletServiceSimpleSend { /// Transaction ID typealias T = Int - func transferViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Adamant.transfer) as? AdmTransferViewController else { - fatalError("Can't get AdmTransferViewController") - } - - vc.service = self - return vc - } - func sendMoney( recipient: String, amount: Decimal, comments: String, replyToMessageId: String? - ) async throws -> TransactionDetails { + ) async throws -> AdamantTransactionDetails { do { let transaction = try await transfersProvider.transferFunds( toAddress: recipient, @@ -40,9 +31,9 @@ extension AdmWalletService: WalletServiceSimpleSend { } catch let error as TransfersProviderError { throw error.asWalletServiceError() } catch { - throw WalletServiceError.internalError( - message: String.adamant.sharedErrors.unknownError, - error: nil + throw WalletServiceError.remoteServiceError( + message: error.localizedDescription, + error: error ) } } diff --git a/Adamant/Wallets/Adamant/AdmWalletService.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService.swift similarity index 77% rename from Adamant/Wallets/Adamant/AdmWalletService.swift rename to Adamant/Modules/Wallets/Adamant/AdmWalletService.swift index 8c8b98c03..8fddcbcb8 100644 --- a/Adamant/Wallets/Adamant/AdmWalletService.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService.swift @@ -14,7 +14,7 @@ import MessageKit import Combine import CommonKit -class AdmWalletService: NSObject, WalletService { +final class AdmWalletService: NSObject, WalletService { // MARK: - Constants let addressRegex = try! NSRegularExpression(pattern: "^U([0-9]{6,20})$") @@ -33,7 +33,7 @@ class AdmWalletService: NSObject, WalletService { return AdmWalletService.fixedFee } - var tokenNetworkSymbol: String { + static var tokenNetworkSymbol: String { return Self.currencySymbol } @@ -42,7 +42,7 @@ class AdmWalletService: NSObject, WalletService { } var tokenUnicID: String { - return tokenNetworkSymbol + tokenSymbol + Self.tokenNetworkSymbol + tokenSymbol } var richMessageType: String { @@ -57,7 +57,8 @@ class AdmWalletService: NSObject, WalletService { weak var accountService: AccountService? var apiService: ApiService! var transfersProvider: TransfersProvider! - var router: Router! + var coreDataStack: CoreDataStack! + var vibroService: VibroService! // MARK: - Notifications let walletUpdatedNotification = Notification.Name("adamant.admWallet.updated") @@ -71,22 +72,30 @@ class AdmWalletService: NSObject, WalletService { // MARK: - Properties let enabled: Bool = true - var walletViewController: WalletViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Adamant.wallet) as? AdmWalletViewController else { - fatalError("Can't get AdmWalletViewController") - } - - vc.service = self - return vc - } - private var transfersController: NSFetchedResultsController? - private (set) var isWarningGasPrice = false - private var subscriptions = Set() + @Atomic private(set) var isWarningGasPrice = false + @Atomic private var subscriptions = Set() + + @ObservableValue private(set) var transactions: [TransactionDetails] = [] + @ObservableValue private(set) var hasMoreOldTransactions: Bool = true + var transactionsPublisher: AnyObservable<[TransactionDetails]> { + $transactions.eraseToAnyPublisher() + } + + var hasMoreOldTransactionsPublisher: AnyObservable { + $hasMoreOldTransactions.eraseToAnyPublisher() + } + + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( + coinId: tokenUnicID, + coreDataStack: coreDataStack, + blockchainType: richMessageType + ) + // MARK: - State - private (set) var state: WalletServiceState = .upToDate - private (set) var wallet: WalletAccount? + @Atomic private(set) var state: WalletServiceState = .upToDate + @Atomic private(set) var wallet: WalletAccount? // MARK: - Logic override init() { @@ -129,14 +138,20 @@ class AdmWalletService: NSObject, WalletService { } let notify: Bool + + let isRaised: Bool + if let wallet = wallet as? AdmWallet { - wallet.isBalanceInitialized = true + isRaised = (wallet.balance < account.balance) && wallet.isBalanceInitialized if wallet.balance != account.balance { wallet.balance = account.balance notify = true + } else if wallet.isBalanceInitialized { + notify = true } else { notify = false } + wallet.isBalanceInitialized = true } else { let wallet = AdmWallet(address: account.address) wallet.isBalanceInitialized = true @@ -144,8 +159,12 @@ class AdmWalletService: NSObject, WalletService { self.wallet = wallet notify = true + isRaised = false } + if isRaised { + vibroService.applyVibration(.success) + } if notify, let wallet = wallet { postUpdateNotification(with: wallet) } @@ -153,7 +172,7 @@ class AdmWalletService: NSObject, WalletService { // MARK: - Tools func getBalance(address: String) async throws -> Decimal { - let account = try await apiService.getAccount(byAddress: address) + let account = try await apiService.getAccount(byAddress: address).get() return account.balance } @@ -168,12 +187,12 @@ class AdmWalletService: NSObject, WalletService { func getWalletAddress(byAdamantAddress address: String) async throws -> String { return address } -} - -extension AdmWalletService: WalletServiceWithTransfers { - func transferListViewController() -> UIViewController { - return router.get(scene: AdamantScene.Wallets.Adamant.transactionsList) - } + + func loadTransactions(offset: Int, limit: Int) async throws -> Int { .zero } + + func getLocalTransactionHistory() -> [TransactionDetails] { [] } + + func updateStatus(for id: String, status: TransactionStatus?) { } } // MARK: - NSFetchedResultsControllerDelegate @@ -197,7 +216,8 @@ extension AdmWalletService: SwinjectDependentService { accountService = container.resolve(AccountService.self) apiService = container.resolve(ApiService.self) transfersProvider = container.resolve(TransfersProvider.self) - router = container.resolve(Router.self) + coreDataStack = container.resolve(CoreDataStack.self) + vibroService = container.resolve(VibroService.self) Task { let controller = await transfersProvider.unreadTransfersController() diff --git a/Adamant/Wallets/Adamant/AdmWalletViewController.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletViewController.swift similarity index 95% rename from Adamant/Wallets/Adamant/AdmWalletViewController.swift rename to Adamant/Modules/Wallets/Adamant/AdmWalletViewController.swift index 7a9d5ea7a..5fb118b3f 100644 --- a/Adamant/Wallets/Adamant/AdmWalletViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletViewController.swift @@ -32,7 +32,7 @@ extension String.adamant.wallets { static let buyTokensUrlFormat = "" } -class AdmWalletViewController: WalletViewControllerBase { +final class AdmWalletViewController: WalletViewControllerBase { // MARK: - Rows & Sections enum Rows { case buyTokens, freeTokens @@ -60,9 +60,6 @@ class AdmWalletViewController: WalletViewControllerBase { } // MARK: - Props & Deps - - var router: Router! - var hideFreeTokensRow = false // MARK: - Lifecycle @@ -97,17 +94,15 @@ class AdmWalletViewController: WalletViewControllerBase { cell.separatorInset = .zero } }.onCellSelection { [weak self] (_, row) in - guard let vc = self?.router.get(scene: AdamantScene.Wallets.Adamant.buyAndSell) else { - fatalError("Failed to get BuyAndSell scele") - } - + guard let self = self else { return } + let vc = screensFactory.makeBuyAndSell() row.deselect() - if let split = self?.splitViewController { + if let split = splitViewController { let details = UINavigationController(rootViewController:vc) split.showDetailViewController(details, sender: self) } else { - self?.navigationController?.pushViewController(vc, animated: true ) + navigationController?.pushViewController(vc, animated: true) } } diff --git a/Adamant/Wallets/Adamant/BuyAndSellViewController.swift b/Adamant/Modules/Wallets/Adamant/BuyAndSellViewController.swift similarity index 99% rename from Adamant/Wallets/Adamant/BuyAndSellViewController.swift rename to Adamant/Modules/Wallets/Adamant/BuyAndSellViewController.swift index 00289a363..6ddae0071 100644 --- a/Adamant/Wallets/Adamant/BuyAndSellViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/BuyAndSellViewController.swift @@ -11,7 +11,7 @@ import Eureka import SafariServices import CommonKit -class BuyAndSellViewController: FormViewController { +final class BuyAndSellViewController: FormViewController { // MARK: Rows enum Rows { case adamantMessage diff --git a/Adamant/Wallets/BalanceTableViewCell.swift b/Adamant/Modules/Wallets/BalanceTableViewCell.swift similarity index 77% rename from Adamant/Wallets/BalanceTableViewCell.swift rename to Adamant/Modules/Wallets/BalanceTableViewCell.swift index d0d70b87b..4a782dbdd 100644 --- a/Adamant/Wallets/BalanceTableViewCell.swift +++ b/Adamant/Modules/Wallets/BalanceTableViewCell.swift @@ -9,6 +9,7 @@ import UIKit import Eureka import FreakingSimpleRoundImageView +import SnapKit // MARK: - Value struct public struct BalanceRowValue: Equatable { @@ -25,11 +26,18 @@ public final class BalanceTableViewCell: Cell, CellType { static let fullHeight: CGFloat = 58.0 // MARK: IBOutlets - @IBOutlet var titleLabel: UILabel! @IBOutlet var cryptoBalanceLabel: UILabel! @IBOutlet var fiatBalanceLabel: UILabel! @IBOutlet var alertLabel: RoundedLabel! + lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 17) + label.text = "Balance" + label.textColor = .adamant.textColor + return label + }() + // MARK: Properties var cryptoValue: String? { get { @@ -68,6 +76,24 @@ public final class BalanceTableViewCell: Cell, CellType { } } + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupView() + } + + required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupView() + } + + private func setupView() { + addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.leading.equalTo(layoutMarginsGuide) + make.centerY.equalToSuperview() + } + } + // MARK: Update public override func update() { super.update() diff --git a/Adamant/Wallets/BalanceTableViewCell.xib b/Adamant/Modules/Wallets/BalanceTableViewCell.xib similarity index 73% rename from Adamant/Wallets/BalanceTableViewCell.xib rename to Adamant/Modules/Wallets/BalanceTableViewCell.xib index b1eca4269..9d8abe2da 100644 --- a/Adamant/Wallets/BalanceTableViewCell.xib +++ b/Adamant/Modules/Wallets/BalanceTableViewCell.xib @@ -1,37 +1,30 @@ - - - - + + - + + - + - + - - + - - - - - + @@ -64,7 +53,6 @@ - diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcApiService.swift b/Adamant/Modules/Wallets/Bitcoin/BtcApiService.swift new file mode 100644 index 000000000..7dd9e441d --- /dev/null +++ b/Adamant/Modules/Wallets/Bitcoin/BtcApiService.swift @@ -0,0 +1,80 @@ +// +// BtcApiService.swift +// Adamant +// +// Created by Andrew G on 12.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +final class BtcApiCore: BlockchainHealthCheckableService { + let apiCore: APICoreProtocol + + init(apiCore: APICoreProtocol) { + self.apiCore = apiCore + } + + func request( + node: Node, + _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + ) async -> WalletServiceResult { + await request(apiCore, node).mapError { $0.asWalletServiceError() } + } + + func getStatusInfo(node: Node) async -> WalletServiceResult { + let startTimestamp = Date.now.timeIntervalSince1970 + + let response = await request(node: node) { core, node in + await core.sendRequest(node: node, path: BtcApiCommands.getHeight()) + } + + return response.flatMap { data in + guard + let raw = String(data: data, encoding: .utf8), + let height = Int(string: raw) + else { + return .failure(.internalError(.parsingFailed)) + } + + return .success(.init( + ping: Date.now.timeIntervalSince1970 - startTimestamp, + height: height, + wsEnabled: false, + wsPort: nil, + version: nil + )) + } + } +} + +final class BtcApiService: WalletApiService { + let api: BlockchainHealthCheckWrapper + + var preferredNodeIds: [UUID] { + api.preferredNodeIds + } + + init(api: BlockchainHealthCheckWrapper) { + self.api = api + } + + func healthCheck() { + api.healthCheck() + } + + func request( + _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + ) async -> WalletServiceResult { + await api.request { core, node in + await core.request(node: node, request) + } + } + + func getStatusInfo() async -> WalletServiceResult { + await api.request { core, node in + await core.getStatusInfo(node: node) + } + } +} diff --git a/Adamant/Wallets/Bitcoin/BtcTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Bitcoin/BtcTransactionDetailsViewController.swift similarity index 96% rename from Adamant/Wallets/Bitcoin/BtcTransactionDetailsViewController.swift rename to Adamant/Modules/Wallets/Bitcoin/BtcTransactionDetailsViewController.swift index 318a498cd..b6b62df74 100644 --- a/Adamant/Wallets/Bitcoin/BtcTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcTransactionDetailsViewController.swift @@ -9,7 +9,7 @@ import UIKit import CommonKit -class BtcTransactionDetailsViewController: TransactionDetailsViewControllerBase { +final class BtcTransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies weak var service: BtcWalletService? diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcTransactionsViewController.swift b/Adamant/Modules/Wallets/Bitcoin/BtcTransactionsViewController.swift new file mode 100644 index 000000000..41875eebb --- /dev/null +++ b/Adamant/Modules/Wallets/Bitcoin/BtcTransactionsViewController.swift @@ -0,0 +1,42 @@ +// +// BtcTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 30/01/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit +import BitcoinKit +import CommonKit + +final class BtcTransactionsViewController: TransactionsListViewControllerBase { + + // MARK: - Dependencies + var btcWalletService: BtcWalletService! + var screensFactory: ScreensFactory! + var addressBook: AddressBookService! + + // MARK: - UITableView + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let address = btcWalletService.wallet?.address, + let transaction = transactions[safe: indexPath.row] + else { return } + + let controller = screensFactory.makeDetailsVC(service: btcWalletService) + + controller.transaction = transaction + + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { + controller.senderName = String.adamant.transactionDetails.yourAddress + } + + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { + controller.recipientName = String.adamant.transactionDetails.yourAddress + } + + navigationController?.pushViewController(controller, animated: true) + } +} diff --git a/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift b/Adamant/Modules/Wallets/Bitcoin/BtcTransferViewController.swift similarity index 74% rename from Adamant/Wallets/Bitcoin/BtcTransferViewController.swift rename to Adamant/Modules/Wallets/Bitcoin/BtcTransferViewController.swift index cd800f8c4..349c38c95 100644 --- a/Adamant/Wallets/Bitcoin/BtcTransferViewController.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcTransferViewController.swift @@ -35,7 +35,11 @@ final class BtcTransferViewController: TransferViewControllerBase { comments = "" } - guard let service = service as? BtcWalletService, let recipient = recipientAddress, let amount = amount else { + guard let service = service as? BtcWalletService, + let recipient = recipientAddress, + let amount = amount, + let wallet = service.wallet + else { return } @@ -58,9 +62,26 @@ final class BtcTransferViewController: TransferViewControllerBase { Task { do { + let simpleTransaction = SimpleTransactionDetails( + txId: transaction.txID, + senderAddress: wallet.address, + recipientAddress: recipient, + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: true, + transactionStatus: nil + ) + + service.coinStorage.append(simpleTransaction) try await service.sendTransaction(transaction) } catch { dialogService.showRichError(error: error) + service.coinStorage.updateStatus( + for: transaction.txId, + status: .failed + ) } await service.update() @@ -97,25 +118,25 @@ final class BtcTransferViewController: TransferViewControllerBase { ) { vc.dialogService.showSuccess(withMessage: String.adamant.transfer.transferSuccess) - if let detailsVc = vc.router.get(scene: AdamantScene.Wallets.Bitcoin.transactionDetails) as? BtcTransactionDetailsViewController { - detailsVc.transaction = localTransaction ?? transaction - detailsVc.service = service - detailsVc.senderName = String.adamant.transactionDetails.yourAddress - - if recipientAddress == service.wallet?.address { - detailsVc.recipientName = String.adamant.transactionDetails.yourAddress - } else { - detailsVc.recipientName = self.recipientName - } - - if comments.count > 0 { - detailsVc.comment = comments - } - - vc.delegate?.transferViewController(vc, didFinishWithTransfer: transaction, detailsViewController: detailsVc) + let detailsVc = screensFactory.makeDetailsVC(service: service) + detailsVc.transaction = localTransaction ?? transaction + detailsVc.senderName = String.adamant.transactionDetails.yourAddress + + if recipientAddress == service.wallet?.address { + detailsVc.recipientName = String.adamant.transactionDetails.yourAddress } else { - vc.delegate?.transferViewController(vc, didFinishWithTransfer: transaction, detailsViewController: nil) + detailsVc.recipientName = self.recipientName + } + + if comments.count > 0 { + detailsVc.comment = comments } + + vc.delegate?.transferViewController( + vc, + didFinishWithTransfer: transaction, + detailsViewController: detailsVc + ) } // MARK: Overrides diff --git a/Adamant/Wallets/Bitcoin/BtcWallet.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWallet.swift similarity index 100% rename from Adamant/Wallets/Bitcoin/BtcWallet.swift rename to Adamant/Modules/Wallets/Bitcoin/BtcWallet.swift diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift new file mode 100644 index 000000000..45d36d3d0 --- /dev/null +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift @@ -0,0 +1,138 @@ +// +// BtcWalletFactory.swift +// Adamant +// +// Created by Andrew G on 09.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Swinject +import UIKit +import CommonKit + +struct BtcWalletFactory: WalletFactory { + typealias Service = BtcWalletService + + let assembler: Assembler + + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { + let c = BtcWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) + c.dialogService = assembler.resolve(DialogService.self) + c.currencyInfoService = assembler.resolve(CurrencyInfoService.self) + c.accountService = assembler.resolve(AccountService.self) + c.screensFactory = screensFactory + c.service = service + return c + } + + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { + let c = BtcTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) + c.dialogService = assembler.resolve(DialogService.self) + c.btcWalletService = service + c.addressBook = assembler.resolve(AddressBookService.self) + c.screensFactory = screensFactory + c.walletService = service + return c + } + + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { + let vc = BtcTransferViewController( + chatsProvider: assembler.resolve(ChatsProvider.self)!, + accountService: assembler.resolve(AccountService.self)!, + accountsProvider: assembler.resolve(AccountsProvider.self)!, + dialogService: assembler.resolve(DialogService.self)!, + screensFactory: screensFactory, + currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, + vibroService: assembler.resolve(VibroService.self)! + ) + + vc.service = service + return vc + } + + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + else { return nil } + + let comment: String? + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { + comment = raw + } else { + comment = nil + } + + return makeTransactionDetailsVC( + hash: hash, + senderId: transaction.senderId, + recipientId: transaction.recipientId, + senderAddress: "", + recipientAddress: "", + comment: comment, + transaction: nil, + richTransaction: transaction, + service: service + ) + } + + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { + makeTransactionDetailsVC(service: service) + } +} + +private extension BtcWalletFactory { + func makeTransactionDetailsVC( + hash: String, + senderId: String?, + recipientId: String?, + senderAddress: String, + recipientAddress: String, + comment: String?, + transaction: BtcTransaction?, + richTransaction: RichMessageTransaction, + service: Service + ) -> UIViewController { + let vc = makeTransactionDetailsVC(service: service) + + let amount: Decimal + if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), + let decimal = Decimal(string: amountRaw) { + amount = decimal + } else { + amount = 0 + } + + let failedTransaction = SimpleTransactionDetails( + txId: hash, + senderAddress: senderAddress, + recipientAddress: recipientAddress, + dateValue: nil, + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: richTransaction.isOutgoing, + transactionStatus: nil + ) + + vc.senderId = senderId + vc.recipientId = recipientId + vc.comment = comment + vc.transaction = transaction ?? failedTransaction + vc.richTransaction = richTransaction + return vc + } + + func makeTransactionDetailsVC(service: Service) -> BtcTransactionDetailsViewController { + let vc = BtcTransactionDetailsViewController( + dialogService: assembler.resolve(DialogService.self)!, + currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + addressBookService: assembler.resolve(AddressBookService.self)!, + accountService: assembler.resolve(AccountService.self)!, + walletService: service + ) + + vc.service = service + return vc + } +} diff --git a/Adamant/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift similarity index 85% rename from Adamant/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift rename to Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift index feebf7ad2..9425f8e2f 100644 --- a/Adamant/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift @@ -53,12 +53,16 @@ extension BtcWalletService { 10 } + var minNodeVersion: String? { + nil + } + static let explorerAddress = "https://explorer.btc.com/btc/transaction/" static var nodes: [Node] { [ - Node(url: URL(string: "https://btcnode1.adamant.im")!), -Node(url: URL(string: "https://btcnode2.adamant.im")!), + Node(url: URL(string: "https://btcnode1.adamant.im")!, altUrl: URL(string: "http://176.9.38.204:44099")), +Node(url: URL(string: "https://btcnode2.adamant.im")!, altUrl: URL(string: "http://176.9.32.126:44099")), ] } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift new file mode 100644 index 000000000..924d63d9c --- /dev/null +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift @@ -0,0 +1,64 @@ +// +// BtcWalletService+RichMessageProvider.swift +// Adamant +// +// Created by Anton Boyarkin on 20/02/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation +import MessageKit +import UIKit +import CommonKit + +extension BtcWalletService: RichMessageProvider { + var newPendingInterval: TimeInterval { + .init(milliseconds: type(of: self).newPendingInterval) + } + + var oldPendingInterval: TimeInterval { + .init(milliseconds: type(of: self).oldPendingInterval) + } + + var registeredInterval: TimeInterval { + .init(milliseconds: type(of: self).registeredInterval) + } + + var newPendingAttempts: Int { + type(of: self).newPendingAttempts + } + + var oldPendingAttempts: Int { + type(of: self).oldPendingAttempts + } + + var dynamicRichMessageType: String { + return type(of: self).richMessageType + } + + // MARK: Short description + + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { + let amount: String + + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { + return NSAttributedString(string: "⬅️ \(BtcWalletService.currencySymbol)") + } + + if let decimal = Decimal(string: raw) { + amount = AdamantBalanceFormat.full.format(decimal) + } else { + amount = raw + } + + let string: String + if transaction.isOutgoing { + string = "⬅️ \(amount) \(BtcWalletService.currencySymbol)" + } else { + string = "➡️ \(amount) \(BtcWalletService.currencySymbol)" + } + + return NSAttributedString(string: string) + } +} diff --git a/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift similarity index 59% rename from Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift rename to Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift index a23c041f6..ffa6bc719 100644 --- a/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift @@ -10,9 +10,16 @@ import Foundation import CommonKit extension BtcWalletService: RichMessageProviderWithStatusCheck { - func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { - guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) - else { + func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { + let hash: String? + + if let transaction = transaction as? RichMessageTransaction { + hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + } else { + hash = transaction.txId + } + + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent) } @@ -24,19 +31,14 @@ extension BtcWalletService: RichMessageProviderWithStatusCheck { status: getStatus(transaction: transaction, btcTransaction: btcTransaction) ) } catch { - switch error { - case ApiServiceError.networkError(_): - return .init(sentDate: nil, status: .noNetwork) - default: - return .init(sentDate: nil, status: .pending) - } + return .init(error: error) } } } private extension BtcWalletService { func getStatus( - transaction: RichMessageTransaction, + transaction: CoinTransaction, btcTransaction: BtcTransaction ) -> TransactionStatus { guard let status = btcTransaction.transactionStatus else { @@ -54,8 +56,7 @@ private extension BtcWalletService { else { return .inconsistent } // MARK: Check amount - if let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), - let reported = AdamantBalanceFormat.deserializeBalance(from: raw) { + if let reported = reportedValue(for: transaction) { guard reported == btcTransaction.amountValue else { return .inconsistent } @@ -63,4 +64,20 @@ private extension BtcWalletService { return .success } + + func reportedValue(for transaction: CoinTransaction) -> Decimal? { + guard let transaction = transaction as? RichMessageTransaction + else { + return transaction.amountValue + } + + guard + let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) + else { + return nil + } + + return reportedValue + } } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+Send.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+Send.swift new file mode 100644 index 000000000..e9b30cfe0 --- /dev/null +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+Send.swift @@ -0,0 +1,127 @@ +// +// BtcWalletService+Send.swift +// Adamant +// +// Created by Anton Boyarkin on 08/02/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit +import Alamofire +import BitcoinKit + +extension BtcWalletService: WalletServiceTwoStepSend { + typealias T = BitcoinKit.Transaction + + // MARK: Create & Send + func createTransaction(recipient: String, amount: Decimal) async throws -> BitcoinKit.Transaction { + // MARK: 1. Prepare + guard let wallet = self.btcWallet else { + throw WalletServiceError.notLogged + } + + let key = wallet.privateKey + + guard let toAddress = try? addressConverter.convert(address: recipient) else { + throw WalletServiceError.accountNotFound + } + + let rawAmount = NSDecimalNumber(decimal: amount * BtcWalletService.multiplier).uint64Value + let fee = NSDecimalNumber(decimal: self.transactionFee * BtcWalletService.multiplier).uint64Value + + // MARK: 2. Search for unspent transactions + + let utxos = try await getUnspentTransactions() + + // MARK: 3. Check if we have enought money + + let totalAmount: UInt64 = UInt64(utxos.reduce(0) { $0 + $1.output.value }) + guard totalAmount >= rawAmount + fee else { // This shit can crash BitcoinKit + throw WalletServiceError.notEnoughMoney + } + + // MARK: 4. Create local transaction + + let transaction = BitcoinKit.Transaction.createNewTransaction( + toAddress: toAddress, + amount: rawAmount, + fee: fee, + changeAddress: wallet.addressEntity, + utxos: utxos, + keys: [key] + ) + + return transaction + } + + func sendTransaction(_ transaction: BitcoinKit.Transaction) async throws { + // MARK: Prepare params + + let txHex = transaction.serialized().hex + + // MARK: Sending request + let responseData = try await btcApiService.request { core, node in + await core.sendRequest( + node: node, + path: BtcApiCommands.sendTransaction(), + method: .post, + parameters: [String.empty: txHex], + encoding: .bodyString + ) + }.get() + + let response = String(decoding: responseData, as: UTF8.self) + guard response != transaction.txId else { return } + throw WalletServiceError.remoteServiceError(message: response) + } + + func getUnspentTransactions() async throws -> [UnspentTransaction] { + guard let wallet = self.btcWallet else { + throw WalletServiceError.notLogged + } + + let address = wallet.address + let parameters = ["noCache": "1"] + + let responseData = try await btcApiService.request { core, node in + await core.sendRequest( + node: node, + path: BtcApiCommands.getUnspentTransactions(for: address), + method: .get, + parameters: parameters, + encoding: .url + ) + }.get() + + guard + let items = try? Self.jsonDecoder.decode( + [BtcUnspentTransactionResponse].self, + from: responseData + ) + else { + throw WalletServiceError.internalError(message: "BTC Wallet: not valid response", error: nil) + } + + var utxos = [UnspentTransaction]() + for item in items { + guard item.status.confirmed else { + continue + } + + let value = NSDecimalNumber(decimal: item.value).uint64Value + + let lockScript = wallet.addressEntity.lockingScript + let txHash = Data(hex: item.txId).map { Data($0.reversed()) } ?? Data() + let txIndex = item.vout + + let unspentOutput = TransactionOutput(value: value, lockingScript: lockScript) + let unspentOutpoint = TransactionOutPoint(hash: txHash, index: txIndex) + let utxo = UnspentTransaction(output: unspentOutput, outpoint: unspentOutpoint) + + utxos.append(utxo) + } + + return utxos + } + +} diff --git a/Adamant/Wallets/Bitcoin/BtcWalletService.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift similarity index 76% rename from Adamant/Wallets/Bitcoin/BtcWalletService.swift rename to Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift index 61c74dc4c..c3e78f06c 100644 --- a/Adamant/Wallets/Bitcoin/BtcWalletService.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift @@ -71,7 +71,7 @@ final class BtcWalletService: WalletService { type(of: self).currencyLogo } - var tokenNetworkSymbol: String { + static var tokenNetworkSymbol: String { "BTC" } @@ -80,7 +80,7 @@ final class BtcWalletService: WalletService { } var tokenUnicID: String { - return tokenNetworkSymbol + tokenSymbol + Self.tokenNetworkSymbol + tokenSymbol } var richMessageType: String { @@ -101,35 +101,27 @@ final class BtcWalletService: WalletService { var wallet: WalletAccount? { return btcWallet } - var walletViewController: WalletViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Bitcoin.wallet) as? BtcWalletViewController else { - fatalError("Can't get BtcWalletViewController") - } - - vc.service = self - return vc - } - // MARK: RichMessageProvider properties static let richMessageType = "btc_transaction" // MARK: - Dependencies var apiService: ApiService! + var btcApiService: BtcApiService! var accountService: AccountService! var dialogService: DialogService! - var router: Router! var increaseFeeService: IncreaseFeeService! var addressConverter: AddressConverter! + var coreDataStack: CoreDataStack! + var vibroService: VibroService! // MARK: - Constants - static var currencyLogo = UIImage.asset(named: "bitcoin_wallet") ?? .init() - + static let currencyLogo = UIImage.asset(named: "bitcoin_wallet") ?? .init() static let multiplier = Decimal(sign: .plus, exponent: 8, significand: 1) - private (set) var currentHeight: Decimal? - private var feeRate: Decimal = 1 - private (set) var transactionFee: Decimal = DefaultBtcTransferFee.medium.rawValue / multiplier - private (set) var isWarningGasPrice = false + @Atomic private(set) var currentHeight: Decimal? + @Atomic private var feeRate: Decimal = 1 + @Atomic private(set) var transactionFee: Decimal = DefaultBtcTransferFee.medium.rawValue / multiplier + @Atomic private(set) var isWarningGasPrice = false static let kvsAddress = "btc:address" private let walletPath = "m/44'/0'/21'/0/0" @@ -141,25 +133,42 @@ final class BtcWalletService: WalletService { let transactionFeeUpdated = Notification.Name("adamant.btcWallet.feeUpdated") // MARK: - Delayed KVS save - private var balanceObserver: NSObjectProtocol? + @Atomic private var balanceObserver: NSObjectProtocol? // MARK: - Properties - private (set) var btcWallet: BtcWallet? + @Atomic private(set) var btcWallet: BtcWallet? + @Atomic private(set) var enabled = true + @Atomic public var network: Network - private (set) var enabled = true + static let jsonDecoder = JSONDecoder() - public var network: Network + let defaultDispatchQueue = DispatchQueue( + label: "im.adamant.btcWalletService", + qos: .userInteractive, + attributes: [.concurrent] + ) - private var initialBalanceCheck = false + @Atomic private var subscriptions = Set() - static let jsonDecoder = JSONDecoder() + @ObservableValue private(set) var transactions: [TransactionDetails] = [] + @ObservableValue private(set) var hasMoreOldTransactions: Bool = true + + var transactionsPublisher: AnyObservable<[TransactionDetails]> { + $transactions.eraseToAnyPublisher() + } - let defaultDispatchQueue = DispatchQueue(label: "im.adamant.btcWalletService", qos: .userInteractive, attributes: [.concurrent]) + var hasMoreOldTransactionsPublisher: AnyObservable { + $hasMoreOldTransactions.eraseToAnyPublisher() + } - private var subscriptions = Set() + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( + coinId: tokenUnicID, + coreDataStack: coreDataStack, + blockchainType: richMessageType + ) // MARK: - State - private (set) var state: WalletServiceState = .notInitiated + @Atomic private(set) var state: WalletServiceState = .notInitiated private func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { @@ -205,11 +214,21 @@ final class BtcWalletService: WalletService { .receive(on: OperationQueue.main) .sink { [weak self] _ in self?.btcWallet = nil - self?.initialBalanceCheck = false if let balanceObserver = self?.balanceObserver { NotificationCenter.default.removeObserver(balanceObserver) self?.balanceObserver = nil } + self?.coinStorage.clear() + self?.hasMoreOldTransactions = true + self?.transactions = [] + } + .store(in: &subscriptions) + } + + func addTransactionObserver() { + coinStorage.transactionsPublisher + .sink { [weak self] transactions in + self?.transactions = transactions } .store(in: &subscriptions) } @@ -236,20 +255,25 @@ final class BtcWalletService: WalletService { setState(.updating) if let balance = try? await getBalance() { - wallet.isBalanceInitialized = true let notification: Notification.Name? + let isRaised = (wallet.balance < balance) && wallet.isBalanceInitialized + if wallet.balance != balance { wallet.balance = balance notification = walletUpdatedNotification - initialBalanceCheck = false - } else if initialBalanceCheck { - initialBalanceCheck = false + } else if !wallet.isBalanceInitialized { notification = walletUpdatedNotification } else { notification = nil } + wallet.isBalanceInitialized = true + + if isRaised { + vibroService.applyVibration(.success) + } + if let notification = notification { NotificationCenter.default.post( name: notification, @@ -331,7 +355,7 @@ final class BtcWalletService: WalletService { func getWalletAddress(byAdamantAddress address: String) async throws -> String { do { - let result = try await apiService.get(key: BtcWalletService.kvsAddress, sender: address) + let result = try await apiService.get(key: BtcWalletService.kvsAddress, sender: address).get() guard let result = result else { throw WalletServiceError.walletNotInitiated @@ -388,6 +412,12 @@ extension BtcWalletService: InitiatedWithPassphraseService { let eWallet = try BtcWallet(privateKey: privateKey, addressConverter: addressConverter) self.btcWallet = eWallet + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: eWallet] + ) + if !self.enabled { self.enabled = true NotificationCenter.default.post(name: self.serviceEnabledChanged, object: self) @@ -404,7 +434,6 @@ extension BtcWalletService: InitiatedWithPassphraseService { throw WalletServiceError.accountNotFound } - service.initialBalanceCheck = true service.setState(.upToDate, silent: true) Task { service.update() @@ -445,9 +474,13 @@ extension BtcWalletService: SwinjectDependentService { accountService = container.resolve(AccountService.self) apiService = container.resolve(ApiService.self) dialogService = container.resolve(DialogService.self) - router = container.resolve(Router.self) increaseFeeService = container.resolve(IncreaseFeeService.self) addressConverter = container.resolve(AddressConverterFactory.self)?.make(network: network) + btcApiService = container.resolve(BtcApiService.self) + vibroService = container.resolve(VibroService.self) + coreDataStack = container.resolve(CoreDataStack.self) + + addTransactionObserver() } } @@ -462,74 +495,23 @@ extension BtcWalletService { } func getBalance(address: String) async throws -> Decimal { - guard let url = BtcWalletService.nodes.randomElement()?.asURL() else { - let message = "Failed to get BTC endpoint URL" - assertionFailure(message) - throw WalletServiceError.internalError(message: message, error: nil) - } - - // Request url - let endpoint = url.appendingPathComponent(BtcApiCommands.balance(for: address)) - - // MARK: Sending request - - let response: BtcBalanceResponse = try await apiService.sendRequest( - url: endpoint, - method: .get, - parameters: nil - ) - - let balance = response.value / BtcWalletService.multiplier - - return balance + let response: BtcBalanceResponse = try await btcApiService.request { api, node in + await api.sendRequestJsonResponse(node: node, path: BtcApiCommands.balance(for: address)) + }.get() + + return response.value / BtcWalletService.multiplier } func getFeeRate() async throws -> Decimal { - guard let url = BtcWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get BTC endpoint URL") - } - - // Request url - let endpoint = url.appendingPathComponent(BtcApiCommands.getFeeRate()) + let response: [String: Decimal] = try await btcApiService.request { api, node in + await api.sendRequestJsonResponse(node: node, path: BtcApiCommands.getFeeRate()) + }.get() - // MARK: Sending request - - let response: [String: Decimal] = try await apiService.sendRequest( - url: endpoint, - method: .get, - parameters: nil - ) - - let value = response["2"] ?? 1 - - return value + return response["2"] ?? 1 } func getCurrentHeight() async throws -> Decimal { - guard let url = BtcWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get BTC endpoint URL") - } - - // Request url - let endpoint = url.appendingPathComponent(BtcApiCommands.getHeight()) - - // MARK: Sending request - let data = try await apiService.sendRequest( - url: endpoint, - method: .get, - parameters: nil - ) - - guard - let raw = String(data: data, encoding: .utf8), - let value = Decimal(string: raw) - else { - throw WalletServiceError.remoteServiceError( - message: "BTC Wallet: not a valid response" - ) - } - - return value + try await .init(btcApiService.getStatusInfo().get().height) } } @@ -552,14 +534,20 @@ extension BtcWalletService { } Task { - await apiService.store(key: BtcWalletService.kvsAddress, value: btcAddress, type: .keyValue, sender: adamant.address, keypair: keypair) { result in - switch result { - case .success: - completion(.success) - - case .failure(let error): - completion(.failure(error: .apiError(error))) - } + let result = await apiService.store( + key: BtcWalletService.kvsAddress, + value: btcAddress, + type: .keyValue, + sender: adamant.address, + keypair: keypair + ) + + switch result { + case .success: + completion(.success) + + case .failure(let error): + completion(.failure(error: .apiError(error))) } } } @@ -650,26 +638,19 @@ extension BtcWalletService { return transactions } - private func getTransactions(for address: String, fromTx: String? = nil) async throws -> [RawBtcTransactionResponse] { - guard let url = BtcWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get BTC endpoint URL") - } - - // Request url - let endpoint = url.appendingPathComponent(BtcApiCommands.getTransactions( - for: address, - fromTx: fromTx - )) - - // MARK: Sending request - - let transactions: [RawBtcTransactionResponse] = try await apiService.sendRequest( - url: endpoint, - method: .get, - parameters: nil - ) - - return transactions + private func getTransactions( + for address: String, + fromTx: String? = nil + ) async throws -> [RawBtcTransactionResponse] { + return try await btcApiService.request { api, node in + await api.sendRequestJsonResponse( + node: node, + path: BtcApiCommands.getTransactions( + for: address, + fromTx: fromTx + ) + ) + }.get() } func getTransaction(by hash: String) async throws -> BtcTransaction { @@ -677,44 +658,43 @@ extension BtcWalletService { throw WalletServiceError.notLogged } - guard let url = BtcWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get BTC endpoint URL") - } - - // Request url - let endpoint = url.appendingPathComponent(BtcApiCommands.getTransaction(by: hash)) - - // MARK: Sending request - - do { - let rawTransaction: RawBtcTransactionResponse = try await apiService.sendRequest( - url: endpoint, - method: .get, - parameters: nil + let rawTransaction: RawBtcTransactionResponse = try await btcApiService.request { api, node in + await api.sendRequestJsonResponse( + node: node, + path: BtcApiCommands.getTransaction(by: hash) ) - - let transaction = rawTransaction.asBtcTransaction( - BtcTransaction.self, - for: address, - height: self.currentHeight - ) - - return transaction - } catch let error as ApiServiceError { - throw WalletServiceError.remoteServiceError(message: error.message) - } + }.get() + + return rawTransaction.asBtcTransaction( + BtcTransaction.self, + for: address, + height: self.currentHeight + ) } -} - -extension BtcWalletService: WalletServiceWithTransfers { - func transferListViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Bitcoin.transactionsList) as? BtcTransactionsViewController else { - fatalError("Can't get BtcTransactionsViewController") + func loadTransactions(offset: Int, limit: Int) async throws -> Int { + let txId = offset == .zero + ? transactions.first?.txId + : transactions.last?.txId + + let trs = try await getTransactions(fromTx: txId) + + guard trs.count > 0 else { + hasMoreOldTransactions = false + return .zero } - vc.btcWalletService = self - return vc + coinStorage.append(trs) + + return trs.count + } + + func getLocalTransactionHistory() -> [TransactionDetails] { + transactions + } + + func updateStatus(for id: String, status: TransactionStatus?) { + coinStorage.updateStatus(for: id, status: status) } } @@ -743,6 +723,6 @@ extension BtcWalletService: PrivateKeyGenerator { } } -class BtcTransaction: BaseBtcTransaction { +final class BtcTransaction: BaseBtcTransaction { override var defaultCurrencySymbol: String? { BtcWalletService.currencySymbol } } diff --git a/Adamant/Wallets/Bitcoin/BtcWalletViewController.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletViewController.swift similarity index 93% rename from Adamant/Wallets/Bitcoin/BtcWalletViewController.swift rename to Adamant/Modules/Wallets/Bitcoin/BtcWalletViewController.swift index fc17b0d77..d8ec655e1 100644 --- a/Adamant/Wallets/Bitcoin/BtcWalletViewController.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletViewController.swift @@ -15,7 +15,7 @@ extension String.adamant { static let sendBtc = String.localized("AccountTab.Row.SendBtc", comment: "Account tab: 'Send BTC tokens' button") } -class BtcWalletViewController: WalletViewControllerBase { +final class BtcWalletViewController: WalletViewControllerBase { // MARK: Lifecycle override func viewDidLoad() { diff --git a/Adamant/Wallets/Bitcoin/Models/BtcBalanceResponse.swift b/Adamant/Modules/Wallets/Bitcoin/DTO/BtcBalanceResponse.swift similarity index 100% rename from Adamant/Wallets/Bitcoin/Models/BtcBalanceResponse.swift rename to Adamant/Modules/Wallets/Bitcoin/DTO/BtcBalanceResponse.swift diff --git a/Adamant/Wallets/Bitcoin/Models/BtcTransactionResponse.swift b/Adamant/Modules/Wallets/Bitcoin/DTO/BtcTransactionResponse.swift similarity index 100% rename from Adamant/Wallets/Bitcoin/Models/BtcTransactionResponse.swift rename to Adamant/Modules/Wallets/Bitcoin/DTO/BtcTransactionResponse.swift diff --git a/Adamant/Wallets/Bitcoin/Models/BtcUnspentTransactionResponse.swift b/Adamant/Modules/Wallets/Bitcoin/DTO/BtcUnspentTransactionResponse.swift similarity index 100% rename from Adamant/Wallets/Bitcoin/Models/BtcUnspentTransactionResponse.swift rename to Adamant/Modules/Wallets/Bitcoin/DTO/BtcUnspentTransactionResponse.swift diff --git a/Adamant/Modules/Wallets/DI/AdamantWalletFactoryCompose.swift b/Adamant/Modules/Wallets/DI/AdamantWalletFactoryCompose.swift new file mode 100644 index 000000000..f24a943b5 --- /dev/null +++ b/Adamant/Modules/Wallets/DI/AdamantWalletFactoryCompose.swift @@ -0,0 +1,161 @@ +// +// AdamantWalletFactoryCompose.swift +// Adamant +// +// Created by Andrew G on 11.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit + +struct AdamantWalletFactoryCompose: WalletFactoryCompose { + private let factories: [any WalletFactory] + + init( + lskWalletFactory: LskWalletFactory, + dogeWalletFactory: DogeWalletFactory, + dashWalletFactory: DashWalletFactory, + btcWalletFactory: BtcWalletFactory, + ethWalletFactory: EthWalletFactory, + erc20WalletFactory: ERC20WalletFactory, + admWalletFactory: AdmWalletFactory + ) { + factories = [ + lskWalletFactory, + dogeWalletFactory, + dashWalletFactory, + btcWalletFactory, + ethWalletFactory, + erc20WalletFactory, + admWalletFactory + ] + } + + func makeWalletVC(service: WalletService, screensFactory: ScreensFactory) -> WalletViewController { + for factory in factories { + guard let result = tryMakeWalletVC( + factory: factory, + service: service, + screensFactory: screensFactory + ) else { continue } + + return result + } + + fatalError("No suitable factory") + } + + func makeTransferListVC(service: WalletService, screenFactory: ScreensFactory) -> UIViewController { + for factory in factories { + guard let result = tryMakeTransferListVC( + factory: factory, + service: service, + screenFactory: screenFactory + ) else { continue } + + return result + } + + fatalError("No suitable factory") + } + + func makeTransferVC(service: WalletService, screenFactory: ScreensFactory) -> TransferViewControllerBase { + for factory in factories { + guard let result = tryMakeTransferVC( + factory: factory, + service: service, + screenFactory: screenFactory + ) else { continue } + + return result + } + + fatalError("No suitable factory") + } + + func makeDetailsVC(service: WalletService) -> TransactionDetailsViewControllerBase { + for factory in factories { + guard let result = tryMakeDetailsVC( + factory: factory, + service: service + ) else { continue } + + return result + } + + fatalError("No suitable factory") + } + + func makeDetailsVC(service: WalletService, transaction: RichMessageTransaction) -> UIViewController? { + for factory in factories { + guard let result = tryMakeDetailsVC( + factory: factory, + service: service, + transaction: transaction + ) else { continue } + + return result + } + + fatalError("No suitable factory") + } +} + +private extension AdamantWalletFactoryCompose { + func tryMakeWalletVC( + factory: Factory, + service: WalletService, + screensFactory: ScreensFactory + ) -> WalletViewController? { + tryExecuteFactoryMethod(factory: factory, service: service) { + factory.makeWalletVC(service: $0, screensFactory: screensFactory) + } + } + + func tryMakeTransferListVC( + factory: Factory, + service: WalletService, + screenFactory: ScreensFactory + ) -> UIViewController? { + tryExecuteFactoryMethod(factory: factory, service: service) { + factory.makeTransferListVC(service: $0, screensFactory: screenFactory) + } + } + + func tryMakeTransferVC( + factory: Factory, + service: WalletService, + screenFactory: ScreensFactory + ) -> TransferViewControllerBase? { + tryExecuteFactoryMethod(factory: factory, service: service) { + factory.makeTransferVC(service: $0, screensFactory: screenFactory) + } + } + + func tryMakeDetailsVC( + factory: Factory, + service: WalletService, + transaction: RichMessageTransaction + ) -> UIViewController?? { + tryExecuteFactoryMethod(factory: factory, service: service) { + factory.makeDetailsVC(service: $0, transaction: transaction) + } + } + + func tryMakeDetailsVC( + factory: Factory, + service: WalletService + ) -> TransactionDetailsViewControllerBase? { + tryExecuteFactoryMethod(factory: factory, service: service) { + factory.makeDetailsVC(service: $0) + } + } + + func tryExecuteFactoryMethod( + factory _: Factory, + service: WalletService, + method: (Factory.Service) -> Result + ) -> Result? { + (service as? Factory.Service).map { method($0) } + } +} diff --git a/Adamant/Modules/Wallets/DI/WalletFactory.swift b/Adamant/Modules/Wallets/DI/WalletFactory.swift new file mode 100644 index 000000000..648a29b05 --- /dev/null +++ b/Adamant/Modules/Wallets/DI/WalletFactory.swift @@ -0,0 +1,20 @@ +// +// WalletFactory.swift +// Adamant +// +// Created by Andrew G on 11.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit + +@MainActor +protocol WalletFactory { + associatedtype Service = WalletService + + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? +} diff --git a/Adamant/Modules/Wallets/DI/WalletFactoryCompose.swift b/Adamant/Modules/Wallets/DI/WalletFactoryCompose.swift new file mode 100644 index 000000000..b22b1b40b --- /dev/null +++ b/Adamant/Modules/Wallets/DI/WalletFactoryCompose.swift @@ -0,0 +1,22 @@ +// +// WalletFactoryCompose.swift +// Adamant +// +// Created by Andrew G on 10.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import UIKit + +@MainActor +protocol WalletFactoryCompose { + func makeWalletVC(service: WalletService, screensFactory: ScreensFactory) -> WalletViewController + func makeTransferListVC(service: WalletService, screenFactory: ScreensFactory) -> UIViewController + func makeTransferVC(service: WalletService, screenFactory: ScreensFactory) -> TransferViewControllerBase + func makeDetailsVC(service: WalletService) -> TransactionDetailsViewControllerBase + + func makeDetailsVC( + service: WalletService, + transaction: RichMessageTransaction + ) -> UIViewController? +} diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashBlockchainInfoDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashBlockchainInfoDTO.swift new file mode 100644 index 000000000..3811ec414 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DTO/DashBlockchainInfoDTO.swift @@ -0,0 +1,14 @@ +// +// DashBlockchainInfoDTO.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct DashBlockchainInfoDTO: Codable { + let chain: String + let blocks: Int +} diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashErrorDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashErrorDTO.swift new file mode 100644 index 000000000..b2ed0e85c --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DTO/DashErrorDTO.swift @@ -0,0 +1,18 @@ +// +// DashErrorDTO.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct DashErrorDTO: Codable, LocalizedError { + let code: Int + let message: String + + var errorDescription: String? { + message + } +} diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressBalanceDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressBalanceDTO.swift new file mode 100644 index 000000000..4be8440c5 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressBalanceDTO.swift @@ -0,0 +1,19 @@ +// +// DashGetAddressBalanceDTO.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct DashGetAddressBalanceDTO: Codable { + let method: String + let params: [String] + + init(address: String) { + self.method = "getaddressbalance" + self.params = [address] + } +} diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressTransactionIds.swift b/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressTransactionIds.swift new file mode 100644 index 000000000..304c48837 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressTransactionIds.swift @@ -0,0 +1,19 @@ +// +// DashGetAddressTransactionIds.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct DashGetAddressTransactionIds: Codable { + let method: String + let params: [String] + + init(address: String) { + self.method = "getaddresstxids" + self.params = [address] + } +} diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashGetBlockDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashGetBlockDTO.swift new file mode 100644 index 000000000..a5855c58a --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DTO/DashGetBlockDTO.swift @@ -0,0 +1,19 @@ +// +// DashGetBlockDTO.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct DashGetBlockDTO: Codable { + let method: String + let params: [String] + + init(hash: String) { + self.method = "getblock" + self.params = [hash] + } +} diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashGetRawTransactionDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashGetRawTransactionDTO.swift new file mode 100644 index 000000000..dd40573b8 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DTO/DashGetRawTransactionDTO.swift @@ -0,0 +1,39 @@ +// +// DashGetRawTransactionDTO.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct DashGetRawTransactionDTO: Encodable { + let method: String + let params: [Parameter] + + init(hash: String) { + self.method = "getrawtransaction" + self.params = [.string(hash), .bool(true)] + } +} + +extension DashGetRawTransactionDTO { + enum Parameter { + case string(String) + case bool(Bool) + } +} + +extension DashGetRawTransactionDTO.Parameter: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch self { + case let .string(value): + try container.encode(value) + case let .bool(value): + try container.encode(value) + } + } +} diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashGetUnspentTransactionsDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashGetUnspentTransactionsDTO.swift new file mode 100644 index 000000000..e00e09cf1 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DTO/DashGetUnspentTransactionsDTO.swift @@ -0,0 +1,19 @@ +// +// DashGetUnspentTransactionsDTO.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct DashGetUnspentTransactionDTO: Codable { + let method: String + let params: [String] + + init(address: String) { + self.method = "getaddressutxos" + self.params = [address] + } +} diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashResponseDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashResponseDTO.swift new file mode 100644 index 000000000..4d04e5da1 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DTO/DashResponseDTO.swift @@ -0,0 +1,14 @@ +// +// DashResponseDTO.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct DashResponseDTO: Codable { + let result: T? + let error: DashErrorDTO? +} diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashSendRawTransactionDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashSendRawTransactionDTO.swift new file mode 100644 index 000000000..cc80e2970 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DTO/DashSendRawTransactionDTO.swift @@ -0,0 +1,19 @@ +// +// DashSendRawTransactionDTO.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct DashSendRawTransactionDTO: Codable { + let method: String + let params: [String] + + init(txHex: String) { + self.method = "sendrawtransaction" + self.params = [txHex] + } +} diff --git a/Adamant/Modules/Wallets/Dash/DashApiService.swift b/Adamant/Modules/Wallets/Dash/DashApiService.swift new file mode 100644 index 000000000..c5de87166 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DashApiService.swift @@ -0,0 +1,87 @@ +// +// DashApiService.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +final class DashApiCore: BlockchainHealthCheckableService { + let apiCore: APICoreProtocol + + init(apiCore: APICoreProtocol) { + self.apiCore = apiCore + } + + func request( + node: Node, + _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + ) async -> WalletServiceResult { + await request(apiCore, node).mapError { $0.asWalletServiceError() } + } + + func getStatusInfo(node: Node) async -> WalletServiceResult { + let startTimestamp = Date.now.timeIntervalSince1970 + + let response: WalletServiceResult = await request(node: node) { core, node in + let response: ApiServiceResult> = await core.sendRequestJsonResponse( + node: node, + path: .empty, + method: .post, + parameters: ["method": "getblockchaininfo"], + encoding: .json + ) + + return response.flatMap { dto in + if let result = dto.result, dto.error == nil { + return .success(result) + } else { + return .failure(.serverError(error: dto.error?.localizedDescription ?? .empty)) + } + } + } + + return response.map { data in + return .init( + ping: Date.now.timeIntervalSince1970 - startTimestamp, + height: data.blocks, + wsEnabled: false, + wsPort: nil, + version: nil + ) + } + } +} + +final class DashApiService: WalletApiService { + let api: BlockchainHealthCheckWrapper + + var preferredNodeIds: [UUID] { + api.preferredNodeIds + } + + init(api: BlockchainHealthCheckWrapper) { + self.api = api + } + + func healthCheck() { + api.healthCheck() + } + + func request( + _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + ) async -> WalletServiceResult { + await api.request { core, node in + await core.request(node: node, request) + } + } + + func getStatusInfo() async -> WalletServiceResult { + await api.request { core, node in + await core.getStatusInfo(node: node) + } + } +} diff --git a/Adamant/Wallets/Dash/DashMainnet.swift b/Adamant/Modules/Wallets/Dash/DashMainnet.swift similarity index 96% rename from Adamant/Wallets/Dash/DashMainnet.swift rename to Adamant/Modules/Wallets/Dash/DashMainnet.swift index e05a1dc0f..a95f117ea 100644 --- a/Adamant/Wallets/Dash/DashMainnet.swift +++ b/Adamant/Modules/Wallets/Dash/DashMainnet.swift @@ -9,7 +9,7 @@ import Foundation import BitcoinKit -class DashMainnet: Network { +final class DashMainnet: Network { override var name: String { return "livenet" } diff --git a/Adamant/Wallets/Dash/DashTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Dash/DashTransactionDetailsViewController.swift similarity index 97% rename from Adamant/Wallets/Dash/DashTransactionDetailsViewController.swift rename to Adamant/Modules/Wallets/Dash/DashTransactionDetailsViewController.swift index 65681cfaa..46edaf5ed 100644 --- a/Adamant/Wallets/Dash/DashTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Dash/DashTransactionDetailsViewController.swift @@ -10,7 +10,7 @@ import UIKit import Eureka import CommonKit -class DashTransactionDetailsViewController: TransactionDetailsViewControllerBase { +final class DashTransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies weak var service: DashWalletService? diff --git a/Adamant/Modules/Wallets/Dash/DashTransactionsViewController.swift b/Adamant/Modules/Wallets/Dash/DashTransactionsViewController.swift new file mode 100644 index 000000000..dcc0825ce --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DashTransactionsViewController.swift @@ -0,0 +1,66 @@ +// +// DashTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 19/05/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit +import ProcedureKit + +final class DashTransactionsViewController: TransactionsListViewControllerBase { + + // MARK: - Dependencies + var screensFactory: ScreensFactory! + var dashWalletService: DashWalletService! + + // MARK: - UITableView + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let address = walletService.wallet?.address, + let transaction = transactions[safe: indexPath.row] + else { return } + + let controller = screensFactory.makeDetailsVC(service: walletService) + controller.transaction = transaction + + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { + controller.senderName = String.adamant.transactionDetails.yourAddress + } + + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { + controller.recipientName = String.adamant.transactionDetails.yourAddress + } + + navigationController?.pushViewController(controller, animated: true) + } +} + +private class LoadMoreDashTransactionsProcedure: Procedure { + let service: DashWalletService + + private(set) var result: DashTransactionsPointer? + + init(service: DashWalletService) { + self.service = service + + super.init() + + log.severity = .warning + } + + override func execute() { + service.getNextTransaction { result in + switch result { + case .success(let result): + self.result = result + self.finish() + + case .failure(let error): + self.result = nil + self.finish(with: error) + } + } + } +} diff --git a/Adamant/Wallets/Dash/DashTransferViewController.swift b/Adamant/Modules/Wallets/Dash/DashTransferViewController.swift similarity index 84% rename from Adamant/Wallets/Dash/DashTransferViewController.swift rename to Adamant/Modules/Wallets/Dash/DashTransferViewController.swift index a7c954185..dbfd4a865 100644 --- a/Adamant/Wallets/Dash/DashTransferViewController.swift +++ b/Adamant/Modules/Wallets/Dash/DashTransferViewController.swift @@ -40,7 +40,7 @@ final class DashTransferViewController: TransferViewControllerBase { return } - guard service.wallet != nil else { + guard let wallet = service.wallet else { return } @@ -64,9 +64,26 @@ final class DashTransferViewController: TransferViewControllerBase { Task { do { + let simpleTransaction = SimpleTransactionDetails( + txId: transaction.txID, + senderAddress: wallet.address, + recipientAddress: recipient, + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: true, + transactionStatus: nil + ) + + service.coinStorage.append(simpleTransaction) try await service.sendTransaction(transaction) } catch { dialogService.showRichError(error: error) + service.coinStorage.updateStatus( + for: transaction.txId, + status: .failed + ) } await service.update() @@ -93,13 +110,8 @@ final class DashTransferViewController: TransferViewControllerBase { comments: String, service: DashWalletService ) { - guard let detailsVc = router.get(scene: AdamantScene.Wallets.Dash.transactionDetails) as? DashTransactionDetailsViewController else { - delegate?.transferViewController(self, didFinishWithTransfer: transaction, detailsViewController: nil) - return - } - + let detailsVc = screensFactory.makeDetailsVC(service: service) detailsVc.transaction = transaction - detailsVc.service = service detailsVc.senderName = String.adamant.transactionDetails.yourAddress detailsVc.recipientName = recipientName diff --git a/Adamant/Wallets/Dash/DashWallet.swift b/Adamant/Modules/Wallets/Dash/DashWallet.swift similarity index 95% rename from Adamant/Wallets/Dash/DashWallet.swift rename to Adamant/Modules/Wallets/Dash/DashWallet.swift index df50db49a..77876b482 100644 --- a/Adamant/Wallets/Dash/DashWallet.swift +++ b/Adamant/Modules/Wallets/Dash/DashWallet.swift @@ -9,7 +9,7 @@ import Foundation import BitcoinKit -class DashWallet: WalletAccount { +final class DashWallet: WalletAccount { let addressEntity: Address let privateKey: PrivateKey let publicKey: PublicKey diff --git a/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift b/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift new file mode 100644 index 000000000..ffa3e29af --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift @@ -0,0 +1,147 @@ +// +// DashWalletFactory.swift +// Adamant +// +// Created by Anton Boyarkin on 25/04/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Swinject +import CommonKit +import UIKit + +struct DashWalletFactory: WalletFactory { + typealias Service = DashWalletService + + let assembler: Assembler + + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { + let c = DashWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) + c.dialogService = assembler.resolve(DialogService.self) + c.currencyInfoService = assembler.resolve(CurrencyInfoService.self) + c.accountService = assembler.resolve(AccountService.self) + c.service = service + c.screensFactory = screensFactory + return c + } + + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { + let c = DashTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) + c.dialogService = assembler.resolve(DialogService.self) + c.screensFactory = screensFactory + c.walletService = service + c.dashWalletService = service + return c + } + + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { + let vc = DashTransferViewController( + chatsProvider: assembler.resolve(ChatsProvider.self)!, + accountService: assembler.resolve(AccountService.self)!, + accountsProvider: assembler.resolve(AccountsProvider.self)!, + dialogService: assembler.resolve(DialogService.self)!, + screensFactory: screensFactory, + currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, + vibroService: assembler.resolve(VibroService.self)! + ) + + vc.service = service + return vc + } + + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { + guard + let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash), + let address = assembler.resolve(AccountService.self)?.account?.address + else { return nil } + + let comment: String? + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { + comment = raw + } else { + comment = nil + } + + return makeTransactionDetailsVC( + hash: hash, + senderId: transaction.senderId, + recipientId: transaction.recipientId, + senderAddress: "", + recipientAddress: "", + comment: comment, + address: address, + blockId: nil, + transaction: nil, + richTransaction: transaction, + service: service + ) + } + + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { + makeTransactionDetailsVC(service: service) + } +} + +private extension DashWalletFactory { + func makeTransactionDetailsVC( + hash: String, + senderId: String?, + recipientId: String?, + senderAddress: String, + recipientAddress: String, + comment: String?, + address: String, + blockId: String?, + transaction: BTCRawTransaction?, + richTransaction: RichMessageTransaction, + service: Service + ) -> UIViewController { + let vc = makeTransactionDetailsVC(service: service) + + let amount: Decimal + if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), + let decimal = Decimal(string: amountRaw) { + amount = decimal + } else { + amount = 0 + } + + var dashTransaction = transaction?.asBtcTransaction(DashTransaction.self, for: address) + if let blockId = blockId { + dashTransaction = transaction?.asBtcTransaction(DashTransaction.self, for: address, blockId: blockId) + } + let failedTransaction = SimpleTransactionDetails( + txId: hash, + senderAddress: senderAddress, + recipientAddress: recipientAddress, + dateValue: nil, + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: richTransaction.isOutgoing, + transactionStatus: nil + ) + + vc.senderId = senderId + vc.recipientId = recipientId + vc.comment = comment + vc.transaction = dashTransaction ?? failedTransaction + vc.richTransaction = richTransaction + return vc + } + + func makeTransactionDetailsVC(service: Service) -> DashTransactionDetailsViewController { + let vc = DashTransactionDetailsViewController( + dialogService: assembler.resolve(DialogService.self)!, + currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + addressBookService: assembler.resolve(AddressBookService.self)!, + accountService: assembler.resolve(AccountService.self)!, + walletService: service + ) + + vc.service = service + return vc + } +} diff --git a/Adamant/Wallets/Dash/DashWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+DynamicConstants.swift similarity index 95% rename from Adamant/Wallets/Dash/DashWalletService+DynamicConstants.swift rename to Adamant/Modules/Wallets/Dash/DashWalletService+DynamicConstants.swift index 5ede80f03..964917edc 100644 --- a/Adamant/Wallets/Dash/DashWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+DynamicConstants.swift @@ -53,6 +53,10 @@ extension DashWalletService { 80 } + var minNodeVersion: String? { + nil + } + static let explorerAddress = "https://dashblockexplorer.com/tx/" static var nodes: [Node] { diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProvider.swift new file mode 100644 index 000000000..4a114b464 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProvider.swift @@ -0,0 +1,64 @@ +// +// DashWalletService+RichMessageProvider.swift +// Adamant +// +// Created by Anton Boyarkin on 26/05/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation +import MessageKit +import UIKit +import CommonKit + +extension DashWalletService: RichMessageProvider { + var newPendingInterval: TimeInterval { + .init(milliseconds: type(of: self).newPendingInterval) + } + + var oldPendingInterval: TimeInterval { + .init(milliseconds: type(of: self).oldPendingInterval) + } + + var registeredInterval: TimeInterval { + .init(milliseconds: type(of: self).registeredInterval) + } + + var newPendingAttempts: Int { + type(of: self).newPendingAttempts + } + + var oldPendingAttempts: Int { + type(of: self).oldPendingAttempts + } + + var dynamicRichMessageType: String { + return type(of: self).richMessageType + } + + // MARK: Short description + + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { + let amount: String + + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { + return NSAttributedString(string: "⬅️ \(DashWalletService.currencySymbol)") + } + + if let decimal = Decimal(string: raw) { + amount = AdamantBalanceFormat.full.format(decimal) + } else { + amount = raw + } + + let string: String + if transaction.isOutgoing { + string = "⬅️ \(amount) \(DashWalletService.currencySymbol)" + } else { + string = "➡️ \(amount) \(DashWalletService.currencySymbol)" + } + + return NSAttributedString(string: string) + } +} diff --git a/Adamant/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift similarity index 70% rename from Adamant/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift rename to Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift index 2d3fb301c..49a371d19 100644 --- a/Adamant/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift @@ -10,9 +10,16 @@ import Foundation import CommonKit extension DashWalletService: RichMessageProviderWithStatusCheck { - func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { - guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) - else { + func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { + let hash: String? + + if let transaction = transaction as? RichMessageTransaction { + hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + } else { + hash = transaction.txId + } + + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent) } @@ -21,12 +28,7 @@ extension DashWalletService: RichMessageProviderWithStatusCheck { do { dashTransaction = try await getTransaction(by: hash) } catch { - switch error { - case ApiServiceError.networkError(_): - return .init(sentDate: nil, status: .noNetwork) - default: - return .init(sentDate: nil, status: .pending) - } + return .init(error: error) } return .init( @@ -39,7 +41,7 @@ extension DashWalletService: RichMessageProviderWithStatusCheck { private extension DashWalletService { func getStatus( dashTransaction: BTCRawTransaction, - transaction: RichMessageTransaction + transaction: CoinTransaction ) -> TransactionStatus { // MARK: Check confirmations @@ -48,8 +50,7 @@ private extension DashWalletService { } // MARK: Check amount & address - guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), - let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) else { + guard let reportedValue = reportedValue(for: transaction) else { return .inconsistent } @@ -91,4 +92,20 @@ private extension DashWalletService { return result } + + func reportedValue(for transaction: CoinTransaction) -> Decimal? { + guard let transaction = transaction as? RichMessageTransaction + else { + return transaction.amountValue + } + + guard + let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) + else { + return nil + } + + return reportedValue + } } diff --git a/Adamant/Wallets/Dash/DashWalletService+Send.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+Send.swift similarity index 59% rename from Adamant/Wallets/Dash/DashWalletService+Send.swift rename to Adamant/Modules/Wallets/Dash/DashWalletService+Send.swift index 74f67b726..2551c52d5 100644 --- a/Adamant/Wallets/Dash/DashWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+Send.swift @@ -13,15 +13,6 @@ import Alamofire extension DashWalletService: WalletServiceTwoStepSend { typealias T = BitcoinKit.Transaction - func transferViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Dash.transfer) as? DashTransferViewController else { - fatalError("Can't get DashTransferViewController") - } - - vc.service = self - return vc - } - // MARK: Create & Send func create(recipient: String, amount: Decimal) async throws -> BitcoinKit.Transaction { guard let lastTransaction = self.lastTransactionId else { @@ -33,7 +24,7 @@ extension DashWalletService: WalletServiceTwoStepSend { guard let confirmations = transaction.confirmations, confirmations >= 1 else { - throw WalletServiceError.internalError(message: "WAIT_FOR_COMPLETION", error: nil) + throw WalletServiceError.remoteServiceError(message: "WAIT_FOR_COMPLETION", error: nil) } return try await createTransaction(recipient: recipient, amount: amount) @@ -77,48 +68,31 @@ extension DashWalletService: WalletServiceTwoStepSend { } func sendTransaction(_ transaction: BitcoinKit.Transaction) async throws { - guard let endpoint = DashWalletService.nodes.randomElement()?.asURL() else { - throw WalletServiceError.internalError( - message: "Failed to get DASH endpoint URL", - error: nil - ) - } - let txHex = transaction.serialized().hex - let parameters: Parameters = [ - "method": "sendrawtransaction", - "params": [ - txHex - ] - ] - - // MARK: Sending request - - do { - let response: BTCRPCServerResponce = try await apiService.sendRequest( - url: endpoint, + let response: BTCRPCServerResponce = try await dashApiService.request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: .empty, method: .post, - parameters: parameters, - encoding: JSONEncoding.default + parameters: DashSendRawTransactionDTO(txHex: txHex), + encoding: .json ) - - if response.result != nil { - lastTransactionId = transaction.txID - } else if let error = response.error?.message { - if error.lowercased().contains("16: tx-txlock-conflict") { - throw WalletServiceError.internalError( - message: String.adamant.sharedErrors.walletFrezzed, - error: nil - ) - } else { - throw WalletServiceError.internalError(message: error, error: nil) - } + }.get() + + if response.result != nil { + lastTransactionId = transaction.txID + } else if let error = response.error?.message { + if error.lowercased().contains("16: tx-txlock-conflict") { + throw WalletServiceError.remoteServiceError( + message: String.adamant.sharedErrors.walletFrezzed, + error: nil + ) } else { - throw WalletServiceError.internalError(message: "DASH Wallet: not valid response", error: nil) + throw WalletServiceError.remoteServiceError(message: error, error: nil) } - } catch { - throw WalletServiceError.remoteServiceError(message: error.localizedDescription) + } else { + throw WalletServiceError.internalError(message: "DASH Wallet: not valid response", error: nil) } } } diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService+Transactions.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+Transactions.swift new file mode 100644 index 000000000..da22af581 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+Transactions.swift @@ -0,0 +1,189 @@ +// +// DashWalletService+Transactions.swift +// Adamant +// +// Created by Anton Boyarkin on 11.04.2021. +// Copyright © 2021 Adamant. All rights reserved. +// + +import Foundation +import Alamofire +import BitcoinKit + +struct DashTransactionsPointer { + let total: Int + let transactions: [DashTransaction] + let hasMore: Bool +} + +extension DashWalletService { + + func getNextTransaction(completion: @escaping (ApiServiceResult) -> Void) { + guard let id = transatrionsIds.last else { + completion(.success(.init(total: transatrionsIds.count, transactions: [], hasMore: false))) + return + } + Task { + do { + let transaction = try await getTransaction(by: id) + handleTransactionResponse(id: id, .success(transaction), completion) + } catch { + let error = error as? WalletServiceError + let errorApi = ApiServiceError.serverError(error: error?.message ?? .empty) + handleTransactionResponse(id: id, .failure(errorApi), completion) + } + } + } + + func getTransaction(by hash: String) async throws -> BTCRawTransaction { + let result: BTCRPCServerResponce = try await dashApiService.request { + core, node in + await core.sendRequestJsonResponse( + node: node, + path: .empty, + method: .post, + parameters: DashGetRawTransactionDTO(hash: hash), + encoding: .json + ) + }.get() + + if let transaction = result.result { + return transaction + } else { + throw ApiServiceError.serverError(error: "Unaviable transaction") + } + } + + func getTransactions(by hashes: [String]) async throws -> [DashTransaction] { + guard let address = wallet?.address else { + throw ApiServiceError.notLogged + } + + let parameters: [Any] = hashes.compactMap { + DashGetRawTransactionDTO(hash: $0).asDictionary() + } + + let result: [BTCRPCServerResponce] = try await dashApiService.request { + core, node in + await core.sendRequestJsonResponse( + node: node, + path: .empty, + method: .post, + jsonParameters: parameters + ) + }.get() + + return result.compactMap { $0.result?.asBtcTransaction(DashTransaction.self, for: address) } + } + + func getBlockId(by hash: String?) async throws -> String { + guard let hash = hash else { + throw WalletServiceError.internalError(message: "Hash is empty", error: nil) + } + + let result: BTCRPCServerResponce = try await dashApiService.request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: .empty, + method: .post, + parameters: DashGetBlockDTO(hash: hash), + encoding: .json + ) + }.get() + + if let block = result.result { + return String(block.height) + } else { + throw WalletServiceError.internalError(message: "DASH: Parsing block error", error: nil) + } + } + + func getUnspentTransactions() async throws -> [UnspentTransaction] { + guard let wallet = dashWallet else { + throw WalletServiceError.internalError(message: "DASH Wallet not found", error: nil) + } + + let response: BTCRPCServerResponce<[DashUnspentTransaction]> = try await dashApiService.request { + core, node in + await core.sendRequestJsonResponse( + node: node, + path: .empty, + method: .post, + parameters: DashGetUnspentTransactionDTO(address: wallet.address), + encoding: .json + ) + }.get() + + if let result = response.result { + return result.map { + $0.asUnspentTransaction(lockScript: wallet.addressEntity.lockingScript) + } + } else if let error = response.error?.message { + throw WalletServiceError.remoteServiceError(message: error, error: nil) + } + + throw WalletServiceError.internalError( + message: "DASH Wallet: not a valid response", + error: nil + ) + } + +} + +// MARK: - Handlers + +private extension DashWalletService { + + func handleTransactionsResponse(_ response: ApiServiceResult<[String]>, _ completion: @escaping (ApiServiceResult) -> Void) { + switch response { + case .success(let ids): + transatrionsIds = ids + getNextTransaction(completion: completion) + case .failure(let error): + completion(.failure(error)) + } + } + + func handleTransactionResponse(id: String, _ response: ApiServiceResult, _ completion: @escaping (ApiServiceResult) -> Void) { + guard let address = wallet?.address else { + completion(.failure(.notLogged)) + return + } + + switch response { + case .success(let rawTransaction): + if let idx = self.transatrionsIds.firstIndex(of: id) { + self.transatrionsIds.remove(at: idx) + } + let transaction = rawTransaction.asBtcTransaction(DashTransaction.self, for: address) + completion(.success(.init(total: transatrionsIds.count, transactions: [transaction], hasMore: !transatrionsIds.isEmpty))) + case .failure(let error): + completion(.failure(error)) + } + } + +} + +// MARK: - Network Requests + +extension DashWalletService { + func requestTransactionsIds(for address: String) async throws -> [String] { + let response: BTCRPCServerResponce<[String]> = try await dashApiService.request { + core, node in + await core.sendRequestJsonResponse( + node: node, + path: .empty, + method: .post, + parameters: DashGetAddressTransactionIds(address: address), + encoding: .json + ) + }.get() + + if let result = response.result { + return result + } + + throw WalletServiceError.internalError(message: "DASH Wallet: not a valid response", error: nil) + } + +} diff --git a/Adamant/Wallets/Dash/DashWalletService.swift b/Adamant/Modules/Wallets/Dash/DashWalletService.swift similarity index 75% rename from Adamant/Wallets/Dash/DashWalletService.swift rename to Adamant/Modules/Wallets/Dash/DashWalletService.swift index aeeb0084b..3e5abd33f 100644 --- a/Adamant/Wallets/Dash/DashWalletService.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService.swift @@ -23,7 +23,7 @@ final class DashWalletService: WalletService { return type(of: self).currencyLogo } - var tokenNetworkSymbol: String { + static var tokenNetworkSymbol: String { return "DASH" } @@ -32,7 +32,7 @@ final class DashWalletService: WalletService { } var tokenUnicID: String { - return tokenNetworkSymbol + tokenSymbol + Self.tokenNetworkSymbol + tokenSymbol } var richMessageType: String { @@ -45,29 +45,21 @@ final class DashWalletService: WalletService { var wallet: WalletAccount? { return dashWallet } - var walletViewController: WalletViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Dash.wallet) as? DashWalletViewController else { - fatalError("Can't get DashWalletViewController") - } - - vc.service = self - return vc - } - // MARK: RichMessageProvider properties static let richMessageType = "dash_transaction" // MARK: - Dependencies var apiService: ApiService! + var dashApiService: DashApiService! var accountService: AccountService! var securedStore: SecuredStore! var dialogService: DialogService! - var router: Router! var addressConverter: AddressConverter! + var coreDataStack: CoreDataStack! + var vibroService: VibroService! // MARK: - Constants - static var currencyLogo = UIImage.asset(named: "dash_wallet") ?? .init() - + static let currencyLogo = UIImage.asset(named: "dash_wallet") ?? .init() static let multiplier = Decimal(sign: .plus, exponent: 8, significand: 1) static let chunkSize = 20 @@ -75,13 +67,13 @@ final class DashWalletService: WalletService { return DashWalletService.fixedFee } - private (set) var isWarningGasPrice = false + @Atomic private (set) var isWarningGasPrice = false static let kvsAddress = "dash:address" - internal var transatrionsIds = [String]() + @Atomic var transatrionsIds = [String]() - internal var lastTransactionId: String? { + var lastTransactionId: String? { get { guard let hash: String = self.securedStore.get("lastDashTransactionId"), @@ -119,24 +111,37 @@ final class DashWalletService: WalletService { let transactionFeeUpdated = Notification.Name("adamant.dashWallet.feeUpdated") // MARK: - Delayed KVS save - private var balanceObserver: NSObjectProtocol? + @Atomic private var balanceObserver: NSObjectProtocol? // MARK: - Properties - private (set) var dashWallet: DashWallet? - - private (set) var enabled = true - - public var network: Network - - private var initialBalanceCheck = false + @Atomic private (set) var dashWallet: DashWallet? + @Atomic private (set) var enabled = true + @Atomic public var network: Network let defaultDispatchQueue = DispatchQueue(label: "im.adamant.dashWalletService", qos: .userInteractive, attributes: [.concurrent]) static let jsonDecoder = JSONDecoder() - private var subscriptions = Set() + @Atomic private var subscriptions = Set() + + @ObservableValue private(set) var historyTransactions: [TransactionDetails] = [] + @ObservableValue private(set) var hasMoreOldTransactions: Bool = true + var transactionsPublisher: AnyObservable<[TransactionDetails]> { + $historyTransactions.eraseToAnyPublisher() + } + + var hasMoreOldTransactionsPublisher: AnyObservable { + $hasMoreOldTransactions.eraseToAnyPublisher() + } + + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( + coinId: tokenUnicID, + coreDataStack: coreDataStack, + blockchainType: richMessageType + ) + // MARK: - State - private (set) var state: WalletServiceState = .notInitiated + @Atomic private (set) var state: WalletServiceState = .notInitiated private func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { @@ -146,9 +151,11 @@ final class DashWalletService: WalletService { state = newState if !silent { - NotificationCenter.default.post(name: serviceStateChanged, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.walletState: state]) + NotificationCenter.default.post( + name: serviceStateChanged, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.walletState: state] + ) } } @@ -183,11 +190,21 @@ final class DashWalletService: WalletService { .receive(on: OperationQueue.main) .sink { [weak self] _ in self?.dashWallet = nil - self?.initialBalanceCheck = false if let balanceObserver = self?.balanceObserver { NotificationCenter.default.removeObserver(balanceObserver) self?.balanceObserver = nil } + self?.coinStorage.clear() + self?.hasMoreOldTransactions = true + self?.historyTransactions = [] + } + .store(in: &subscriptions) + } + + func addTransactionObserver() { + coinStorage.transactionsPublisher + .sink { [weak self] transactions in + self?.historyTransactions = transactions } .store(in: &subscriptions) } @@ -214,20 +231,24 @@ final class DashWalletService: WalletService { setState(.updating) if let balance = try? await getBalance() { - wallet.isBalanceInitialized = true let notification: Notification.Name? + let isRaised = (wallet.balance < balance) && wallet.isBalanceInitialized if wallet.balance != balance { wallet.balance = balance notification = walletUpdatedNotification - initialBalanceCheck = false - } else if initialBalanceCheck { - initialBalanceCheck = false + } else if !wallet.isBalanceInitialized { notification = walletUpdatedNotification } else { notification = nil } + wallet.isBalanceInitialized = true + + if isRaised { + vibroService.applyVibration(.success) + } + if let notification = notification { NotificationCenter.default.post( name: notification, @@ -282,6 +303,12 @@ extension DashWalletService: InitiatedWithPassphraseService { self.dashWallet = eWallet + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: eWallet] + ) + if !self.enabled { self.enabled = true NotificationCenter.default.post(name: self.serviceEnabledChanged, object: self) @@ -297,7 +324,6 @@ extension DashWalletService: InitiatedWithPassphraseService { } } - service.initialBalanceCheck = true service.setState(.upToDate, silent: true) Task { service.update() @@ -337,9 +363,13 @@ extension DashWalletService: SwinjectDependentService { apiService = container.resolve(ApiService.self) securedStore = container.resolve(SecuredStore.self) dialogService = container.resolve(DialogService.self) - router = container.resolve(Router.self) addressConverter = container.resolve(AddressConverterFactory.self)? .make(network: network) + dashApiService = container.resolve(DashApiService.self) + vibroService = container.resolve(VibroService.self) + coreDataStack = container.resolve(CoreDataStack.self) + + addTransactionObserver() } } @@ -354,28 +384,15 @@ extension DashWalletService { } func getBalance(address: String) async throws -> Decimal { - guard let endpoint = DashWalletService.nodes.randomElement()?.asURL() else { - let message = "Failed to get DASH endpoint URL" - assertionFailure(message) - throw WalletServiceError.internalError(message: message, error: nil) - } - - // Parameters - let parameters: Parameters = [ - "method": "getaddressbalance", - "params": [ - address - ] - ] - - // MARK: Sending request - - let data = try await apiService.sendRequest( - url: endpoint, - method: .post, - parameters: parameters, - encoding: JSONEncoding.default - ) + let data: Data = try await dashApiService.request { core, node in + await core.sendRequest( + node: node, + path: .empty, + method: .post, + parameters: DashGetAddressBalanceDTO(address: address), + encoding: .json + ) + }.get() let object = try? JSONSerialization.jsonObject( with: data, @@ -401,7 +418,7 @@ extension DashWalletService { func getWalletAddress(byAdamantAddress address: String) async throws -> String { do { - let result = try await apiService.get(key: DashWalletService.kvsAddress, sender: address) + let result = try await apiService.get(key: DashWalletService.kvsAddress, sender: address).get() guard let result = result else { throw WalletServiceError.walletNotInitiated @@ -414,6 +431,43 @@ extension DashWalletService { ) } } + + func loadTransactions(offset: Int, limit: Int) async throws -> Int { + guard let address = wallet?.address else { + return .zero + } + + let allTransactionsIds = try await requestTransactionsIds(for: address).reversed() + + let availableToLoad = allTransactionsIds.count - offset + + let maxPerRequest = availableToLoad > limit + ? limit + : availableToLoad + + let startIndex = allTransactionsIds.index(allTransactionsIds.startIndex, offsetBy: offset) + let endIndex = allTransactionsIds.index(startIndex, offsetBy: maxPerRequest) + let ids = Array(allTransactionsIds[startIndex.. 0 else { + hasMoreOldTransactions = false + return .zero + } + + coinStorage.append(trs) + + return trs.count + } + + func getLocalTransactionHistory() -> [TransactionDetails] { + historyTransactions + } + + func updateStatus(for id: String, status: TransactionStatus?) { + coinStorage.updateStatus(for: id, status: status) + } } // MARK: - KVS @@ -434,14 +488,20 @@ extension DashWalletService { } Task { - await apiService.store(key: DashWalletService.kvsAddress, value: dashAddress, type: .keyValue, sender: adamant.address, keypair: keypair) { result in - switch result { - case .success: - completion(.success) + let result = await apiService.store( + key: DashWalletService.kvsAddress, + value: dashAddress, + type: .keyValue, + sender: adamant.address, + keypair: keypair + ) + + switch result { + case .success: + completion(.success) - case .failure(let error): - completion(.failure(error: .apiError(error))) - } + case .failure(let error): + completion(.failure(error: .apiError(error))) } } } @@ -481,18 +541,6 @@ extension DashWalletService { } } -// MARK: - WalletServiceWithTransfers -extension DashWalletService: WalletServiceWithTransfers { - func transferListViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Dash.transactionsList) as? DashTransactionsViewController else { - fatalError("Can't get DashTransactionsViewController") - } - - vc.walletService = self - return vc - } -} - // MARK: - PrivateKey generator extension DashWalletService: PrivateKeyGenerator { var rowTitle: String { diff --git a/Adamant/Wallets/Dash/DashWalletViewController.swift b/Adamant/Modules/Wallets/Dash/DashWalletViewController.swift similarity index 93% rename from Adamant/Wallets/Dash/DashWalletViewController.swift rename to Adamant/Modules/Wallets/Dash/DashWalletViewController.swift index 6791ddf7c..6bda5de7d 100644 --- a/Adamant/Wallets/Dash/DashWalletViewController.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletViewController.swift @@ -16,7 +16,7 @@ extension String.adamant { static let sendDash = String.localized("AccountTab.Row.SendDash", comment: "Account tab: 'Send Dash tokens' button") } -class DashWalletViewController: WalletViewControllerBase { +final class DashWalletViewController: WalletViewControllerBase { // MARK: Lifecycle override func viewDidLoad() { diff --git a/Adamant/Modules/Wallets/Doge/DTO/DogeBlockDTO.swift b/Adamant/Modules/Wallets/Doge/DTO/DogeBlockDTO.swift new file mode 100644 index 000000000..42861486f --- /dev/null +++ b/Adamant/Modules/Wallets/Doge/DTO/DogeBlockDTO.swift @@ -0,0 +1,15 @@ +// +// DogeBlockDTO.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct DogeBlockDTO: Codable { + let height: Int + let hash: String + // there are more fields +} diff --git a/Adamant/Modules/Wallets/Doge/DTO/DogeBlocksDTO.swift b/Adamant/Modules/Wallets/Doge/DTO/DogeBlocksDTO.swift new file mode 100644 index 000000000..03e1439f3 --- /dev/null +++ b/Adamant/Modules/Wallets/Doge/DTO/DogeBlocksDTO.swift @@ -0,0 +1,13 @@ +// +// DogeBlocksDTO.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +struct DogeBlocksDTO: Codable { + let blocks: [DogeBlockDTO] +} diff --git a/Adamant/Modules/Wallets/Doge/DogeApiService.swift b/Adamant/Modules/Wallets/Doge/DogeApiService.swift new file mode 100644 index 000000000..8f9c77b35 --- /dev/null +++ b/Adamant/Modules/Wallets/Doge/DogeApiService.swift @@ -0,0 +1,79 @@ +// +// DogeApiService.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +final class DogeApiCore: BlockchainHealthCheckableService { + let apiCore: APICoreProtocol + + init(apiCore: APICoreProtocol) { + self.apiCore = apiCore + } + + func request( + node: Node, + _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + ) async -> WalletServiceResult { + await request(apiCore, node).mapError { $0.asWalletServiceError() } + } + + func getStatusInfo(node: Node) async -> WalletServiceResult { + let startTimestamp = Date.now.timeIntervalSince1970 + + let response: WalletServiceResult = await request(node: node) { core, node in + await core.sendRequestJsonResponse( + node: node, + path: DogeApiCommands.getBlocks(), + method: .get, + parameters: ["limit": 0], + encoding: .url + ) + } + + return response.map { data in + return .init( + ping: Date.now.timeIntervalSince1970 - startTimestamp, + height: data.blocks.first?.height ?? .zero, + wsEnabled: false, + wsPort: nil, + version: nil + ) + } + } +} + +final class DogeApiService: WalletApiService { + let api: BlockchainHealthCheckWrapper + + var preferredNodeIds: [UUID] { + api.preferredNodeIds + } + + init(api: BlockchainHealthCheckWrapper) { + self.api = api + } + + func healthCheck() { + api.healthCheck() + } + + func request( + _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + ) async -> WalletServiceResult { + await api.request { core, node in + await core.request(node: node, request) + } + } + + func getStatusInfo() async -> WalletServiceResult { + await api.request { core, node in + await core.getStatusInfo(node: node) + } + } +} diff --git a/Adamant/Wallets/Doge/DogeMainnet.swift b/Adamant/Modules/Wallets/Doge/DogeMainnet.swift similarity index 96% rename from Adamant/Wallets/Doge/DogeMainnet.swift rename to Adamant/Modules/Wallets/Doge/DogeMainnet.swift index fdae743aa..fbc2907d0 100644 --- a/Adamant/Wallets/Doge/DogeMainnet.swift +++ b/Adamant/Modules/Wallets/Doge/DogeMainnet.swift @@ -9,7 +9,7 @@ import Foundation import BitcoinKit -class DogeMainnet: Network { +final class DogeMainnet: Network { override var name: String { return "livenet" } diff --git a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Doge/DogeTransactionDetailsViewController.swift similarity index 97% rename from Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift rename to Adamant/Modules/Wallets/Doge/DogeTransactionDetailsViewController.swift index dbf098b86..fc3fdfa8d 100644 --- a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Doge/DogeTransactionDetailsViewController.swift @@ -10,7 +10,7 @@ import UIKit import Eureka import CommonKit -class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase { +final class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies weak var service: DogeWalletService? diff --git a/Adamant/Modules/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Modules/Wallets/Doge/DogeTransactionsViewController.swift new file mode 100644 index 000000000..bed5bf415 --- /dev/null +++ b/Adamant/Modules/Wallets/Doge/DogeTransactionsViewController.swift @@ -0,0 +1,67 @@ +// +// DogeTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 11/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit +import ProcedureKit +import CommonKit + +final class DogeTransactionsViewController: TransactionsListViewControllerBase { + + // MARK: - Dependencies + var dogeWalletService: DogeWalletService! + var screensFactory: ScreensFactory! + + // MARK: - UITableView + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let address = walletService.wallet?.address, + let transaction = transactions[safe: indexPath.row] + else { return } + + let controller = screensFactory.makeDetailsVC(service: dogeWalletService) + controller.transaction = transaction + + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { + controller.senderName = String.adamant.transactionDetails.yourAddress + } + + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { + controller.recipientName = String.adamant.transactionDetails.yourAddress + } + + navigationController?.pushViewController(controller, animated: true) + } +} + +private class LoadMoreDogeTransactionsProcedure: Procedure { + let from: Int + let service: DogeWalletService + + private(set) var result: (transactions: [DogeTransaction], hasMore: Bool)? + + init(service: DogeWalletService, from: Int) { + self.from = from + self.service = service + + super.init() + log.severity = .warning + } + + override func execute() { + Task { + do { + let result = try await service.getTransactions(from: from) + self.result = result + self.finish() + } catch { + self.result = nil + self.finish(with: error) + } + } + } +} diff --git a/Adamant/Wallets/Doge/DogeTransferViewController.swift b/Adamant/Modules/Wallets/Doge/DogeTransferViewController.swift similarity index 82% rename from Adamant/Wallets/Doge/DogeTransferViewController.swift rename to Adamant/Modules/Wallets/Doge/DogeTransferViewController.swift index 2867d5d4b..7f5483130 100644 --- a/Adamant/Wallets/Doge/DogeTransferViewController.swift +++ b/Adamant/Modules/Wallets/Doge/DogeTransferViewController.swift @@ -31,7 +31,7 @@ final class DogeTransferViewController: TransferViewControllerBase { return } - guard service.wallet != nil else { + guard let wallet = service.wallet else { return } @@ -55,9 +55,26 @@ final class DogeTransferViewController: TransferViewControllerBase { Task { do { + let simpleTransaction = SimpleTransactionDetails( + txId: transaction.txID, + senderAddress: wallet.address, + recipientAddress: recipient, + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: true, + transactionStatus: nil + ) + + service.coinStorage.append(simpleTransaction) try await service.sendTransaction(transaction) } catch { dialogService.showRichError(error: error) + service.coinStorage.updateStatus( + for: transaction.txId, + status: .failed + ) } await service.update() @@ -84,13 +101,8 @@ final class DogeTransferViewController: TransferViewControllerBase { comments: String, service: DogeWalletService ) { - guard let detailsVc = router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController else { - delegate?.transferViewController(self, didFinishWithTransfer: transaction, detailsViewController: nil) - return - } - + let detailsVc = screensFactory.makeDetailsVC(service: service) detailsVc.transaction = transaction - detailsVc.service = service detailsVc.senderName = String.adamant.transactionDetails.yourAddress detailsVc.recipientName = recipientName diff --git a/Adamant/Wallets/Doge/DogeWallet.swift b/Adamant/Modules/Wallets/Doge/DogeWallet.swift similarity index 100% rename from Adamant/Wallets/Doge/DogeWallet.swift rename to Adamant/Modules/Wallets/Doge/DogeWallet.swift diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift b/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift new file mode 100644 index 000000000..2f6230e57 --- /dev/null +++ b/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift @@ -0,0 +1,137 @@ +// +// DogeWalletFactory.swift +// Adamant +// +// Created by Anton Boyarkin on 05/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Swinject +import CommonKit +import UIKit + +struct DogeWalletFactory: WalletFactory { + typealias Service = DogeWalletService + + let assembler: Assembler + + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { + let c = DogeWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) + c.dialogService = assembler.resolve(DialogService.self) + c.currencyInfoService = assembler.resolve(CurrencyInfoService.self) + c.accountService = assembler.resolve(AccountService.self) + c.service = service + c.screensFactory = screensFactory + return c + } + + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { + let vc = DogeTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) + vc.dialogService = assembler.resolve(DialogService.self) + vc.screensFactory = screensFactory + vc.dogeWalletService = service + vc.walletService = service + return vc + } + + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { + let vc = DogeTransferViewController( + chatsProvider: assembler.resolve(ChatsProvider.self)!, + accountService: assembler.resolve(AccountService.self)!, + accountsProvider: assembler.resolve(AccountsProvider.self)!, + dialogService: assembler.resolve(DialogService.self)!, + screensFactory: screensFactory, + currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, + vibroService: assembler.resolve(VibroService.self)! + ) + + vc.service = service + return vc + } + + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + else { return nil } + + let comment: String? + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { + comment = raw + } else { + comment = nil + } + + return makeTransactionDetailsVC( + hash: hash, + senderId: transaction.senderId, + recipientId: transaction.recipientId, + comment: comment, + senderAddress: "", + recipientAddress: "", + transaction: nil, + richTransaction: transaction, + service: service + ) + } + + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { + makeTransactionDetailsVC(service: service) + } +} + +private extension DogeWalletFactory { + func makeTransactionDetailsVC( + hash: String, + senderId: String?, + recipientId: String?, + comment: String?, + senderAddress: String, + recipientAddress: String, + transaction: DogeTransaction?, + richTransaction: RichMessageTransaction, + service: Service + ) -> UIViewController { + let vc = makeTransactionDetailsVC(service: service) + vc.senderId = senderId + vc.recipientId = recipientId + vc.comment = comment + + let amount: Decimal + if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), + let decimal = Decimal(string: amountRaw) { + amount = decimal + } else { + amount = 0 + } + + let failedTransaction = SimpleTransactionDetails( + txId: hash, + senderAddress: senderAddress, + recipientAddress: recipientAddress, + dateValue: nil, + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: richTransaction.isOutgoing, + transactionStatus: nil + ) + + vc.transaction = transaction ?? failedTransaction + vc.richTransaction = richTransaction + return vc + } + + func makeTransactionDetailsVC(service: Service) -> DogeTransactionDetailsViewController { + let vc = DogeTransactionDetailsViewController( + dialogService: assembler.resolve(DialogService.self)!, + currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + addressBookService: assembler.resolve(AddressBookService.self)!, + accountService: assembler.resolve(AccountService.self)!, + walletService: service + ) + + vc.service = service + return vc + } +} diff --git a/Adamant/Wallets/Doge/DogeWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService+DynamicConstants.swift similarity index 88% rename from Adamant/Wallets/Doge/DogeWalletService+DynamicConstants.swift rename to Adamant/Modules/Wallets/Doge/DogeWalletService+DynamicConstants.swift index 9c754f943..95ab74a36 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService+DynamicConstants.swift @@ -53,12 +53,16 @@ extension DogeWalletService { 70 } + var minNodeVersion: String? { + nil + } + static let explorerAddress = "https://dogechain.info/tx/" static var nodes: [Node] { [ Node(url: URL(string: "https://dogenode1.adamant.im")!), -Node(url: URL(string: "https://dogenode2.adamant.im")!), +Node(url: URL(string: "https://dogenode2.adamant.im")!, altUrl: URL(string: "http://176.9.32.126:44098")), ] } diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProvider.swift new file mode 100644 index 000000000..f23784ea1 --- /dev/null +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProvider.swift @@ -0,0 +1,64 @@ +// +// DogeWalletService+RichMessageProvider.swift +// Adamant +// +// Created by Anton Boyarkin on 13/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation +import MessageKit +import UIKit +import CommonKit + +extension DogeWalletService: RichMessageProvider { + var newPendingInterval: TimeInterval { + .init(milliseconds: type(of: self).newPendingInterval) + } + + var oldPendingInterval: TimeInterval { + .init(milliseconds: type(of: self).oldPendingInterval) + } + + var registeredInterval: TimeInterval { + .init(milliseconds: type(of: self).registeredInterval) + } + + var newPendingAttempts: Int { + type(of: self).newPendingAttempts + } + + var oldPendingAttempts: Int { + type(of: self).oldPendingAttempts + } + + var dynamicRichMessageType: String { + return type(of: self).richMessageType + } + + // MARK: Short description + + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { + let amount: String + + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { + return NSAttributedString(string: "⬅️ \(DogeWalletService.currencySymbol)") + } + + if let decimal = Decimal(string: raw) { + amount = AdamantBalanceFormat.full.format(decimal) + } else { + amount = raw + } + + let string: String + if transaction.isOutgoing { + string = "⬅️ \(amount) \(DogeWalletService.currencySymbol)" + } else { + string = "➡️ \(amount) \(DogeWalletService.currencySymbol)" + } + + return NSAttributedString(string: string) + } +} diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift similarity index 76% rename from Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift rename to Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift index 6c2d4d0c8..bf9b84306 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift @@ -10,9 +10,16 @@ import Foundation import CommonKit extension DogeWalletService: RichMessageProviderWithStatusCheck { - func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { - guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) - else { + func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { + let hash: String? + + if let transaction = transaction as? RichMessageTransaction { + hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + } else { + hash = transaction.txId + } + + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent) } @@ -21,12 +28,7 @@ extension DogeWalletService: RichMessageProviderWithStatusCheck { do { dogeTransaction = try await getTransaction(by: hash) } catch { - switch error { - case ApiServiceError.networkError(_): - return .init(sentDate: nil, status: .noNetwork) - default: - return .init(sentDate: nil, status: .pending) - } + return .init(error: error) } return .init( @@ -42,7 +44,7 @@ extension DogeWalletService: RichMessageProviderWithStatusCheck { private extension DogeWalletService { func getStatus( dogeTransaction: BTCRawTransaction, - transaction: RichMessageTransaction + transaction: CoinTransaction ) -> TransactionStatus { // MARK: Check confirmations guard let confirmations = dogeTransaction.confirmations, @@ -53,10 +55,7 @@ private extension DogeWalletService { } // MARK: Check amount & address - guard - let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), - let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) - else { + guard let reportedValue = reportedValue(for: transaction) else { return .inconsistent } @@ -98,4 +97,20 @@ private extension DogeWalletService { return result } + + func reportedValue(for transaction: CoinTransaction) -> Decimal? { + guard let transaction = transaction as? RichMessageTransaction + else { + return transaction.amountValue + } + + guard + let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) + else { + return nil + } + + return reportedValue + } } diff --git a/Adamant/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift similarity index 59% rename from Adamant/Wallets/Doge/DogeWalletService+Send.swift rename to Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift index caaf4f960..a089708b3 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift @@ -9,6 +9,7 @@ import UIKit import BitcoinKit import Alamofire +import CommonKit extension BitcoinKit.Transaction: RawTransaction { var txHash: String? { @@ -19,15 +20,6 @@ extension BitcoinKit.Transaction: RawTransaction { extension DogeWalletService: WalletServiceTwoStepSend { typealias T = BitcoinKit.Transaction - func transferViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Doge.transfer) as? DogeTransferViewController else { - fatalError("Can't get DogeTransferViewController") - } - - vc.service = self - return vc - } - // MARK: Create & Send func createTransaction(recipient: String, amount: Decimal) async throws -> BitcoinKit.Transaction { // Prepare @@ -65,61 +57,31 @@ extension DogeWalletService: WalletServiceTwoStepSend { ) return transaction } catch { - throw WalletServiceError.notEnoughMoney + throw error } } func sendTransaction(_ transaction: BitcoinKit.Transaction) async throws { - guard let url = DogeWalletService.nodes.randomElement()?.asURL() else { - throw WalletServiceError.internalError( - message: "Failed to get DOGE endpoint URL", - error: nil - ) - } - - // Request url - let endpoint = url.appendingPathComponent(DogeApiCommands.sendTransaction()) - - // Headers - let headers: HTTPHeaders = [ - "Content-Type": "application/json" - ] - - // MARK: Prepare params let txHex = transaction.serialized().hex - let parameters: Parameters = [ - "rawtx": txHex - ] - - // MARK: Sending request - _ = try await withUnsafeThrowingContinuation { continuation in - AF.request( - endpoint, + _ = try await dogeApiService.api.request { core, node in + let response: APIResponseModel = await core.apiCore.sendRequestBasic( + node: node, + path: DogeApiCommands.sendTransaction(), method: .post, - parameters: parameters, - encoding: JSONEncoding.default, - headers: headers + parameters: ["rawtx": txHex], + encoding: .json ) - .validate(statusCode: 200 ... 299) - .responseJSON(queue: defaultDispatchQueue) { response in - switch response.result { - case .success: - continuation.resume() - case .failure(let error): - guard let data = response.data else { - continuation.resume(throwing: WalletServiceError.remoteServiceError(message: error.localizedDescription)) - return - } - let result = String(decoding: data, as: UTF8.self) - if result.contains("dust") && result.contains("-26") { - continuation.resume(throwing: WalletServiceError.dustAmountError) - return - } - continuation.resume(throwing: WalletServiceError.remoteServiceError(message: error.localizedDescription)) - } - } - } + + guard + !(200 ... 299).contains(response.code ?? .zero), + let dataString = response.data.map({ String(decoding: $0, as: UTF8.self) }), + dataString.contains("dust"), + dataString.contains("-26") + else { return response.result.mapError { $0.asWalletServiceError() } } + + return .failure(.dustAmountError) + }.get() } } diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService.swift similarity index 72% rename from Adamant/Wallets/Doge/DogeWalletService.swift rename to Adamant/Modules/Wallets/Doge/DogeWalletService.swift index e3af97d18..a6a4d2a06 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService.swift @@ -30,6 +30,10 @@ struct DogeApiCommands { return "/api/block/\(hash)" } + static func getBlocks() -> String { + return "/api/blocks" + } + static func getUnspentTransactions(for address: String) -> String { return "/api/addr/\(address)/utxo" } @@ -39,30 +43,23 @@ struct DogeApiCommands { } } -class DogeWalletService: WalletService { +final class DogeWalletService: WalletService { var wallet: WalletAccount? { return dogeWallet } - var walletViewController: WalletViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Doge.wallet) as? DogeWalletViewController else { - fatalError("Can't get DogeWalletViewController") - } - - vc.service = self - return vc - } - // MARK: RichMessageProvider properties static let richMessageType = "doge_transaction" // MARK: - Dependencies var apiService: ApiService! + var dogeApiService: DogeApiService! var accountService: AccountService! var dialogService: DialogService! - var router: Router! var addressConverter: AddressConverter! + var vibroService: VibroService! + var coreDataStack: CoreDataStack! // MARK: - Constants - static var currencyLogo = UIImage.asset(named: "doge_wallet") ?? .init() + static let currencyLogo = UIImage.asset(named: "doge_wallet") ?? .init() static let multiplier = Decimal(sign: .plus, exponent: 8, significand: 1) static let chunkSize = 20 @@ -74,7 +71,7 @@ class DogeWalletService: WalletService { return type(of: self).currencyLogo } - var tokenNetworkSymbol: String { + static var tokenNetworkSymbol: String { return "DOGE" } @@ -83,7 +80,7 @@ class DogeWalletService: WalletService { } var tokenUnicID: String { - return tokenNetworkSymbol + tokenSymbol + Self.tokenNetworkSymbol + tokenSymbol } var transactionFee: Decimal { @@ -100,7 +97,7 @@ class DogeWalletService: WalletService { static let kvsAddress = "doge:address" - private (set) var isWarningGasPrice = false + @Atomic private(set) var isWarningGasPrice = false // MARK: - Notifications let walletUpdatedNotification = Notification.Name("adamant.dogeWallet.walletUpdated") @@ -109,24 +106,41 @@ class DogeWalletService: WalletService { let transactionFeeUpdated = Notification.Name("adamant.dogeWallet.feeUpdated") // MARK: - Delayed KVS save - private var balanceObserver: NSObjectProtocol? + @Atomic private var balanceObserver: NSObjectProtocol? // MARK: - Properties - private (set) var dogeWallet: DogeWallet? + @Atomic private(set) var dogeWallet: DogeWallet? + @Atomic private(set) var enabled = true + @Atomic public var network: Network - private (set) var enabled = true + let defaultDispatchQueue = DispatchQueue( + label: "im.adamant.dogeWalletService", + qos: .userInteractive, + attributes: [.concurrent] + ) - public var network: Network + private static let jsonDecoder = JSONDecoder() + @Atomic private var subscriptions = Set() + + @ObservableValue private(set) var historyTransactions: [TransactionDetails] = [] + @ObservableValue private(set) var hasMoreOldTransactions: Bool = true + + var transactionsPublisher: AnyObservable<[TransactionDetails]> { + $historyTransactions.eraseToAnyPublisher() + } - private var initialBalanceCheck = false + var hasMoreOldTransactionsPublisher: AnyObservable { + $hasMoreOldTransactions.eraseToAnyPublisher() + } - let defaultDispatchQueue = DispatchQueue(label: "im.adamant.dogeWalletService", qos: .userInteractive, attributes: [.concurrent]) + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( + coinId: tokenUnicID, + coreDataStack: coreDataStack, + blockchainType: richMessageType + ) - private static let jsonDecoder = JSONDecoder() - private var subscriptions = Set() - // MARK: - State - private (set) var state: WalletServiceState = .notInitiated + @Atomic private (set) var state: WalletServiceState = .notInitiated private func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { @@ -136,15 +150,16 @@ class DogeWalletService: WalletService { state = newState if !silent { - NotificationCenter.default.post(name: serviceStateChanged, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.walletState: state]) + NotificationCenter.default.post( + name: serviceStateChanged, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.walletState: state] + ) } } init() { self.network = DogeMainnet() - self.setState(.notInitiated) // Notifications @@ -173,11 +188,21 @@ class DogeWalletService: WalletService { .receive(on: OperationQueue.main) .sink { [weak self] _ in self?.dogeWallet = nil - self?.initialBalanceCheck = false if let balanceObserver = self?.balanceObserver { NotificationCenter.default.removeObserver(balanceObserver) self?.balanceObserver = nil } + self?.coinStorage.clear() + self?.hasMoreOldTransactions = true + self?.historyTransactions = [] + } + .store(in: &subscriptions) + } + + func addTransactionObserver() { + coinStorage.transactionsPublisher + .sink { [weak self] transactions in + self?.historyTransactions = transactions } .store(in: &subscriptions) } @@ -204,20 +229,24 @@ class DogeWalletService: WalletService { setState(.updating) if let balance = try? await getBalance() { - wallet.isBalanceInitialized = true let notification: Notification.Name? + let isRaised = (wallet.balance < balance) && wallet.isBalanceInitialized if wallet.balance != balance { wallet.balance = balance notification = walletUpdatedNotification - initialBalanceCheck = false - } else if initialBalanceCheck { - initialBalanceCheck = false + } else if !wallet.isBalanceInitialized { notification = walletUpdatedNotification } else { notification = nil } + wallet.isBalanceInitialized = true + + if isRaised { + vibroService.applyVibration(.success) + } + if let notification = notification { NotificationCenter.default.post(name: notification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet]) } @@ -263,6 +292,12 @@ extension DogeWalletService: InitiatedWithPassphraseService { let eWallet = try DogeWallet(privateKey: privateKey, addressConverter: addressConverter) self.dogeWallet = eWallet + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: eWallet] + ) + if !self.enabled { self.enabled = true NotificationCenter.default.post(name: self.serviceEnabledChanged, object: self) @@ -278,7 +313,6 @@ extension DogeWalletService: InitiatedWithPassphraseService { } } - service.initialBalanceCheck = true service.setState(.upToDate, silent: true) Task { @@ -313,14 +347,17 @@ extension DogeWalletService: InitiatedWithPassphraseService { // MARK: - Dependencies extension DogeWalletService: SwinjectDependentService { - @MainActor func injectDependencies(from container: Container) { accountService = container.resolve(AccountService.self) apiService = container.resolve(ApiService.self) dialogService = container.resolve(DialogService.self) - router = container.resolve(Router.self) addressConverter = container.resolve(AddressConverterFactory.self)? .make(network: network) + dogeApiService = container.resolve(DogeApiService.self) + vibroService = container.resolve(VibroService.self) + coreDataStack = container.resolve(CoreDataStack.self) + + addTransactionObserver() } } @@ -335,43 +372,27 @@ extension DogeWalletService { } func getBalance(address: String) async throws -> Decimal { - guard let url = DogeWalletService.nodes.randomElement()?.asURL() else { - let message = "Failed to get DOGE endpoint URL" - assertionFailure(message) - throw WalletServiceError.internalError(message: message, error: nil) - } - - // Headers - let headers: HTTPHeaders = [ - "Content-Type": "application/json" - ] - - // Request url - let endpoint = url.appendingPathComponent(DogeApiCommands.balance(for: address)) - - // MARK: Sending request - - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - AF.request(endpoint, method: .get, headers: headers).responseString { response in - switch response.result { - case .success(let data): - if let raw = Decimal(string: data) { - let balance = raw / DogeWalletService.multiplier - continuation.resume(returning: balance) - } else { - continuation.resume(throwing: WalletServiceError.remoteServiceError(message: "DOGE Wallet: \(data)")) - } - - case .failure: - continuation.resume(throwing: WalletServiceError.networkError) - } - } + let data: Data = try await dogeApiService.request { core, node in + await core.sendRequest( + node: node, + path: DogeApiCommands.balance(for: address) + ) + }.get() + + if + let string = String(data: data, encoding: .utf8), + let raw = Decimal(string: string) + { + let balance = raw / DogeWalletService.multiplier + return balance + } else { + throw WalletServiceError.internalError(InternalAPIError.parsingFailed) } } func getWalletAddress(byAdamantAddress address: String) async throws -> String { do { - let result = try await apiService.get(key: DogeWalletService.kvsAddress, sender: address) + let result = try await apiService.get(key: DogeWalletService.kvsAddress, sender: address).get() guard let result = result else { throw WalletServiceError.walletNotInitiated @@ -403,14 +424,20 @@ extension DogeWalletService { } Task { - await apiService.store(key: DogeWalletService.kvsAddress, value: dogeAddress, type: .keyValue, sender: adamant.address, keypair: keypair) { result in - switch result { - case .success: - completion(.success) - - case .failure(let error): - completion(.failure(error: .apiError(error))) - } + let result = await apiService.store( + key: DogeWalletService.kvsAddress, + value: dogeAddress, + type: .keyValue, + sender: adamant.address, + keypair: keypair + ) + + switch result { + case .success: + completion(.success) + + case .failure(let error): + completion(.failure(error: .apiError(error))) } } } @@ -470,63 +497,54 @@ extension DogeWalletService { return (transactions: transactions, hasMore: hasMore) } - private func getTransactions(for address: String, from: Int, to: Int) async throws -> DogeGetTransactionsResponse { - guard let url = DogeWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get DOGE endpoint URL") - } - - let parameters: Parameters = [ + private func getTransactions( + for address: String, + from: Int, + to: Int + ) async throws -> DogeGetTransactionsResponse { + let parameters = [ "from": from, "to": to ] - // Request url - let endpoint = url.appendingPathComponent(DogeApiCommands.getTransactions(for: address)) - - // MARK: Sending request - do { - let dogeResponse: DogeGetTransactionsResponse = try await apiService.sendRequest( - url: endpoint, + return try await dogeApiService.request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: DogeApiCommands.getTransactions(for: address), method: .get, - parameters: parameters + parameters: parameters, + encoding: .url ) - return dogeResponse - } catch { - throw WalletServiceError.remoteServiceError(message: "DOGE Wallet: not a valid response") - } + }.get() } func getUnspentTransactions() async throws -> [UnspentTransaction] { - guard let url = DogeWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get DOGE endpoint URL") - } - guard let wallet = self.dogeWallet else { throw WalletServiceError.notLogged } let address = wallet.address - // Request url - let endpoint = url.appendingPathComponent(DogeApiCommands.getUnspentTransactions(for: address)) - - let parameters: Parameters = [ + let parameters = [ "noCache": "1" ] // MARK: Sending request + let data = try await dogeApiService.request { core, node in + await core.sendRequest( + node: node, + path: DogeApiCommands.getUnspentTransactions(for: address), + method: .get, + parameters: parameters, + encoding: .url + ) + }.get() - let data = try await apiService.sendRequest( - url: endpoint, - method: .get, - parameters: parameters - ) - let items = try? JSONSerialization.jsonObject( with: data, options: [] ) as? [[String: Any]] - + guard let items = items else { throw WalletServiceError.remoteServiceError( message: "DOGE Wallet: not valid response" @@ -561,39 +579,18 @@ extension DogeWalletService { } func getTransaction(by hash: String) async throws -> BTCRawTransaction { - guard let url = DogeWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get DOGE endpoint URL") - } - - // Request url - - let endpoint = url.appendingPathComponent(DogeApiCommands.getTransaction(by: hash)) - - // MARK: Sending request - - let transaction: BTCRawTransaction = try await apiService.sendRequest( - url: endpoint, - method: .get, - parameters: nil - ) - - return transaction + try await dogeApiService.request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: DogeApiCommands.getTransaction(by: hash) + ) + }.get() } func getBlockId(by hash: String) async throws -> String { - guard let url = DogeWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get DOGE endpoint URL") - } - - // Request url - - let endpoint = url.appendingPathComponent(DogeApiCommands.getBlock(by: hash)) - - let data = try await apiService.sendRequest( - url: endpoint, - method: .get, - parameters: nil - ) + let data = try await dogeApiService.request { core, node in + await core.sendRequest(node: node, path: DogeApiCommands.getBlock(by: hash)) + }.get() let json = try? JSONSerialization.jsonObject( with: data, @@ -612,17 +609,29 @@ extension DogeWalletService { throw WalletServiceError.remoteServiceError(message: "Failed to parse block") } } -} - -// MARK: - WalletServiceWithTransfers -extension DogeWalletService: WalletServiceWithTransfers { - func transferListViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Doge.transactionsList) as? DogeTransactionsViewController else { - fatalError("Can't get DogeTransactionsViewController") + + func loadTransactions(offset: Int, limit: Int) async throws -> Int { + let tuple = try await getTransactions(from: offset) + + let trs = tuple.transactions + hasMoreOldTransactions = tuple.hasMore + + guard trs.count > 0 else { + hasMoreOldTransactions = false + return .zero } - vc.walletService = self - return vc + coinStorage.append(trs) + + return trs.count + } + + func getLocalTransactionHistory() -> [TransactionDetails] { + historyTransactions + } + + func updateStatus(for id: String, status: TransactionStatus?) { + coinStorage.updateStatus(for: id, status: status) } } diff --git a/Adamant/Wallets/Doge/DogeWalletViewController.swift b/Adamant/Modules/Wallets/Doge/DogeWalletViewController.swift similarity index 92% rename from Adamant/Wallets/Doge/DogeWalletViewController.swift rename to Adamant/Modules/Wallets/Doge/DogeWalletViewController.swift index fba1beb88..7499bbb50 100644 --- a/Adamant/Wallets/Doge/DogeWalletViewController.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletViewController.swift @@ -15,7 +15,7 @@ extension String.adamant { static let sendDoge = String.localized("AccountTab.Row.SendDoge", comment: "Account tab: 'Send DOGE tokens' button") } -class DogeWalletViewController: WalletViewControllerBase { +final class DogeWalletViewController: WalletViewControllerBase { // MARK: Lifecycle override func viewDidLoad() { diff --git a/Adamant/Modules/Wallets/ERC20/ERC20ApiService.swift b/Adamant/Modules/Wallets/ERC20/ERC20ApiService.swift new file mode 100644 index 000000000..dcfab8933 --- /dev/null +++ b/Adamant/Modules/Wallets/ERC20/ERC20ApiService.swift @@ -0,0 +1,25 @@ +// +// ERC20ApiService.swift +// Adamant +// +// Created by Andrew G on 13.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import web3swift +import Web3Core +import CommonKit + +final class ERC20ApiService: EthApiService { + func requestERC20( + token: ERC20Token, + _ body: @Sendable @escaping (ERC20) async throws -> Output + ) async -> WalletServiceResult { + let contractAddress = EthereumAddress(token.contractAddress) ?? .zero + + return await requestWeb3 { web3 in + let erc20 = ERC20(web3: web3, provider: web3.provider, address: contractAddress) + return try await body(erc20) + } + } +} diff --git a/Adamant/Wallets/ERC20/ERC20TransactionDetailsViewController.swift b/Adamant/Modules/Wallets/ERC20/ERC20TransactionDetailsViewController.swift similarity index 96% rename from Adamant/Wallets/ERC20/ERC20TransactionDetailsViewController.swift rename to Adamant/Modules/Wallets/ERC20/ERC20TransactionDetailsViewController.swift index 3bd8b6341..a9361c81b 100644 --- a/Adamant/Wallets/ERC20/ERC20TransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20TransactionDetailsViewController.swift @@ -9,7 +9,7 @@ import UIKit import CommonKit -class ERC20TransactionDetailsViewController: TransactionDetailsViewControllerBase { +final class ERC20TransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies weak var service: ERC20WalletService? diff --git a/Adamant/Modules/Wallets/ERC20/ERC20TransactionsViewController.swift b/Adamant/Modules/Wallets/ERC20/ERC20TransactionsViewController.swift new file mode 100644 index 000000000..1fc9f0e88 --- /dev/null +++ b/Adamant/Modules/Wallets/ERC20/ERC20TransactionsViewController.swift @@ -0,0 +1,50 @@ +// +// ERC20TransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 06/07/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit +import web3swift +import CommonKit + +final class ERC20TransactionsViewController: TransactionsListViewControllerBase { + + // MARK: - Dependencies + var ercWalletService: ERC20WalletService! { + didSet { + ethAddress = ercWalletService.wallet?.address ?? "" + } + } + var screensFactory: ScreensFactory! + + // MARK: - Properties + + private var ethAddress: String = "" + + // MARK: - UITableView + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let address = walletService.wallet?.address, + let transaction = transactions[safe: indexPath.row] + else { return } + + tableView.deselectRow(at: indexPath, animated: true) + + let vc = screensFactory.makeDetailsVC(service: ercWalletService) + + vc.transaction = transaction + + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { + vc.senderName = String.adamant.transactionDetails.yourAddress + } + + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { + vc.recipientName = String.adamant.transactionDetails.yourAddress + } + + navigationController?.pushViewController(vc, animated: true) + } +} diff --git a/Adamant/Wallets/ERC20/ERC20TransferViewController.swift b/Adamant/Modules/Wallets/ERC20/ERC20TransferViewController.swift similarity index 88% rename from Adamant/Wallets/ERC20/ERC20TransferViewController.swift rename to Adamant/Modules/Wallets/ERC20/ERC20TransferViewController.swift index a21f9a5ca..be309cf36 100644 --- a/Adamant/Wallets/ERC20/ERC20TransferViewController.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20TransferViewController.swift @@ -68,6 +68,10 @@ final class ERC20TransferViewController: TransferViewControllerBase { try await service.sendTransaction(transaction) } catch { dialogService.showRichError(error: error) + service.coinStorage.updateStatus( + for: txHash, + status: .failed + ) } await service.update() @@ -82,6 +86,7 @@ final class ERC20TransferViewController: TransferViewControllerBase { transaction: transaction, recipient: recipient, comments: comments, + amount: amount, service: service ) } catch { @@ -96,28 +101,37 @@ final class ERC20TransferViewController: TransferViewControllerBase { transaction: CodableTransaction, recipient: String, comments: String, + amount: Decimal, service: ERC20WalletService ) { let transaction = SimpleTransactionDetails( txId: hash, senderAddress: transaction.sender?.address ?? "", recipientAddress: recipient, - isOutgoing: true + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: true, + transactionStatus: nil ) - if let detailsVc = router.get(scene: AdamantScene.Wallets.ERC20.transactionDetails) as? ERC20TransactionDetailsViewController { - detailsVc.transaction = transaction - detailsVc.service = service - detailsVc.senderName = String.adamant.transactionDetails.yourAddress - detailsVc.recipientName = recipientName - - if comments.count > 0 { - detailsVc.comment = comments - } - - delegate?.transferViewController(self, didFinishWithTransfer: transaction, detailsViewController: detailsVc) - } else { - delegate?.transferViewController(self, didFinishWithTransfer: transaction, detailsViewController: nil) + + service.coinStorage.append(transaction) + + let detailsVc = screensFactory.makeDetailsVC(service: service) + detailsVc.transaction = transaction + detailsVc.senderName = String.adamant.transactionDetails.yourAddress + detailsVc.recipientName = recipientName + + if comments.count > 0 { + detailsVc.comment = comments } + + delegate?.transferViewController( + self, + didFinishWithTransfer: transaction, + detailsViewController: detailsVc + ) } // MARK: Overrides @@ -212,7 +226,7 @@ final class ERC20TransferViewController: TransferViewControllerBase { } override func defaultSceneTitle() -> String? { - let networkSymbol = service?.tokenNetworkSymbol ?? "ERC20" + let networkSymbol = ERC20WalletService.tokenNetworkSymbol return String.adamant.wallets.erc20.sendToken(service?.tokenSymbol ?? "ERC20") + " (\(networkSymbol))" } diff --git a/Adamant/Wallets/ERC20/ERC20Wallet.swift b/Adamant/Modules/Wallets/ERC20/ERC20Wallet.swift similarity index 94% rename from Adamant/Wallets/ERC20/ERC20Wallet.swift rename to Adamant/Modules/Wallets/ERC20/ERC20Wallet.swift index 0b594a9c5..d0eed70f5 100644 --- a/Adamant/Wallets/ERC20/ERC20Wallet.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20Wallet.swift @@ -10,7 +10,7 @@ import Foundation import web3swift import Web3Core -class ERC20Wallet: WalletAccount { +final class ERC20Wallet: WalletAccount { let address: String let ethAddress: EthereumAddress let keystore: BIP32Keystore diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift new file mode 100644 index 000000000..498fa885d --- /dev/null +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift @@ -0,0 +1,139 @@ +// +// ERC20WalletFactory.swift +// Adamant +// +// Created by Anton Boyarkin on 26/06/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Swinject +import CommonKit +import UIKit + +struct ERC20WalletFactory: WalletFactory { + typealias Service = ERC20WalletService + + let assembler: Assembler + + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { + let c = ERC20WalletViewController(nibName: "WalletViewControllerBase", bundle: nil) + c.dialogService = assembler.resolve(DialogService.self) + c.currencyInfoService = assembler.resolve(CurrencyInfoService.self) + c.accountService = assembler.resolve(AccountService.self) + c.service = service + c.screensFactory = screensFactory + return c + } + + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { + let vc = ERC20TransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) + vc.dialogService = assembler.resolve(DialogService.self) + vc.screensFactory = screensFactory + vc.walletService = service + vc.ercWalletService = service + return vc + } + + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { + let vc = ERC20TransferViewController( + chatsProvider: assembler.resolve(ChatsProvider.self)!, + accountService: assembler.resolve(AccountService.self)!, + accountsProvider: assembler.resolve(AccountsProvider.self)!, + dialogService: assembler.resolve(DialogService.self)!, + screensFactory: screensFactory, + currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, + vibroService: assembler.resolve(VibroService.self)! + ) + + vc.service = service + return vc + } + + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + else { return nil } + + let comment: String? + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { + comment = raw + } else { + comment = nil + } + + // MARK: Go to transaction + + return makeTransactionDetailsVC( + hash: hash, + senderId: transaction.senderId, + recipientId: transaction.recipientId, + senderAddress: "", + recipientAddress: "", + comment: comment, + transaction: nil, + richTransaction: transaction, + service: service + ) + } + + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { + makeTransactionDetailsVC(service: service) + } +} + +private extension ERC20WalletFactory { + func makeTransactionDetailsVC( + hash: String, + senderId: String?, + recipientId: String?, + senderAddress: String, + recipientAddress: String, + comment: String?, + transaction: EthTransaction?, + richTransaction: RichMessageTransaction, + service: Service + ) -> UIViewController { + let vc = makeTransactionDetailsVC(service: service) + + let amount: Decimal + if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), + let decimal = Decimal(string: amountRaw) { + amount = decimal + } else { + amount = 0 + } + + let failedTransaction = SimpleTransactionDetails( + txId: hash, + senderAddress: senderAddress, + recipientAddress: recipientAddress, + dateValue: nil, + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: richTransaction.isOutgoing, + transactionStatus: nil + ) + + vc.senderId = senderId + vc.recipientId = recipientId + vc.comment = comment + vc.transaction = transaction ?? failedTransaction + vc.richTransaction = richTransaction + return vc + } + + func makeTransactionDetailsVC(service: Service) -> ERC20TransactionDetailsViewController { + let vc = ERC20TransactionDetailsViewController( + dialogService: assembler.resolve(DialogService.self)!, + currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + addressBookService: assembler.resolve(AddressBookService.self)!, + accountService: assembler.resolve(AccountService.self)!, + walletService: service + ) + + vc.service = service + return vc + } +} diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift new file mode 100644 index 000000000..0325ae204 --- /dev/null +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift @@ -0,0 +1,60 @@ +// +// ERC20WalletService+RichMessageProvider.swift +// Adamant +// +// Created by Anton Boyarkin on 06/07/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation +import MessageKit +import UIKit +import CommonKit + +extension ERC20WalletService: RichMessageProvider { + var newPendingInterval: TimeInterval { + .init(milliseconds: EthWalletService.newPendingInterval) + } + + var oldPendingInterval: TimeInterval { + .init(milliseconds: EthWalletService.oldPendingInterval) + } + + var registeredInterval: TimeInterval { + .init(milliseconds: EthWalletService.registeredInterval) + } + + var newPendingAttempts: Int { + EthWalletService.newPendingAttempts + } + + var oldPendingAttempts: Int { + EthWalletService.oldPendingAttempts + } + + // MARK: Short description + + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { + let amount: String + + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { + return NSAttributedString(string: "⬅️ \(self.tokenSymbol)") + } + + if let decimal = Decimal(string: raw) { + amount = AdamantBalanceFormat.full.format(decimal) + } else { + amount = raw + } + + let string: String + if transaction.isOutgoing { + string = "⬅️ \(amount) \(self.tokenSymbol)" + } else { + string = "➡️ \(amount) \(self.tokenSymbol)" + } + + return NSAttributedString(string: string) + } +} diff --git a/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift similarity index 72% rename from Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift rename to Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift index d760f40bc..89e6cfdb6 100644 --- a/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift @@ -12,9 +12,16 @@ import struct BigInt.BigUInt import CommonKit extension ERC20WalletService: RichMessageProviderWithStatusCheck { - func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { - guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) - else { + func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { + let hash: String? + + if let transaction = transaction as? RichMessageTransaction { + hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + } else { + hash = transaction.txId + } + + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent) } @@ -23,12 +30,7 @@ extension ERC20WalletService: RichMessageProviderWithStatusCheck { do { erc20Transaction = try await getTransaction(by: hash) } catch { - switch error { - case WalletServiceError.networkError: - return .init(sentDate: nil, status: .noNetwork) - default: - return .init(sentDate: nil, status: .pending) - } + return .init(error: error) } return .init( @@ -44,7 +46,7 @@ extension ERC20WalletService: RichMessageProviderWithStatusCheck { private extension ERC20WalletService { func getStatus( erc20Transaction: EthTransaction, - transaction: RichMessageTransaction + transaction: CoinTransaction ) -> TransactionStatus { let status = erc20Transaction.receiptStatus.asTransactionStatus() guard status == .success else { return status } @@ -67,10 +69,7 @@ private extension ERC20WalletService { } // MARK: Compare amounts - guard - let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), - let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) - else { + guard let reportedValue = reportedValue(for: transaction) else { return .inconsistent } @@ -83,4 +82,20 @@ private extension ERC20WalletService { return .success } + + func reportedValue(for transaction: CoinTransaction) -> Decimal? { + guard let transaction = transaction as? RichMessageTransaction + else { + return transaction.amountValue + } + + guard + let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) + else { + return nil + } + + return reportedValue + } } diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletService+Send.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+Send.swift new file mode 100644 index 000000000..270aec647 --- /dev/null +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+Send.swift @@ -0,0 +1,73 @@ +// +// ERC20WalletService+Send.swift +// Adamant +// +// Created by Anton Boyarkin on 06/07/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit +import web3swift +import struct BigInt.BigUInt +import Web3Core +import CommonKit + +extension ERC20WalletService: WalletServiceTwoStepSend { + typealias T = CodableTransaction + + // MARK: Create & Send + func createTransaction(recipient: String, amount: Decimal) async throws -> CodableTransaction { + guard let ethWallet = ethWallet else { + throw WalletServiceError.notLogged + } + + guard let ethRecipient = EthereumAddress(recipient) else { + throw WalletServiceError.accountNotFound + } + + guard let keystoreManager = await erc20ApiService.keystoreManager else { + throw WalletServiceError.internalError(message: "Failed to get web3.provider.KeystoreManager", error: nil) + } + + let provider = try await erc20ApiService.requestWeb3 { web3 in web3.provider }.get() + let resolver = PolicyResolver(provider: provider) + + // MARK: Create transaction + + var tx = try await erc20ApiService.requestERC20(token: token) { erc20 in + try await erc20.transfer( + from: ethWallet.ethAddress, + to: ethRecipient, + amount: "\(amount)" + ).transaction + }.get() + + await calculateFee(for: ethRecipient) + + let policies: Policies = Policies( + gasLimitPolicy: .manual(gasLimit), + gasPricePolicy: .manual(gasPrice) + ) + + try await resolver.resolveAll(for: &tx, with: policies) + + try Web3Signer.signTX( + transaction: &tx, + keystore: keystoreManager, + account: ethWallet.ethAddress, + password: ERC20WalletService.walletPassword + ) + + return tx + } + + func sendTransaction(_ transaction: CodableTransaction) async throws { + guard let txEncoded = transaction.encode() else { + throw WalletServiceError.internalError(message: .adamant.sharedErrors.unknownError, error: nil) + } + + _ = try await erc20ApiService.requestWeb3 { web3 in + try await web3.eth.send(raw: txEncoded) + }.get() + } +} diff --git a/Adamant/Wallets/ERC20/ERC20WalletService.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift similarity index 53% rename from Adamant/Wallets/ERC20/ERC20WalletService.swift rename to Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift index d1b2cd1b2..c5fd96fc0 100644 --- a/Adamant/Wallets/ERC20/ERC20WalletService.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift @@ -16,7 +16,7 @@ import Web3Core import Combine import CommonKit -class ERC20WalletService: WalletService { +final class ERC20WalletService: WalletService { // MARK: - Constants let addressRegex = try! NSRegularExpression(pattern: "^0x[a-fA-F0-9]{40}$") @@ -28,18 +28,18 @@ class ERC20WalletService: WalletService { var minAmount: Decimal = 0 var tokenSymbol: String { - return token?.symbol ?? "" + return token.symbol } var tokenName: String { - return token?.name ?? "" + return token.name } var tokenLogo: UIImage { - return token?.logo ?? UIImage() + return token.logo } - var tokenNetworkSymbol: String { + static var tokenNetworkSymbol: String { return "ERC20" } @@ -48,19 +48,19 @@ class ERC20WalletService: WalletService { } var tokenContract: String { - return token?.contractAddress ?? "" + return token.contractAddress } var tokenUnicID: String { - return tokenNetworkSymbol + tokenSymbol + tokenContract + Self.tokenNetworkSymbol + tokenSymbol + tokenContract } var defaultVisibility: Bool { - return token?.defaultVisibility ?? false + return token.defaultVisibility } var defaultOrdinalLevel: Int? { - return token?.defaultOrdinalLevel + return token.defaultOrdinalLevel } var richMessageType: String { @@ -99,57 +99,32 @@ class ERC20WalletService: WalletService { // MARK: - Dependencies weak var accountService: AccountService? var apiService: ApiService! + var erc20ApiService: ERC20ApiService! var dialogService: DialogService! - var router: Router! var increaseFeeService: IncreaseFeeService! + var vibroService: VibroService! + var coreDataStack: CoreDataStack! // MARK: - Notifications - var walletUpdatedNotification = Notification.Name("adamant.erc20Wallet.walletUpdated") - var serviceEnabledChanged = Notification.Name("adamant.erc20Wallet.enabledChanged") - var transactionFeeUpdated = Notification.Name("adamant.erc20Wallet.feeUpdated") - var serviceStateChanged = Notification.Name("adamant.erc20Wallet.stateChanged") + let walletUpdatedNotification: Notification.Name + let serviceEnabledChanged: Notification.Name + let transactionFeeUpdated: Notification.Name + let serviceStateChanged: Notification.Name // MARK: RichMessageProvider properties static let richMessageType = "erc20_transaction" var dynamicRichMessageType: String { - return "\(self.token?.symbol.lowercased() ?? "erc20")_transaction" + return "\(self.token.symbol.lowercased())_transaction" } // MARK: - Properties - private (set) var token: ERC20Token? - private (set) var erc20: ERC20? - private (set) var enabled = true - - private var subscriptions = Set() - private var _ethNodeUrl: String? - private var _web3: Web3? - var web3: Web3? { - get async { - if _web3 != nil { - return _web3 - } - guard let url = _ethNodeUrl else { - return nil - } - - return await setupEthNode(with: url) - } - } - - var walletViewController: WalletViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.ERC20.wallet) as? ERC20WalletViewController else { - fatalError("Can't get erc20WalletViewController") - } - - vc.service = self - return vc - } - - private var initialBalanceCheck = false + let token: ERC20Token + @Atomic private(set) var enabled = true + @Atomic private var subscriptions = Set() // MARK: - State - private (set) var state: WalletServiceState = .notInitiated + @Atomic private (set) var state: WalletServiceState = .notInitiated private func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { @@ -159,39 +134,46 @@ class ERC20WalletService: WalletService { state = newState if !silent { - NotificationCenter.default.post(name: serviceStateChanged, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.walletState: state]) + NotificationCenter.default.post( + name: serviceStateChanged, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.walletState: state] + ) } } private (set) var ethWallet: EthWallet? var wallet: WalletAccount? { return ethWallet } - - private (set) var contract: Web3.Contract? private var balanceObserver: NSObjectProtocol? + @ObservableValue private(set) var historyTransactions: [TransactionDetails] = [] + @ObservableValue private(set) var hasMoreOldTransactions: Bool = true + + var transactionsPublisher: AnyObservable<[TransactionDetails]> { + $historyTransactions.eraseToAnyPublisher() + } + + var hasMoreOldTransactionsPublisher: AnyObservable { + $hasMoreOldTransactions.eraseToAnyPublisher() + } + + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( + coinId: tokenUnicID, + coreDataStack: coreDataStack, + blockchainType: dynamicRichMessageType + ) + init(token: ERC20Token) { self.token = token - - self.setState(.notInitiated) - walletUpdatedNotification = Notification.Name("adamant.erc20Wallet.\(token.symbol).walletUpdated") serviceEnabledChanged = Notification.Name("adamant.erc20Wallet.\(token.symbol).enabledChanged") transactionFeeUpdated = Notification.Name("adamant.erc20Wallet.\(token.symbol).feeUpdated") serviceStateChanged = Notification.Name("adamant.erc20Wallet.\(token.symbol).stateChanged") + self.setState(.notInitiated) + // Notifications addObservers() - - guard let node = EthWalletService.nodes.randomElement() else { - fatalError("Failed to get ETH endpoint") - } - let apiUrl = node.asString() - _ethNodeUrl = apiUrl - Task { - _ = await self.setupEthNode(with: apiUrl) - } } func addObservers() { @@ -216,32 +198,23 @@ class ERC20WalletService: WalletService { .receive(on: OperationQueue.main) .sink { [weak self] _ in self?.ethWallet = nil - self?.initialBalanceCheck = false if let balanceObserver = self?.balanceObserver { NotificationCenter.default.removeObserver(balanceObserver) self?.balanceObserver = nil } + self?.coinStorage.clear() + self?.hasMoreOldTransactions = true + self?.historyTransactions = [] } .store(in: &subscriptions) } - func setupEthNode(with apiUrl: String) async -> Web3? { - guard - let url = URL(string: apiUrl), - let web3 = try? await Web3.new(url), - let token = self.token else { - return nil - } - - self._web3 = web3 - - if let address = EthereumAddress(token.contractAddress) { - self.contract = web3.contract(Web3.Utils.erc20ABI, at: address, abiVersion: 2) - - self.erc20 = ERC20(web3: web3, provider: web3.provider, address: address) - } - - return web3 + func addTransactionObserver() { + coinStorage.transactionsPublisher + .sink { [weak self] transactions in + self?.historyTransactions = transactions + } + .store(in: &subscriptions) } func update() { @@ -266,20 +239,24 @@ class ERC20WalletService: WalletService { setState(.updating) if let balance = try? await getBalance(forAddress: wallet.ethAddress) { - wallet.isBalanceInitialized = true let notification: Notification.Name? + let isRaised = (wallet.balance < balance) && wallet.isBalanceInitialized if wallet.balance != balance { wallet.balance = balance notification = walletUpdatedNotification - initialBalanceCheck = false - } else if initialBalanceCheck { - initialBalanceCheck = false + } else if !wallet.isBalanceInitialized { notification = walletUpdatedNotification } else { notification = nil } + wallet.isBalanceInitialized = true + + if isRaised { + vibroService.applyVibration(.success) + } + if let notification = notification { NotificationCenter.default.post(name: notification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet]) } @@ -291,8 +268,6 @@ class ERC20WalletService: WalletService { } func calculateFee(for address: EthereumAddress? = nil) async { - guard let token = token else { return } - let priceRaw = try? await getGasPrices() let gasLimitRaw = try? await getGasLimit(to: address) @@ -336,52 +311,27 @@ class ERC20WalletService: WalletService { } func getGasPrices() async throws -> BigUInt { - guard let web3 = await self.web3 else { - throw WalletServiceError.internalError(message: "Can't get web3 service", error: nil) - } - - do { - let price = try await web3.eth.gasPrice() - return price - } catch { - throw WalletServiceError.remoteServiceError( - message: error.localizedDescription - ) - } + try await erc20ApiService.requestWeb3 { web3 in + try await web3.eth.gasPrice() + }.get() } func getGasLimit(to address: EthereumAddress?) async throws -> BigUInt { - guard let web3 = await self.web3, - let ethWallet = ethWallet, - let erc20 = erc20 - else { - throw WalletServiceError.internalError(message: "Can't get web3 service", error: nil) + guard let ethWallet = ethWallet else { + throw WalletServiceError.internalError(message: "Can't get ethWallet service", error: nil) } - do { - let transaction = try await erc20.transfer( + let transaction = try await erc20ApiService.requestERC20(token: token) { erc20 in + try await erc20.transfer( from: ethWallet.ethAddress, to: address ?? ethWallet.ethAddress, amount: "\(ethWallet.balance)" ).transaction - - let price = try await web3.eth.estimateGas(for: transaction) - return price - } catch { - throw WalletServiceError.remoteServiceError( - message: error.localizedDescription - ) - } - } - - private func buildUrl(url: URL, queryItems: [URLQueryItem]? = nil) throws -> URL { - guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - throw AdamantApiService.InternalError.endpointBuildFailed - } + }.get() - components.queryItems = queryItems - - return try components.asURL() + return try await erc20ApiService.requestWeb3 { web3 in + try await web3.eth.estimateGas(for: transaction) + }.get() } } @@ -409,7 +359,7 @@ extension ERC20WalletService: InitiatedWithPassphraseService { throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: error) } - await web3?.addKeystoreManager(KeystoreManager([keystore])) + await erc20ApiService.setKeystoreManager(.init([keystore])) guard let ethAddress = keystore.addresses?.first else { throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: nil) @@ -424,7 +374,12 @@ extension ERC20WalletService: InitiatedWithPassphraseService { NotificationCenter.default.post(name: serviceEnabledChanged, object: self) } - self.initialBalanceCheck = true + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: eWallet] + ) + self.setState(.upToDate, silent: true) Task { await update() @@ -445,8 +400,12 @@ extension ERC20WalletService: SwinjectDependentService { accountService = container.resolve(AccountService.self) apiService = container.resolve(ApiService.self) dialogService = container.resolve(DialogService.self) - router = container.resolve(Router.self) increaseFeeService = container.resolve(IncreaseFeeService.self) + erc20ApiService = container.resolve(ERC20ApiService.self) + vibroService = container.resolve(VibroService.self) + coreDataStack = container.resolve(CoreDataStack.self) + + addTransactionObserver() } } @@ -454,99 +413,71 @@ extension ERC20WalletService: SwinjectDependentService { extension ERC20WalletService { func getTransaction(by hash: String) async throws -> EthTransaction { let sender = wallet?.address - guard let eth = await web3?.eth else { - throw WalletServiceError.internalError(message: "Failed to get transaction", error: nil) - } - let isOutgoing: Bool - let details: Web3Core.TransactionDetails // MARK: 1. Transaction details - do { - details = try await eth.transactionDetails(hash) - } catch let error as Web3Error { - throw error.asWalletServiceError() - } catch _ as URLError { - throw WalletServiceError.networkError - } catch { - throw WalletServiceError.remoteServiceError(message: "Failed to get transaction") - } - - // MARK: 2. Transaction receipt - do { - let receipt = try await eth.transactionReceipt(hash) - - // MARK: 3. Check if transaction is delivered - guard receipt.status == .ok, - let blockNumber = details.blockNumber - else { - let transaction = details.transaction.asEthTransaction( - date: nil, - gasUsed: receipt.gasUsed, - gasPrice: receipt.effectiveGasPrice, - blockNumber: nil, - confirmations: nil, - receiptStatus: receipt.status, - isOutgoing: false - ) - return transaction - } - - // MARK: 4. Block timestamp & confirmations - let currentBlock = try await eth.blockNumber() - let block = try await eth.block(by: receipt.blockHash) - - guard currentBlock >= blockNumber else { - throw WalletServiceError.remoteServiceError( - message: "ERC20 confirmations calculating error" - ) - } - - let confirmations = currentBlock - blockNumber - - let transaction = details.transaction - - if let sender = sender { - isOutgoing = transaction.sender?.address == sender - } else { - isOutgoing = false - } - - let ethTransaction = transaction.asEthTransaction( - date: block.timestamp, + let details: Web3Core.TransactionDetails = try await erc20ApiService.requestWeb3 { + web3 in + try await web3.eth.transactionDetails(hash) + }.get() + + let receipt = try await erc20ApiService.requestWeb3 { web3 in + try await web3.eth.transactionReceipt(hash) + }.get() + + // MARK: 3. Check if transaction is delivered + guard receipt.status == .ok, + let blockNumber = details.blockNumber + else { + let transaction = details.transaction.asEthTransaction( + date: nil, gasUsed: receipt.gasUsed, gasPrice: receipt.effectiveGasPrice, - blockNumber: String(blockNumber), - confirmations: String(confirmations), + blockNumber: nil, + confirmations: nil, receiptStatus: receipt.status, - isOutgoing: isOutgoing, - for: self.token + isOutgoing: false ) - - return ethTransaction - } catch let error as Web3Error { - switch error { - // Transaction not delivered yet - case .inputError, .nodeError: - let transaction = details.transaction.asEthTransaction( - date: nil, - gasUsed: nil, - gasPrice: nil, - blockNumber: nil, - confirmations: nil, - receiptStatus: TransactionReceipt.TXStatus.notYetProcessed, - isOutgoing: false - ) - return transaction - - default: - throw error.asWalletServiceError() - } - } catch _ as URLError { - throw WalletServiceError.networkError - } catch { - throw error + return transaction } + + // MARK: 4. Block timestamp & confirmations + let currentBlock = try await erc20ApiService.requestWeb3 { web3 in + try await web3.eth.blockNumber() + }.get() + + let block = try await erc20ApiService.requestWeb3 { web3 in + try await web3.eth.block(by: receipt.blockHash) + }.get() + + guard currentBlock >= blockNumber else { + throw WalletServiceError.remoteServiceError( + message: "ERC20 confirmations calculating error" + ) + } + + let confirmations = currentBlock - blockNumber + + let transaction = details.transaction + + if let sender = sender { + isOutgoing = transaction.sender?.address == sender + } else { + isOutgoing = false + } + + let ethTransaction = transaction.asEthTransaction( + date: block.timestamp, + gasUsed: receipt.gasUsed, + gasPrice: receipt.effectiveGasPrice, + blockNumber: String(blockNumber), + confirmations: String(confirmations), + receiptStatus: receipt.status, + isOutgoing: isOutgoing, + for: self.token + ) + + return ethTransaction } func getBalance(address: String) async throws -> Decimal { @@ -558,86 +489,108 @@ extension ERC20WalletService { } func getBalance(forAddress address: EthereumAddress) async throws -> Decimal { - guard let erc20 = self.erc20 else { - throw WalletServiceError.internalError(message: "Can't get address", error: nil) - } + let exponent = -token.naturalUnits - var exponent = EthWalletService.currencyExponent - if let naturalUnits = self.token?.naturalUnits { - exponent = -1 * naturalUnits - } + let balance = try await erc20ApiService.requestERC20(token: token) { erc20 in + try await erc20.getBalance(account: address) + }.get() - do { - let balance = try await erc20.getBalance(account: address) - let value = balance.asDecimal(exponent: exponent) - return value - } catch { - throw WalletServiceError.remoteServiceError( - message: "ERC 20 Service - Fail to get balance" - ) - } + let value = balance.asDecimal(exponent: exponent) + return value } func getWalletAddress(byAdamantAddress address: String) async throws -> String { - do { - let result = try await apiService.get(key: EthWalletService.kvsAddress, sender: address) - - guard let result = result else { - throw WalletServiceError.walletNotInitiated - } - - return result - } catch let error as ApiServiceError { - throw WalletServiceError.remoteServiceError( - message: "ETH Wallet: failed to get address from KVS" - ) + let result = try await apiService.get(key: EthWalletService.kvsAddress, sender: address) + .mapError { $0.asWalletServiceError() } + .get() + + guard let result = result else { + throw WalletServiceError.walletNotInitiated } + + return result } } extension ERC20WalletService { - func getTransactionsHistory(address: String, offset: Int = 0, limit: Int = 100) async throws -> [EthTransactionShort] { - guard let node = EthWalletService.nodes.randomElement(), let url = node.asURL() else { - fatalError("Failed to build ETH endpoint URL") - } - - guard let address = self.ethWallet?.address, let contract = self.token?.contractAddress else { + func getTransactionsHistory( + address: String, + offset: Int = .zero, + limit: Int = 100 + ) async throws -> [EthTransactionShort] { + guard let address = self.ethWallet?.address else { throw WalletServiceError.internalError(message: "Can't get address", error: nil) } // Request - let request = "(txto.eq.\(contract),or(txfrom.eq.\(address.lowercased()),contract_to.eq.000000000000000000000000\(address.lowercased().replacingOccurrences(of: "0x", with: ""))))" + let request = "(txto.eq.\(token.contractAddress),or(txfrom.eq.\(address.lowercased()),contract_to.eq.000000000000000000000000\(address.lowercased().replacingOccurrences(of: "0x", with: ""))))" // MARK: Request - let txQueryItems: [URLQueryItem] = [URLQueryItem(name: "limit", value: String(limit)), - URLQueryItem(name: "and", value: request), - URLQueryItem(name: "offset", value: String(offset)), - URLQueryItem(name: "order", value: "time.desc") + let txQueryParameters = [ + "limit": String(limit), + "and": request, + "offset": String(offset), + "order": "time.desc" ] - let txEndpoint: URL - do { - txEndpoint = try buildUrl(url: url.appendingPathComponent(EthWalletService.transactionsListApiSubpath), queryItems: txQueryItems) - } catch { - let err = AdamantApiService.InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) - throw WalletServiceError.apiError(err) - } - - // MARK: Sending requests + var transactions: [EthTransactionShort] = try await erc20ApiService.requestApiCore { core, node in + await core.sendRequestJsonResponse( + node: node, + path: EthWalletService.transactionsListApiSubpath, + method: .get, + parameters: txQueryParameters, + encoding: .url + ) + }.get() - var transactions: [EthTransactionShort] = try await apiService.sendRequest(url: txEndpoint, method: .get, parameters: nil) transactions.sort { $0.date.compare($1.date) == .orderedDescending } return transactions } -} - -extension ERC20WalletService: WalletServiceWithTransfers { - func transferListViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.ERC20.transactionsList) as? ERC20TransactionsViewController else { - fatalError("Can't get ERC20TransactionsViewController") + + func loadTransactions(offset: Int, limit: Int) async throws -> Int { + guard let address = wallet?.address else { + return .zero } - vc.walletService = self - return vc + let trs = try await getTransactionsHistory( + address: address, + offset: offset, + limit: limit + ) + + guard trs.count > 0 else { + hasMoreOldTransactions = false + return .zero + } + + let newTrs = trs.map { transaction in + let isOutgoing = transaction.from == address + let exponent = -token.naturalUnits + + return SimpleTransactionDetails( + txId: transaction.hash, + senderAddress: transaction.from, + recipientAddress: transaction.to, + dateValue: transaction.date, + amountValue: transaction.contract_value.asDecimal(exponent: exponent), + feeValue: transaction.gasUsed * transaction.gasPrice, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: isOutgoing, + transactionStatus: TransactionStatus.notInitiated + ) + } + + coinStorage.append(newTrs) + + return trs.count + } + + func getLocalTransactionHistory() -> [TransactionDetails] { + historyTransactions + } + + func updateStatus(for id: String, status: TransactionStatus?) { + coinStorage.updateStatus(for: id, status: status) } } diff --git a/Adamant/Wallets/ERC20/ERC20WalletViewController.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletViewController.swift similarity index 93% rename from Adamant/Wallets/ERC20/ERC20WalletViewController.swift rename to Adamant/Modules/Wallets/ERC20/ERC20WalletViewController.swift index f01ffe177..019ca4d4b 100644 --- a/Adamant/Wallets/ERC20/ERC20WalletViewController.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletViewController.swift @@ -22,7 +22,7 @@ extension String.adamant.wallets { } } -class ERC20WalletViewController: WalletViewControllerBase { +final class ERC20WalletViewController: WalletViewControllerBase { // MARK: Lifecycle override func viewDidLoad() { @@ -32,7 +32,7 @@ class ERC20WalletViewController: WalletViewControllerBase { } override func sendRowLocalizedLabel() -> NSAttributedString { - let networkSymbol = service?.tokenNetworkSymbol ?? "ERC20" + let networkSymbol = ERC20WalletService.tokenNetworkSymbol let tokenSymbol = String.adamant.wallets.erc20.sendToken(service?.tokenSymbol ?? "") let currencyFont = UIFont.systemFont(ofSize: 17) let networkFont = currencyFont.withSize(8) diff --git a/Adamant/Modules/Wallets/Ethereum/EthApiCore.swift b/Adamant/Modules/Wallets/Ethereum/EthApiCore.swift new file mode 100644 index 000000000..aa59f1e7e --- /dev/null +++ b/Adamant/Modules/Wallets/Ethereum/EthApiCore.swift @@ -0,0 +1,99 @@ +// +// EthApiCore.swift +// Adamant +// +// Created by Andrew G on 30.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import Foundation +import web3swift +import Web3Core + +actor EthApiCore { + let apiCore: APICoreProtocol + private(set) var keystoreManager: KeystoreManager? + private var web3Cache: [URL: Web3] = .init() + + func performRequest( + node: Node, + _ body: @escaping @Sendable (_ web3: Web3) async throws -> Success + ) async -> WalletServiceResult { + switch await getWeb3(node: node) { + case let .success(web3): + do { + return .success(try await body(web3)) + } catch { + return .failure(mapError(error)) + } + case let .failure(error): + return .failure(error) + } + } + + func setKeystoreManager(_ keystoreManager: KeystoreManager) { + self.keystoreManager = keystoreManager + web3Cache = .init() + } + + init(apiCore: APICoreProtocol) { + self.apiCore = apiCore + } +} + +extension EthApiCore: BlockchainHealthCheckableService { + func getStatusInfo(node: Node) async -> WalletServiceResult { + await performRequest(node: node) { web3 in + let startTimestamp = Date.now.timeIntervalSince1970 + let height = try await web3.eth.blockNumber() + let ping = Date.now.timeIntervalSince1970 - startTimestamp + + return .init( + ping: ping, + height: Int(height.asDouble()), + wsEnabled: false, + wsPort: nil, + version: nil + ) + } + } +} + +private extension EthApiCore { + func getWeb3(node: Node) async -> WalletServiceResult { + guard let url = node.asURL() else { + return .failure(.internalError(.endpointBuildFailed)) + } + + if let web3 = web3Cache[url] { + return .success(web3) + } + + do { + let web3 = try await Web3.new(url) + web3.addKeystoreManager(keystoreManager) + web3Cache[url] = web3 + return .success(web3) + } catch { + return .failure(.internalError( + message: error.localizedDescription, + error: error + )) + } + } +} + +private func mapError(_ error: Error) -> WalletServiceError { + if let error = error as? Web3Error { + return error.asWalletServiceError() + } else if let error = error as? ApiServiceError { + return error.asWalletServiceError() + } else if let error = error as? WalletServiceError { + return error + } else if let _ = error as? URLError { + return .networkError + } else { + return .remoteServiceError(message: error.localizedDescription) + } +} diff --git a/Adamant/Modules/Wallets/Ethereum/EthApiService.swift b/Adamant/Modules/Wallets/Ethereum/EthApiService.swift new file mode 100644 index 000000000..701bad3d2 --- /dev/null +++ b/Adamant/Modules/Wallets/Ethereum/EthApiService.swift @@ -0,0 +1,58 @@ +// +// EthApiService.swift +// Adamant +// +// Created by Andrew G on 13.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import Foundation +import web3swift +import Web3Core + +class EthApiService: WalletApiService { + let api: BlockchainHealthCheckWrapper + + var keystoreManager: KeystoreManager? { + get async { await api.service.keystoreManager } + } + + var preferredNodeIds: [UUID] { + api.preferredNodeIds + } + + init(api: BlockchainHealthCheckWrapper) { + self.api = api + } + + func healthCheck() { + api.healthCheck() + } + + func requestWeb3( + _ request: @Sendable @escaping (Web3) async throws -> Output + ) async -> WalletServiceResult { + await api.request { core, node in + await core.performRequest(node: node, request) + } + } + + func requestApiCore( + _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + ) async -> WalletServiceResult { + await api.request { core, node in + await request(core.apiCore, node).mapError { $0.asWalletServiceError() } + } + } + + func getStatusInfo() async -> WalletServiceResult { + await api.request { core, node in + await core.getStatusInfo(node: node) + } + } + + func setKeystoreManager(_ keystoreManager: KeystoreManager) async { + await api.service.setKeystoreManager(keystoreManager) + } +} diff --git a/Adamant/Wallets/Ethereum/EthTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Ethereum/EthTransactionDetailsViewController.swift similarity index 96% rename from Adamant/Wallets/Ethereum/EthTransactionDetailsViewController.swift rename to Adamant/Modules/Wallets/Ethereum/EthTransactionDetailsViewController.swift index 603fc89b2..b9a863fc0 100644 --- a/Adamant/Wallets/Ethereum/EthTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthTransactionDetailsViewController.swift @@ -9,7 +9,7 @@ import UIKit import CommonKit -class EthTransactionDetailsViewController: TransactionDetailsViewControllerBase { +final class EthTransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies weak var service: EthWalletService? diff --git a/Adamant/Modules/Wallets/Ethereum/EthTransactionsViewController.swift b/Adamant/Modules/Wallets/Ethereum/EthTransactionsViewController.swift new file mode 100644 index 000000000..748351941 --- /dev/null +++ b/Adamant/Modules/Wallets/Ethereum/EthTransactionsViewController.swift @@ -0,0 +1,48 @@ +// +// EthTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 25/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import web3swift +import CommonKit + +final class EthTransactionsViewController: TransactionsListViewControllerBase { + + // MARK: - Dependencies + var ethWalletService: EthWalletService! { + didSet { + ethAddress = ethWalletService.wallet?.address ?? "" + } + } + var screensFactory: ScreensFactory! + + // MARK: - Properties + private var ethAddress: String = "" + + // MARK: - UITableView + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let address = ethWalletService.wallet?.address, + let transaction = transactions[safe: indexPath.row] + else { return } + + tableView.deselectRow(at: indexPath, animated: true) + let vc = screensFactory.makeDetailsVC(service: ethWalletService) + + vc.transaction = transaction + + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { + vc.senderName = String.adamant.transactionDetails.yourAddress + } + + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { + vc.recipientName = String.adamant.transactionDetails.yourAddress + } + + navigationController?.pushViewController(vc, animated: true) + } +} diff --git a/Adamant/Wallets/Ethereum/EthTransferViewController.swift b/Adamant/Modules/Wallets/Ethereum/EthTransferViewController.swift similarity index 87% rename from Adamant/Wallets/Ethereum/EthTransferViewController.swift rename to Adamant/Modules/Wallets/Ethereum/EthTransferViewController.swift index 95514772a..9bae31980 100644 --- a/Adamant/Wallets/Ethereum/EthTransferViewController.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthTransferViewController.swift @@ -62,6 +62,10 @@ final class EthTransferViewController: TransferViewControllerBase { try await service.sendTransaction(transaction) } catch { dialogService.showRichError(error: error) + service.coinStorage.updateStatus( + for: txHash, + status: .failed + ) } await service.update() @@ -76,6 +80,7 @@ final class EthTransferViewController: TransferViewControllerBase { transaction: transaction, recipient: recipient, comments: comments, + amount: amount, service: service ) } catch { @@ -90,28 +95,36 @@ final class EthTransferViewController: TransferViewControllerBase { transaction: CodableTransaction, recipient: String, comments: String, + amount: Decimal, service: EthWalletService ) { let transaction = SimpleTransactionDetails( txId: hash, senderAddress: transaction.sender?.address ?? "", recipientAddress: recipient, - isOutgoing: true + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: true, + transactionStatus: nil ) - if let detailsVc = router.get(scene: AdamantScene.Wallets.Ethereum.transactionDetails) as? EthTransactionDetailsViewController { - detailsVc.transaction = transaction - detailsVc.service = service - detailsVc.senderName = String.adamant.transactionDetails.yourAddress - detailsVc.recipientName = recipientName - - if comments.count > 0 { - detailsVc.comment = comments - } - - delegate?.transferViewController(self, didFinishWithTransfer: transaction, detailsViewController: detailsVc) - } else { - delegate?.transferViewController(self, didFinishWithTransfer: transaction, detailsViewController: nil) + + service.coinStorage.append(transaction) + let detailsVc = screensFactory.makeDetailsVC(service: service) + detailsVc.transaction = transaction + detailsVc.senderName = String.adamant.transactionDetails.yourAddress + detailsVc.recipientName = recipientName + + if comments.count > 0 { + detailsVc.comment = comments } + + delegate?.transferViewController( + self, + didFinishWithTransfer: transaction, + detailsViewController: detailsVc + ) } // MARK: Overrides diff --git a/Adamant/Wallets/Ethereum/EthWallet.swift b/Adamant/Modules/Wallets/Ethereum/EthWallet.swift similarity index 94% rename from Adamant/Wallets/Ethereum/EthWallet.swift rename to Adamant/Modules/Wallets/Ethereum/EthWallet.swift index 56a01fabb..d279d07fc 100644 --- a/Adamant/Wallets/Ethereum/EthWallet.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWallet.swift @@ -10,7 +10,7 @@ import Foundation import web3swift import Web3Core -class EthWallet: WalletAccount { +final class EthWallet: WalletAccount { let address: String let ethAddress: EthereumAddress let keystore: BIP32Keystore diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift new file mode 100644 index 000000000..e60e6771b --- /dev/null +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift @@ -0,0 +1,137 @@ +// +// EthWalletFactory.swift +// Adamant +// +// Created by Anokhov Pavel on 28.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Swinject +import UIKit +import CommonKit + +struct EthWalletFactory: WalletFactory { + typealias Service = EthWalletService + + let assembler: Assembler + + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { + let c = EthWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) + c.dialogService = assembler.resolve(DialogService.self) + c.currencyInfoService = assembler.resolve(CurrencyInfoService.self) + c.accountService = assembler.resolve(AccountService.self) + c.service = service + c.screensFactory = screensFactory + return c + } + + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { + let c = EthTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) + c.dialogService = assembler.resolve(DialogService.self) + c.ethWalletService = service + c.screensFactory = screensFactory + c.walletService = service + return c + } + + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { + let vc = EthTransferViewController( + chatsProvider: assembler.resolve(ChatsProvider.self)!, + accountService: assembler.resolve(AccountService.self)!, + accountsProvider: assembler.resolve(AccountsProvider.self)!, + dialogService: assembler.resolve(DialogService.self)!, + screensFactory: screensFactory, + currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, + vibroService: assembler.resolve(VibroService.self)! + ) + + vc.service = service + return vc + } + + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + else { return nil } + + let comment: String? + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { + comment = raw + } else { + comment = nil + } + + return makeTransactionDetailsVC( + hash: hash, + senderId: transaction.senderId, + recipientId: transaction.recipientId, + senderAddress: "", + recipientAddress: "", + comment: comment, + transaction: nil, + richTransaction: transaction, + service: service + ) + } + + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { + makeTransactionDetailsVC(service: service) + } +} + +private extension EthWalletFactory { + func makeTransactionDetailsVC( + hash: String, + senderId: String?, + recipientId: String?, + senderAddress: String, + recipientAddress: String, + comment: String?, + transaction: EthTransaction?, + richTransaction: RichMessageTransaction, + service: Service + ) -> UIViewController { + let vc = makeTransactionDetailsVC(service: service) + + let amount: Decimal + if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), + let decimal = Decimal(string: amountRaw) { + amount = decimal + } else { + amount = 0 + } + + let failedTransaction = SimpleTransactionDetails( + txId: hash, + senderAddress: senderAddress, + recipientAddress: recipientAddress, + dateValue: nil, + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: richTransaction.isOutgoing, + transactionStatus: nil + ) + + vc.senderId = senderId + vc.recipientId = recipientId + vc.comment = comment + vc.transaction = transaction ?? failedTransaction + vc.richTransaction = richTransaction + return vc + } + + func makeTransactionDetailsVC(service: Service) -> EthTransactionDetailsViewController { + let vc = EthTransactionDetailsViewController( + dialogService: assembler.resolve(DialogService.self)!, + currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + addressBookService: assembler.resolve(AddressBookService.self)!, + accountService: assembler.resolve(AccountService.self)!, + walletService: service + ) + + vc.service = service + return vc + } +} diff --git a/Adamant/Wallets/Ethereum/EthWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService+DynamicConstants.swift similarity index 87% rename from Adamant/Wallets/Ethereum/EthWalletService+DynamicConstants.swift rename to Adamant/Modules/Wallets/Ethereum/EthWalletService+DynamicConstants.swift index 154d74a48..184451ee2 100644 --- a/Adamant/Wallets/Ethereum/EthWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService+DynamicConstants.swift @@ -73,12 +73,16 @@ extension EthWalletService { 20 } + var minNodeVersion: String? { + nil + } + static let explorerAddress = "https://etherscan.io/tx/" static var nodes: [Node] { [ - Node(url: URL(string: "https://ethnode1.adamant.im")!), -Node(url: URL(string: "https://ethnode2.adamant.im")!), + Node(url: URL(string: "https://ethnode1.adamant.im")!, altUrl: URL(string: "http://95.216.41.106:44099")), +Node(url: URL(string: "https://ethnode2.adamant.im")!, altUrl: URL(string: "http://95.216.114.252:44099")), ] } diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift new file mode 100644 index 000000000..79ab8f426 --- /dev/null +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift @@ -0,0 +1,64 @@ +// +// EthWalletService+RichMessageProvider.swift +// Adamant +// +// Created by Anokhov Pavel on 08.09.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import MessageKit +import UIKit +import CommonKit + +extension EthWalletService: RichMessageProvider { + var newPendingInterval: TimeInterval { + .init(milliseconds: type(of: self).newPendingInterval) + } + + var oldPendingInterval: TimeInterval { + .init(milliseconds: type(of: self).oldPendingInterval) + } + + var registeredInterval: TimeInterval { + .init(milliseconds: type(of: self).registeredInterval) + } + + var newPendingAttempts: Int { + type(of: self).newPendingAttempts + } + + var oldPendingAttempts: Int { + type(of: self).oldPendingAttempts + } + + var dynamicRichMessageType: String { + return type(of: self).richMessageType + } + + // MARK: Short description + + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { + let amount: String + + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { + return NSAttributedString(string: "⬅️ \(EthWalletService.currencySymbol)") + } + + if let decimal = Decimal(string: raw) { + amount = AdamantBalanceFormat.full.format(decimal) + } else { + amount = raw + } + + let string: String + if transaction.isOutgoing { + string = "⬅️ \(amount) \(EthWalletService.currencySymbol)" + } else { + string = "➡️ \(amount) \(EthWalletService.currencySymbol)" + } + + return NSAttributedString(string: string) + } +} diff --git a/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift similarity index 66% rename from Adamant/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift rename to Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift index ef51352f7..64c08e907 100644 --- a/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift @@ -12,22 +12,28 @@ import Web3Core import CommonKit extension EthWalletService: RichMessageProviderWithStatusCheck { - func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { - guard - let web3 = await web3, - let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) - else { + func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { + let hash: String? + + if let transaction = transaction as? RichMessageTransaction { + hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + } else { + hash = transaction.txId + } + + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent) } let transactionInfo: EthTransactionInfo do { - transactionInfo = try await getTransactionInfo(hash: hash, web3: web3) - } catch _ as URLError { - return .init(sentDate: nil, status: .noNetwork) + transactionInfo = try await ethApiService.requestWeb3 { [weak self] web3 in + guard let self = self else { throw WalletServiceError.internalError(.unknownError) } + return try await getTransactionInfo(hash: hash, web3: web3) + }.get() } catch { - return .init(sentDate: nil, status: .pending) + return .init(error: error) } guard @@ -39,7 +45,9 @@ extension EthWalletService: RichMessageProviderWithStatusCheck { var sentDate: Date? if let blockHash = details.blockHash { - sentDate = try? await web3.eth.block(by: blockHash).timestamp + sentDate = try? await ethApiService.requestWeb3 { web3 in + try await web3.eth.block(by: blockHash).timestamp + }.get() } return .init( @@ -63,7 +71,7 @@ private extension EthWalletService { func getTransactionInfo(hash: String, web3: Web3) async throws -> EthTransactionInfo { try await withThrowingTaskGroup( of: EthTransactionInfoElement.self, - returning: EthTransactionInfo.self + returning: Atomic.self ) { group in group.addTask(priority: .userInitiated) { .details(try await web3.eth.transactionDetails(hash)) @@ -73,20 +81,22 @@ private extension EthWalletService { .receipt(try await web3.eth.transactionReceipt(hash)) } - return try await group.reduce(into: .init()) { result, value in + return try await group.reduce( + into: .init(wrappedValue: .init()) + ) { result, value in switch value { case let .receipt(receipt): - result.receipt = receipt + result.wrappedValue.receipt = receipt case let .details(details): - result.details = details + result.wrappedValue.details = details } } - } + }.wrappedValue } func getStatus( details: Web3Core.TransactionDetails, - transaction: RichMessageTransaction, + transaction: CoinTransaction, receipt: TransactionReceipt ) -> TransactionStatus { let status = receipt.status.asTransactionStatus() @@ -113,8 +123,7 @@ private extension EthWalletService { // MARK: Compare amounts let realAmount = eth.value.asDecimal(exponent: EthWalletService.currencyExponent) - guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), - let reported = AdamantBalanceFormat.deserializeBalance(from: raw) else { + guard let reported = reportedValue(for: transaction) else { return .inconsistent } let min = reported - reported*0.005 @@ -126,6 +135,22 @@ private extension EthWalletService { return .success } + + func reportedValue(for transaction: CoinTransaction) -> Decimal? { + guard let transaction = transaction as? RichMessageTransaction + else { + return transaction.amountValue + } + + guard + let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) + else { + return nil + } + + return reportedValue + } } extension TransactionReceipt.TXStatus { diff --git a/Adamant/Wallets/Ethereum/EthWalletService+Send.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService+Send.swift similarity index 78% rename from Adamant/Wallets/Ethereum/EthWalletService+Send.swift rename to Adamant/Modules/Wallets/Ethereum/EthWalletService+Send.swift index 03c226a6f..f843009cd 100644 --- a/Adamant/Wallets/Ethereum/EthWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService+Send.swift @@ -21,9 +21,20 @@ extension CodableTransaction: RawTransaction { extension EthWalletService: WalletServiceTwoStepSend { typealias T = CodableTransaction + + func createTransaction(recipient: String, amount: Decimal) async throws -> CodableTransaction { + try await ethApiService.requestWeb3 { [weak self] web3 in + guard let self = self else { throw WalletServiceError.internalError(.unknownError) } + return try await createTransaction(recipient: recipient, amount: amount, web3: web3) + }.get() + } // MARK: Create & Send - func createTransaction(recipient: String, amount: Decimal) async throws -> CodableTransaction { + private func createTransaction( + recipient: String, + amount: Decimal, + web3: Web3 + ) async throws -> CodableTransaction { guard let ethWallet = ethWallet else { throw WalletServiceError.notLogged } @@ -36,10 +47,6 @@ extension EthWalletService: WalletServiceTwoStepSend { throw WalletServiceError.invalidAmount(amount) } - guard let web3 = await web3 else { - throw WalletServiceError.internalError(message: "Failed to get web3", error: nil) - } - guard let keystoreManager = web3.provider.attachedKeystoreManager else { throw WalletServiceError.internalError(message: "Failed to get web3.provider.KeystoreManager", error: nil) } @@ -81,24 +88,13 @@ extension EthWalletService: WalletServiceTwoStepSend { } } - func transferViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Ethereum.transfer) as? EthTransferViewController else { - fatalError("Can't get EthTransferViewController") - } - - vc.service = self - return vc - } - func sendTransaction(_ transaction: CodableTransaction) async throws { guard let txEncoded = transaction.encode() else { - throw WalletServiceError.internalError(message: String.adamant.sharedErrors.unknownError, error: nil) + throw WalletServiceError.internalError(message: .adamant.sharedErrors.unknownError, error: nil) } - do { - _ = try await web3?.eth.send(raw: txEncoded) - } catch { - throw WalletServiceError.internalError(message: "Error: \(error.localizedDescription)", error: nil) - } + _ = try await ethApiService.requestWeb3 { web3 in + try await web3.eth.send(raw: txEncoded) + }.get() } } diff --git a/Adamant/Wallets/Ethereum/EthWalletService.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift similarity index 67% rename from Adamant/Wallets/Ethereum/EthWalletService.swift rename to Adamant/Modules/Wallets/Ethereum/EthWalletService.swift index 54b0461bb..9adaa8377 100644 --- a/Adamant/Wallets/Ethereum/EthWalletService.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift @@ -19,9 +19,7 @@ import CommonKit struct EthWalletStorage { let keystore: BIP32Keystore - func getWalet(with web3: Web3) -> EthWallet? { - web3.addKeystoreManager(KeystoreManager([keystore])) - + func getWallet() -> EthWallet? { guard let ethAddress = keystore.addresses?.first else { return nil } @@ -65,7 +63,7 @@ extension Web3Error { } } -class EthWalletService: WalletService { +final class EthWalletService: WalletService { // MARK: - Constants let addressRegex = try! NSRegularExpression(pattern: "^0x[a-fA-F0-9]{40}$") @@ -79,7 +77,7 @@ class EthWalletService: WalletService { return type(of: self).currencyLogo } - var tokenNetworkSymbol: String { + static var tokenNetworkSymbol: String { return "ERC20" } @@ -88,7 +86,7 @@ class EthWalletService: WalletService { } var tokenUnicID: String { - return tokenNetworkSymbol + tokenSymbol + Self.tokenNetworkSymbol + tokenSymbol } var richMessageType: String { @@ -107,11 +105,11 @@ class EthWalletService: WalletService { return increaseFeeService.isIncreaseFeeEnabled(for: tokenUnicID) } - private (set) var isDynamicFee: Bool = true - private (set) var transactionFee: Decimal = 0.0 - private (set) var gasPrice: BigUInt = 0 - private (set) var gasLimit: BigUInt = 0 - private (set) var isWarningGasPrice = false + @Atomic private(set) var isDynamicFee: Bool = true + @Atomic private(set) var transactionFee: Decimal = 0.0 + @Atomic private(set) var gasPrice: BigUInt = 0 + @Atomic private(set) var gasLimit: BigUInt = 0 + @Atomic private(set) var isWarningGasPrice = false static let transferGas: Decimal = 21000 static let kvsAddress = "eth:address" @@ -122,9 +120,11 @@ class EthWalletService: WalletService { // MARK: - Dependencies weak var accountService: AccountService? var apiService: ApiService! + var ethApiService: EthApiService! var dialogService: DialogService! - var router: Router! var increaseFeeService: IncreaseFeeService! + var vibroService: VibroService! + var coreDataStack: CoreDataStack! // MARK: - Notifications let walletUpdatedNotification = Notification.Name("adamant.ethWallet.walletUpdated") @@ -138,38 +138,28 @@ class EthWalletService: WalletService { // MARK: - Properties public static let transactionsListApiSubpath = "ethtxs" - - private var _ethNodeUrl: String? - private var _web3: Web3? - var web3: Web3? { - get async { - if _web3 != nil { - return _web3 - } - guard let url = _ethNodeUrl else { - return nil - } - - return await setupEthNode(with: url) - } + @Atomic private(set) var enabled = true + @Atomic private var subscriptions = Set() + + @ObservableValue private(set) var historyTransactions: [TransactionDetails] = [] + @ObservableValue private(set) var hasMoreOldTransactions: Bool = true + + var transactionsPublisher: AnyObservable<[TransactionDetails]> { + $historyTransactions.eraseToAnyPublisher() } - private (set) var enabled = true - - var walletViewController: WalletViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Ethereum.wallet) as? EthWalletViewController else { - fatalError("Can't get EthWalletViewController") - } - - vc.service = self - return vc + var hasMoreOldTransactionsPublisher: AnyObservable { + $hasMoreOldTransactions.eraseToAnyPublisher() } - private var initialBalanceCheck = false - private var subscriptions = Set() - + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( + coinId: tokenUnicID, + coreDataStack: coreDataStack, + blockchainType: richMessageType + ) + // MARK: - State - private (set) var state: WalletServiceState = .notInitiated + @Atomic private(set) var state: WalletServiceState = .notInitiated private func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { @@ -179,19 +169,21 @@ class EthWalletService: WalletService { state = newState if !silent { - NotificationCenter.default.post(name: serviceStateChanged, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.walletState: state]) + NotificationCenter.default.post( + name: serviceStateChanged, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.walletState: state] + ) } } - private (set) var ethWallet: EthWallet? - private var waletStorage: EthWalletStorage? + @Atomic private(set) var ethWallet: EthWallet? + @Atomic private var walletStorage: EthWalletStorage? var wallet: WalletAccount? { return ethWallet } // MARK: - Delayed KVS save - private var balanceObserver: NSObjectProtocol? + @Atomic private var balanceObserver: NSObjectProtocol? // MARK: - Logic init() { @@ -221,46 +213,32 @@ class EthWalletService: WalletService { .receive(on: OperationQueue.main) .sink { [weak self] _ in self?.ethWallet = nil - self?.initialBalanceCheck = false if let balanceObserver = self?.balanceObserver { NotificationCenter.default.removeObserver(balanceObserver) self?.balanceObserver = nil } + self?.coinStorage.clear() + self?.hasMoreOldTransactions = true + self?.historyTransactions = [] } .store(in: &subscriptions) } - func initiateNetwork(apiUrl: String, completion: @escaping (WalletServiceSimpleResult) -> Void) { - Task { - self._ethNodeUrl = apiUrl - guard await self.setupEthNode(with: apiUrl) != nil else { - completion(.failure(error: WalletServiceError.networkError)) - return + func addTransactionObserver() { + coinStorage.transactionsPublisher + .sink { [weak self] transactions in + self?.historyTransactions = transactions } - } - } - - func setupEthNode(with apiUrl: String) async -> Web3? { - guard let url = URL(string: apiUrl), - let web3 = try? await Web3.new(url) else { - return nil - } - - self._web3 = web3 - - return web3 + .store(in: &subscriptions) } func getWallet() async -> EthWallet? { if let wallet = ethWallet { return wallet } - guard let storage = waletStorage, - let web3 = await web3 - else { - return nil - } - return storage.getWalet(with: web3) + + guard let storage = walletStorage else { return nil } + return storage.getWallet() } func update() { @@ -286,21 +264,25 @@ class EthWalletService: WalletService { setState(.updating) if let balance = try? await getBalance(forAddress: wallet.ethAddress) { - wallet.isBalanceInitialized = true - let notification: Notification.Name? + let isRaised = (wallet.balance < balance) && wallet.isBalanceInitialized + if wallet.balance != balance { wallet.balance = balance notification = walletUpdatedNotification - initialBalanceCheck = false - } else if initialBalanceCheck { - initialBalanceCheck = false + } else if !wallet.isBalanceInitialized { notification = walletUpdatedNotification } else { notification = nil } + wallet.isBalanceInitialized = true + + if isRaised { + vibroService.applyVibration(.success) + } + if let notification = notification { NotificationCenter.default.post(name: notification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet]) } @@ -357,49 +339,20 @@ class EthWalletService: WalletService { } func getGasPrices() async throws -> BigUInt { - guard let web3 = await self.web3 else { - throw WalletServiceError.internalError(message: "Can't get web3 service", error: nil) - } - - do { - let price = try await web3.eth.gasPrice() - return price - } catch { - throw WalletServiceError.remoteServiceError( - message: error.localizedDescription - ) - } + try await ethApiService.requestWeb3 { web3 in + try await web3.eth.gasPrice() + }.get() } func getGasLimit(to address: EthereumAddress?) async throws -> BigUInt { - guard let web3 = await self.web3, - let ethWallet = ethWallet - else { - throw WalletServiceError.internalError(message: "Can't get web3 service", error: nil) - } + guard let ethWallet = ethWallet else { throw WalletServiceError.internalError(.endpointBuildFailed) } + var transaction: CodableTransaction = .emptyTransaction + transaction.from = ethWallet.ethAddress + transaction.to = address ?? ethWallet.ethAddress - do { - var transaction: CodableTransaction = .emptyTransaction - transaction.from = ethWallet.ethAddress - transaction.to = address ?? ethWallet.ethAddress - - let price = try await web3.eth.estimateGas(for: transaction) - return price - } catch { - throw WalletServiceError.remoteServiceError( - message: error.localizedDescription - ) - } - } - - private func buildUrl(url: URL, queryItems: [URLQueryItem]? = nil) throws -> URL { - guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - throw AdamantApiService.InternalError.endpointBuildFailed - } - - components.queryItems = queryItems - - return try components.asURL() + return try await ethApiService.requestWeb3 { [transaction] web3 in + try await web3.eth.estimateGas(for: transaction) + }.get() } } @@ -429,20 +382,27 @@ extension EthWalletService: InitiatedWithPassphraseService { throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: nil) } - waletStorage = .init(keystore: store) + walletStorage = .init(keystore: store) + await ethApiService.setKeystoreManager(.init([store])) } catch { throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: error) } - guard let web3 = await web3, - let eWallet = waletStorage?.getWalet(with: web3) - else { + let eWallet = walletStorage?.getWallet() + + guard let eWallet = eWallet else { throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: nil) } // MARK: 3. Update ethWallet = eWallet + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: eWallet] + ) + if !enabled { enabled = true NotificationCenter.default.post(name: serviceEnabledChanged, object: self) @@ -458,7 +418,6 @@ extension EthWalletService: InitiatedWithPassphraseService { } } - service.initialBalanceCheck = true service.setState(.upToDate, silent: true) Task { @@ -537,8 +496,12 @@ extension EthWalletService: SwinjectDependentService { accountService = container.resolve(AccountService.self) apiService = container.resolve(ApiService.self) dialogService = container.resolve(DialogService.self) - router = container.resolve(Router.self) increaseFeeService = container.resolve(IncreaseFeeService.self) + ethApiService = container.resolve(EthApiService.self) + vibroService = container.resolve(VibroService.self) + coreDataStack = container.resolve(CoreDataStack.self) + + addTransactionObserver() } } @@ -553,21 +516,16 @@ extension EthWalletService { } func getBalance(forAddress address: EthereumAddress) async throws -> Decimal { - guard let web3 = await self.web3 else { - throw WalletServiceError.internalError(message: "Can't get web3 service", error: nil) - } + let balance = try await ethApiService.requestWeb3 { web3 in + try await web3.eth.getBalance(for: address) + }.get() - do { - let balance = try await web3.eth.getBalance(for: address) - return balance.asDecimal(exponent: EthWalletService.currencyExponent) - } catch { - throw WalletServiceError.remoteServiceError(message: error.localizedDescription) - } + return balance.asDecimal(exponent: EthWalletService.currencyExponent) } func getWalletAddress(byAdamantAddress address: String) async throws -> String { do { - let result = try await apiService.get(key: EthWalletService.kvsAddress, sender: address) + let result = try await apiService.get(key: EthWalletService.kvsAddress, sender: address).get() guard let result = result else { throw WalletServiceError.walletNotInitiated @@ -600,14 +558,20 @@ extension EthWalletService { } Task { - await apiService.store(key: EthWalletService.kvsAddress, value: ethAddress, type: .keyValue, sender: adamant.address, keypair: keypair) { result in - switch result { - case .success: - completion(.success) - - case .failure(let error): - completion(.failure(error: .apiError(error))) - } + let result = await apiService.store( + key: EthWalletService.kvsAddress, + value: ethAddress, + type: .keyValue, + sender: adamant.address, + keypair: keypair + ) + + switch result { + case .success: + completion(.success) + + case .failure(let error): + completion(.failure(error: .apiError(error))) } } } @@ -617,31 +581,24 @@ extension EthWalletService { extension EthWalletService { func getTransaction(by hash: String) async throws -> EthTransaction { let sender = wallet?.address - guard let eth = await web3?.eth else { - throw WalletServiceError.internalError(message: "Failed to get transaction", error: nil) - } - - let isOutgoing: Bool - let details: Web3Core.TransactionDetails // MARK: 1. Transaction details - do { - details = try await eth.transactionDetails(hash) - - if let sender = sender { - isOutgoing = details.transaction.to.address != sender - } else { - isOutgoing = false - } - } catch let error as Web3Error { - throw error.asWalletServiceError() - } catch { - throw WalletServiceError.remoteServiceError(message: "Failed to get transaction") + let details = try await ethApiService.requestWeb3 { web3 in + try await web3.eth.transactionDetails(hash) + }.get() + + let isOutgoing: Bool + if let sender = sender { + isOutgoing = details.transaction.to.address != sender + } else { + isOutgoing = false } // MARK: 2. Transaction receipt do { - let receipt = try await eth.transactionReceipt(hash) + let receipt = try await ethApiService.requestWeb3 { web3 in + try await web3.eth.transactionReceipt(hash) + }.get() // MARK: 3. Check if transaction is delivered guard receipt.status == .ok, @@ -660,8 +617,14 @@ extension EthWalletService { } // MARK: 4. Block timestamp & confirmations - let currentBlock = try await eth.blockNumber() - let block = try await eth.block(by: receipt.blockHash) + let currentBlock = try await ethApiService.requestWeb3 { web3 in + try await web3.eth.blockNumber() + }.get() + + let block = try await ethApiService.requestWeb3 { web3 in + try await web3.eth.block(by: receipt.blockHash) + }.get() + let confirmations = currentBlock - blockNumber let transaction = details.transaction.asEthTransaction( @@ -692,68 +655,108 @@ extension EthWalletService { return transaction default: - throw error.asWalletServiceError() + throw error } + } catch _ as URLError { + throw WalletServiceError.networkError } catch { - throw WalletServiceError.remoteServiceError(message: "Failed to get transaction") + throw error } } - func getTransactionsHistory(address: String, offset: Int = 0, limit: Int = 100) async throws -> [EthTransactionShort] { - guard let node = EthWalletService.nodes.randomElement(), let url = node.asURL() else { - fatalError("Failed to build ETH endpoint URL") - } - - // Request + func getTransactionsHistory( + address: String, + offset: Int, + limit: Int = 100 + ) async throws -> [EthTransactionShort] { let columns = "time,txfrom,txto,gas,gasprice,block,txhash,value" let order = "time.desc" - // MARK: Request txFrom - let txFromQueryItems: [URLQueryItem] = [URLQueryItem(name: "select", value: columns), - URLQueryItem(name: "limit", value: String(limit)), - URLQueryItem(name: "txfrom", value: "eq.\(address)"), - URLQueryItem(name: "offset", value: String(offset)), - URLQueryItem(name: "order", value: order), - URLQueryItem(name: "contract_to", value: "eq.") + let txFromQueryParameters = [ + "select": columns, + "limit": String(limit), + "txfrom": "eq.\(address)", + "offset": String(offset), + "order": order, + "contract_to": "eq." ] - let txFromEndpoint: URL - do { - txFromEndpoint = try buildUrl(url: url.appendingPathComponent(EthWalletService.transactionsListApiSubpath), queryItems: txFromQueryItems) - } catch { - let err = AdamantApiService.InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) - throw WalletServiceError.apiError(err) - } - - // MARK: Request txTo - let txToQueryItems: [URLQueryItem] = [URLQueryItem(name: "select", value: columns), - URLQueryItem(name: "limit", value: String(limit)), - URLQueryItem(name: "txto", value: "eq.\(address)"), - URLQueryItem(name: "offset", value: String(offset)), - URLQueryItem(name: "order", value: order), - URLQueryItem(name: "contract_to", value: "eq.") + let txToQueryParameters = [ + "select": columns, + "limit": String(limit), + "txto": "eq.\(address)", + "offset": String(offset), + "order": order, + "contract_to": "eq." ] - let txToEndpoint: URL - do { - txToEndpoint = try buildUrl(url: url.appendingPathComponent(EthWalletService.transactionsListApiSubpath), queryItems: txToQueryItems) - } catch { - let err = AdamantApiService.InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) - throw WalletServiceError.apiError(err) + let transactionsFrom: [EthTransactionShort] = try await ethApiService.requestApiCore { core, node in + await core.sendRequestJsonResponse( + node: node, + path: EthWalletService.transactionsListApiSubpath, + method: .get, + parameters: txFromQueryParameters, + encoding: .url + ) + }.get() + + let transactionsTo: [EthTransactionShort] = try await ethApiService.requestApiCore { core, node in + await core.sendRequestJsonResponse( + node: node, + path: EthWalletService.transactionsListApiSubpath, + method: .get, + parameters: txToQueryParameters, + encoding: .url + ) + }.get() + + let transactions = transactionsFrom + transactionsTo + return transactions.sorted { $0.date.compare($1.date) == .orderedDescending } + } + + func loadTransactions(offset: Int, limit: Int) async throws -> Int { + guard let address = wallet?.address else { + return . zero } - // MARK: Sending requests + let trs = try await getTransactionsHistory( + address: address, + offset: offset, + limit: limit + ) - var transactions = [EthTransactionShort]() + guard trs.count > 0 else { + hasMoreOldTransactions = false + return .zero + } - let transactionsFrom: [EthTransactionShort] = try await apiService.sendRequest(url: txFromEndpoint, method: .get, parameters: nil) - transactions.append(contentsOf: transactionsFrom) + let newTrs = trs.map { transaction in + let isOutgoing: Bool = transaction.from == address + return SimpleTransactionDetails( + txId: transaction.hash, + senderAddress: transaction.from, + recipientAddress: transaction.to, + dateValue: transaction.date, + amountValue: transaction.value, + feeValue: transaction.gasUsed * transaction.gasPrice, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: isOutgoing, + transactionStatus: TransactionStatus.notInitiated + ) + } - let transactionsTo: [EthTransactionShort] = try await apiService.sendRequest(url: txToEndpoint, method: .get, parameters: nil) - transactions.append(contentsOf: transactionsTo) + coinStorage.append(newTrs) - transactions.sort { $0.date.compare($1.date) == .orderedDescending } - return transactions + return trs.count + } + + func getLocalTransactionHistory() -> [TransactionDetails] { + historyTransactions + } + + func updateStatus(for id: String, status: TransactionStatus?) { + coinStorage.updateStatus(for: id, status: status) } } diff --git a/Adamant/Wallets/Ethereum/EthWalletViewController.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletViewController.swift similarity index 93% rename from Adamant/Wallets/Ethereum/EthWalletViewController.swift rename to Adamant/Modules/Wallets/Ethereum/EthWalletViewController.swift index d2d8a7afa..ae755ec1a 100644 --- a/Adamant/Wallets/Ethereum/EthWalletViewController.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletViewController.swift @@ -15,7 +15,7 @@ extension String.adamant.wallets { static let sendEth = String.localized("AccountTab.Row.SendEth", comment: "Account tab: 'Send ETH tokens' button") } -class EthWalletViewController: WalletViewControllerBase { +final class EthWalletViewController: WalletViewControllerBase { // MARK: Lifecycle override func viewDidLoad() { diff --git a/Adamant/Modules/Wallets/Lisk/LskApiCore.swift b/Adamant/Modules/Wallets/Lisk/LskApiCore.swift new file mode 100644 index 000000000..b2e7e7c7a --- /dev/null +++ b/Adamant/Modules/Wallets/Lisk/LskApiCore.swift @@ -0,0 +1,71 @@ +// +// LskApiCore.swift +// Adamant +// +// Created by Andrew G on 13.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import Foundation +import LiskKit + +class LskApiCore: BlockchainHealthCheckableService { + func makeClient(node: CommonKit.Node) -> APIClient { + .init(options: .init( + nodes: [.init(origin: node.asString())], + nethash: .mainnet, + randomNode: false + )) + } + + func request( + node: CommonKit.Node, + body: @escaping @Sendable ( + _ client: APIClient, + _ completion: @escaping @Sendable (LiskKit.Result) -> Void + ) -> Void + ) async -> WalletServiceResult { + await withCheckedContinuation { continuation in + body(makeClient(node: node)) { result in + continuation.resume(returning: result.asWalletServiceResult()) + } + } + } + + func getStatusInfo(node: CommonKit.Node) async -> WalletServiceResult { + let startTimestamp = Date.now.timeIntervalSince1970 + + return await request(node: node) { client, completion in + LiskKit.Node(client: client).info { completion($0) } + }.map { model in + .init( + ping: Date.now.timeIntervalSince1970 - startTimestamp, + height: model.data.height ?? .zero, + wsEnabled: false, + wsPort: nil, + version: nil + ) + } + } +} + +private extension LiskKit.Result { + func asWalletServiceResult() -> WalletServiceResult { + switch self { + case let .success(response): + return .success(response) + case let .error(error): + return .failure(mapError(error)) + } + } +} + +private func mapError(_ error: APIError) -> WalletServiceError { + switch error { + case .noNetwork: + return .networkError + default: + return .remoteServiceError(message: error.message, error: error) + } +} diff --git a/Adamant/Modules/Wallets/Lisk/LskNodeApiService.swift b/Adamant/Modules/Wallets/Lisk/LskNodeApiService.swift new file mode 100644 index 000000000..cc0993cc4 --- /dev/null +++ b/Adamant/Modules/Wallets/Lisk/LskNodeApiService.swift @@ -0,0 +1,78 @@ +// +// LskNodeApiService.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import LiskKit +import Foundation + +final class LskNodeApiService: WalletApiService { + let api: BlockchainHealthCheckWrapper + + var preferredNodeIds: [UUID] { + api.preferredNodeIds + } + + init(api: BlockchainHealthCheckWrapper) { + self.api = api + } + + func healthCheck() { + api.healthCheck() + } + + func requestNodeApi( + body: @escaping @Sendable ( + _ api: LiskKit.Node, + _ completion: @escaping @Sendable (LiskKit.Result) -> Void + ) -> Void + ) async -> WalletServiceResult { + await requestClient { client, completion in + body(.init(client: client), completion) + } + } + + func requestTransactionsApi( + body: @escaping @Sendable ( + _ api: Transactions, + _ completion: @escaping @Sendable (LiskKit.Result) -> Void + ) -> Void + ) async -> WalletServiceResult { + await requestClient { client, completion in + body(.init(client: client), completion) + } + } + + func requestAccountsApi( + body: @escaping @Sendable ( + _ api: Accounts, + _ completion: @escaping @Sendable (LiskKit.Result) -> Void + ) -> Void + ) async -> WalletServiceResult { + await requestClient { client, completion in + body(.init(client: client), completion) + } + } + + func getStatusInfo() async -> WalletServiceResult { + await api.request { core, node in + await core.getStatusInfo(node: node) + } + } +} + +private extension LskNodeApiService { + func requestClient( + body: @escaping @Sendable ( + _ client: APIClient, + _ completion: @escaping @Sendable (LiskKit.Result) -> Void + ) -> Void + ) async -> WalletServiceResult { + await api.request { core, node in + await core.request(node: node, body: body) + } + } +} diff --git a/Adamant/Modules/Wallets/Lisk/LskServiceApiService.swift b/Adamant/Modules/Wallets/Lisk/LskServiceApiService.swift new file mode 100644 index 000000000..b272d3416 --- /dev/null +++ b/Adamant/Modules/Wallets/Lisk/LskServiceApiService.swift @@ -0,0 +1,71 @@ +// +// LskServiceApiService.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import LiskKit +import Foundation +import CommonKit + +final class LskServiceApiCore: LskApiCore { + override func getStatusInfo( + node: CommonKit.Node + ) async -> WalletServiceResult { + let startTimestamp = Date.now.timeIntervalSince1970 + + return await request(node: node) { client, completion in + LiskKit.Service(client: client).getFees { completion($0) } + }.map { model in + .init( + ping: Date.now.timeIntervalSince1970 - startTimestamp, + height: .init(model.meta.lastBlockHeight), + wsEnabled: false, + wsPort: nil, + version: nil + ) + } + } +} + +final class LskServiceApiService: WalletApiService { + let api: BlockchainHealthCheckWrapper + + var preferredNodeIds: [UUID] { + api.preferredNodeIds + } + + init(api: BlockchainHealthCheckWrapper) { + self.api = api + } + + func healthCheck() { + api.healthCheck() + } + + func requestServiceApi( + body: @escaping @Sendable ( + _ api: LiskKit.Service, + _ completion: @escaping @Sendable (LiskKit.Result) -> Void + ) -> Void + ) async -> WalletServiceResult { + await requestClient { client, completion in + body(.init(client: client, version: .v2), completion) + } + } +} + +private extension LskServiceApiService { + func requestClient( + body: @escaping @Sendable ( + _ client: APIClient, + _ completion: @escaping @Sendable (LiskKit.Result) -> Void + ) -> Void + ) async -> WalletServiceResult { + await api.request { core, node in + await core.request(node: node, body: body) + } + } +} diff --git a/Adamant/Wallets/Lisk/LskTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Lisk/LskTransactionDetailsViewController.swift similarity index 96% rename from Adamant/Wallets/Lisk/LskTransactionDetailsViewController.swift rename to Adamant/Modules/Wallets/Lisk/LskTransactionDetailsViewController.swift index 3d6a90c73..72c65fa1c 100644 --- a/Adamant/Wallets/Lisk/LskTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Lisk/LskTransactionDetailsViewController.swift @@ -9,7 +9,7 @@ import UIKit import CommonKit -class LskTransactionDetailsViewController: TransactionDetailsViewControllerBase { +final class LskTransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies weak var service: LskWalletService? diff --git a/Adamant/Modules/Wallets/Lisk/LskTransactionsViewController.swift b/Adamant/Modules/Wallets/Lisk/LskTransactionsViewController.swift new file mode 100644 index 000000000..b9848906a --- /dev/null +++ b/Adamant/Modules/Wallets/Lisk/LskTransactionsViewController.swift @@ -0,0 +1,226 @@ +// +// LskTransactionsViewController +// Adamant +// +// Created by Anton Boyarkin on 17/07/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import LiskKit +import web3swift +import BigInt +import CommonKit +import Combine + +final class LskTransactionsViewController: TransactionsListViewControllerBase { + + // MARK: - Dependencies + var lskWalletService: LskWalletService! + var screensFactory: ScreensFactory! + + // MARK: - UITableView + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let address = lskWalletService.wallet?.address, + let transaction = transactions[safe: indexPath.row] + else { return } + + let controller = screensFactory.makeDetailsVC(service: lskWalletService) + + controller.transaction = transaction + + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { + controller.senderName = String.adamant.transactionDetails.yourAddress + } + + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { + controller.recipientName = String.adamant.transactionDetails.yourAddress + } + + navigationController?.pushViewController(controller, animated: true) + } +} + +extension Transactions.TransactionModel: TransactionDetails { + + var defaultCurrencySymbol: String? { LskWalletService.currencySymbol } + + var txId: String { + return id + } + + var dateValue: Date? { + return timestamp.map { Date(timeIntervalSince1970: TimeInterval($0)) } + } + + var amountValue: Decimal? { + let value = BigUInt(self.amount) ?? BigUInt(0) + + return value.asDecimal(exponent: LskWalletService.currencyExponent) + } + + var feeValue: Decimal? { + let value = BigUInt(self.fee) ?? BigUInt(0) + + return value.asDecimal(exponent: LskWalletService.currencyExponent) + } + + var confirmationsValue: String? { + guard let confirmations = confirmations, let height = height else { return "0" } + if confirmations < height { return "0" } + if confirmations > 0 { + return "\(confirmations - height + 1)" + } + + return "\(confirmations)" + } + + var blockHeight: UInt64? { + return self.height + } + + var blockValue: String? { + return self.blockId + } + + var transactionStatus: TransactionStatus? { + guard let confirmations = confirmations, + let height = height, + confirmations > .zero + else { return .notInitiated } + + if confirmations < height { return .registered } + + if confirmations > 0 && height > 0 { + let conf = (confirmations - height) + 1 + if conf > 1 { + return .success + } else { + return .pending + } + } + return .notInitiated + } + + var senderAddress: String { + return self.senderId + } + + var recipientAddress: String { + return self.recipientId ?? "" + } + + var sentDate: Date? { + timestamp.map { Date(timeIntervalSince1970: TimeInterval($0)) } + } +} + +extension LocalTransaction: TransactionDetails { + + var defaultCurrencySymbol: String? { LskWalletService.currencySymbol } + + var txId: String { + return id ?? "" + } + + var senderAddress: String { + return "" + } + + var recipientAddress: String { + return self.recipientId ?? "" + } + + var dateValue: Date? { + return Date(timeIntervalSince1970: TimeInterval(self.timestamp)) + } + + var amountValue: Decimal? { + let value = BigUInt(self.amount) + + return value.asDecimal(exponent: LskWalletService.currencyExponent) + } + + var feeValue: Decimal? { + let value = BigUInt(self.fee) + + return value.asDecimal(exponent: LskWalletService.currencyExponent) + } + + var confirmationsValue: String? { + return nil + } + + var blockHeight: UInt64? { + return nil + } + + var blockValue: String? { + return nil + } + + var isOutgoing: Bool { + return true + } + + var transactionStatus: TransactionStatus? { + return .notInitiated + } + +} + +extension TransactionEntity: TransactionDetails { + + var defaultCurrencySymbol: String? { LskWalletService.currencySymbol } + + var txId: String { + return id + } + + var senderAddress: String { + return LiskKit.Crypto.getBase32Address(from: senderPublicKey) + } + + var recipientAddress: String { + return self.asset.recipientAddressBase32 + } + + var dateValue: Date? { + return nil + } + + var amountValue: Decimal? { + let value = BigUInt(self.asset.amount) + + return value.asDecimal(exponent: LskWalletService.currencyExponent) + } + + var feeValue: Decimal? { + let value = BigUInt(self.fee) + + return value.asDecimal(exponent: LskWalletService.currencyExponent) + } + + var confirmationsValue: String? { + return nil + } + + var blockHeight: UInt64? { + return nil + } + + var blockValue: String? { + return nil + } + + var isOutgoing: Bool { + return true + } + + var transactionStatus: TransactionStatus? { + return .notInitiated + } + +} diff --git a/Adamant/Wallets/Lisk/LskTransferViewController.swift b/Adamant/Modules/Wallets/Lisk/LskTransferViewController.swift similarity index 86% rename from Adamant/Wallets/Lisk/LskTransferViewController.swift rename to Adamant/Modules/Wallets/Lisk/LskTransferViewController.swift index db79a0a75..891438077 100644 --- a/Adamant/Wallets/Lisk/LskTransferViewController.swift +++ b/Adamant/Modules/Wallets/Lisk/LskTransferViewController.swift @@ -11,6 +11,7 @@ import Eureka import LiskKit import CommonKit +@MainActor final class LskTransferViewController: TransferViewControllerBase { // MARK: Properties @@ -67,9 +68,14 @@ final class LskTransferViewController: TransferViewControllerBase { Task { do { + service.coinStorage.append(transaction) try await service.sendTransaction(transaction) } catch { dialogService.showRichError(error: error) + service.coinStorage.updateStatus( + for: transaction.id, + status: .failed + ) } await service.update() @@ -98,30 +104,22 @@ final class LskTransferViewController: TransferViewControllerBase { service: LskWalletService, comments: String ) { - if let detailsVc = router.get(scene: AdamantScene.Wallets.Lisk.transactionDetails) as? LskTransactionDetailsViewController { - var transaction: TransactionEntity = transaction - transaction.id = transactionId - detailsVc.transaction = transaction - detailsVc.service = service - detailsVc.senderName = String.adamant.transactionDetails.yourAddress - detailsVc.recipientName = recipientName - - if comments.count > 0 { - detailsVc.comment = comments - } - - delegate?.transferViewController( - self, - didFinishWithTransfer: transaction, - detailsViewController: detailsVc - ) - } else { - delegate?.transferViewController( - self, - didFinishWithTransfer: transaction, - detailsViewController: nil - ) + let detailsVc = screensFactory.makeDetailsVC(service: service) + var transaction: TransactionEntity = transaction + transaction.id = transactionId + detailsVc.transaction = transaction + detailsVc.senderName = String.adamant.transactionDetails.yourAddress + detailsVc.recipientName = recipientName + + if comments.count > 0 { + detailsVc.comment = comments } + + delegate?.transferViewController( + self, + didFinishWithTransfer: transaction, + detailsViewController: detailsVc + ) } // MARK: Overrides diff --git a/Adamant/Wallets/Lisk/LskWallet.swift b/Adamant/Modules/Wallets/Lisk/LskWallet.swift similarity index 96% rename from Adamant/Wallets/Lisk/LskWallet.swift rename to Adamant/Modules/Wallets/Lisk/LskWallet.swift index d1963ef8d..f4bfa7afd 100644 --- a/Adamant/Wallets/Lisk/LskWallet.swift +++ b/Adamant/Modules/Wallets/Lisk/LskWallet.swift @@ -9,7 +9,7 @@ import Foundation import LiskKit -class LskWallet: WalletAccount { +final class LskWallet: WalletAccount { var address: String { return isNewApi ? lisk32Address : legacyAddress diff --git a/Adamant/Modules/Wallets/Lisk/LskWalletFactory.swift b/Adamant/Modules/Wallets/Lisk/LskWalletFactory.swift new file mode 100644 index 000000000..936a03423 --- /dev/null +++ b/Adamant/Modules/Wallets/Lisk/LskWalletFactory.swift @@ -0,0 +1,137 @@ +// +// LskWalletFactory.swift +// Adamant +// +// Created by Anton Boyarkin on 27/11/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Swinject +import UIKit +import CommonKit +import LiskKit + +struct LskWalletFactory: WalletFactory { + typealias Service = LskWalletService + + let assembler: Assembler + + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { + let c = LskWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) + c.dialogService = assembler.resolve(DialogService.self) + c.currencyInfoService = assembler.resolve(CurrencyInfoService.self) + c.accountService = assembler.resolve(AccountService.self) + c.service = service + c.screensFactory = screensFactory + return c + } + + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { + let c = LskTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) + c.dialogService = assembler.resolve(DialogService.self) + c.screensFactory = screensFactory + c.lskWalletService = service + c.walletService = service + return c + } + + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { + let vc = LskTransferViewController( + chatsProvider: assembler.resolve(ChatsProvider.self)!, + accountService: assembler.resolve(AccountService.self)!, + accountsProvider: assembler.resolve(AccountsProvider.self)!, + dialogService: assembler.resolve(DialogService.self)!, + screensFactory: screensFactory, + currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, + vibroService: assembler.resolve(VibroService.self)! + ) + + vc.service = service + return vc + } + + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { + guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + else { return nil } + + let comment: String? + if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { + comment = raw + } else { + comment = nil + } + + return makeTransactionDetailsVC( + hash: hash, + senderId: transaction.senderId, + recipientId: transaction.recipientId, + comment: comment, + senderAddress: "", + recipientAddress: "", + transaction: nil, + richTransaction: transaction, + service: service + ) + } + + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { + makeTransactionDetailsVC(service: service) + } +} + +private extension LskWalletFactory { + private func makeTransactionDetailsVC( + hash: String, + senderId: String?, + recipientId: String?, + comment: String?, + senderAddress: String, + recipientAddress: String, + transaction: Transactions.TransactionModel?, + richTransaction: RichMessageTransaction, + service: Service + ) -> UIViewController { + let vc = makeTransactionDetailsVC(service: service) + vc.senderId = senderId + vc.recipientId = recipientId + vc.comment = comment + + let amount: Decimal + if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), + let decimal = Decimal(string: amountRaw) { + amount = decimal + } else { + amount = 0 + } + + let failedTransaction = SimpleTransactionDetails( + txId: hash, + senderAddress: senderAddress, + recipientAddress: recipientAddress, + dateValue: nil, + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: richTransaction.isOutgoing, + transactionStatus: nil) + + vc.transaction = transaction ?? failedTransaction + vc.richTransaction = richTransaction + return vc + } + + func makeTransactionDetailsVC(service: Service) -> LskTransactionDetailsViewController { + let vc = LskTransactionDetailsViewController( + dialogService: assembler.resolve(DialogService.self)!, + currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + addressBookService: assembler.resolve(AddressBookService.self)!, + accountService: assembler.resolve(AccountService.self)!, + walletService: service + ) + + vc.service = service + return vc + } +} diff --git a/Adamant/Wallets/Lisk/LskWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Lisk/LskWalletService+DynamicConstants.swift similarity index 96% rename from Adamant/Wallets/Lisk/LskWalletService+DynamicConstants.swift rename to Adamant/Modules/Wallets/Lisk/LskWalletService+DynamicConstants.swift index ed23b215f..8d84376d7 100644 --- a/Adamant/Wallets/Lisk/LskWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Lisk/LskWalletService+DynamicConstants.swift @@ -53,6 +53,10 @@ extension LskWalletService { 60 } + var minNodeVersion: String? { + nil + } + static let explorerAddress = "https://liskscan.com/transaction/" static var nodes: [Node] { diff --git a/Adamant/Modules/Wallets/Lisk/LskWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Lisk/LskWalletService+RichMessageProvider.swift new file mode 100644 index 000000000..f27e9d3bf --- /dev/null +++ b/Adamant/Modules/Wallets/Lisk/LskWalletService+RichMessageProvider.swift @@ -0,0 +1,65 @@ +// +// LskWalletService+RichMessageProvider.swift +// Adamant +// +// Created by Anton Boyarkin on 06/12/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import MessageKit +import UIKit +import LiskKit +import CommonKit + +extension LskWalletService: RichMessageProvider { + var newPendingInterval: TimeInterval { + .init(milliseconds: type(of: self).newPendingInterval) + } + + var oldPendingInterval: TimeInterval { + .init(milliseconds: type(of: self).oldPendingInterval) + } + + var registeredInterval: TimeInterval { + .init(milliseconds: type(of: self).registeredInterval) + } + + var newPendingAttempts: Int { + type(of: self).newPendingAttempts + } + + var oldPendingAttempts: Int { + type(of: self).oldPendingAttempts + } + + var dynamicRichMessageType: String { + return type(of: self).richMessageType + } + + // MARK: Short description + + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { + let amount: String + + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) + else { + return NSAttributedString(string: "⬅️ \(LskWalletService.currencySymbol)") + } + + if let decimal = Decimal(string: raw) { + amount = AdamantBalanceFormat.full.format(decimal) + } else { + amount = raw + } + + let string: String + if transaction.isOutgoing { + string = "⬅️ \(amount) \(LskWalletService.currencySymbol)" + } else { + string = "➡️ \(amount) \(LskWalletService.currencySymbol)" + } + + return NSAttributedString(string: string) + } +} diff --git a/Adamant/Wallets/Lisk/LskWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Modules/Wallets/Lisk/LskWalletService+RichMessageProviderWithStatusCheck.swift similarity index 64% rename from Adamant/Wallets/Lisk/LskWalletService+RichMessageProviderWithStatusCheck.swift rename to Adamant/Modules/Wallets/Lisk/LskWalletService+RichMessageProviderWithStatusCheck.swift index 037bc5b00..89494e673 100644 --- a/Adamant/Wallets/Lisk/LskWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Modules/Wallets/Lisk/LskWalletService+RichMessageProviderWithStatusCheck.swift @@ -11,9 +11,16 @@ import LiskKit import CommonKit extension LskWalletService: RichMessageProviderWithStatusCheck { - func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo { - guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) - else { + func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { + let hash: String? + + if let transaction = transaction as? RichMessageTransaction { + hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) + } else { + hash = transaction.txId + } + + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent) } @@ -22,12 +29,7 @@ extension LskWalletService: RichMessageProviderWithStatusCheck { do { lskTransaction = try await getTransaction(by: hash) } catch { - switch error { - case ApiServiceError.networkError(_): - return .init(sentDate: nil, status: .noNetwork) - default: - return .init(sentDate: nil, status: .pending) - } + return .init(error: error) } lskTransaction.updateConfirmations(value: lastHeight) @@ -45,7 +47,7 @@ extension LskWalletService: RichMessageProviderWithStatusCheck { private extension LskWalletService { func getStatus( lskTransaction: Transactions.TransactionModel, - transaction: RichMessageTransaction + transaction: CoinTransaction ) -> TransactionStatus { guard lskTransaction.blockId != nil else { return .registered } @@ -69,16 +71,28 @@ private extension LskWalletService { } // MARK: Check amount - if let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + guard isAmountCorrect( + transaction: transaction, + lskTransaction: lskTransaction + ) else { return .inconsistent } + + return .success + } + + func isAmountCorrect( + transaction: CoinTransaction, + lskTransaction: Transactions.TransactionModel + ) -> Bool { + if let transaction = transaction as? RichMessageTransaction, + let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), let reported = AdamantBalanceFormat.deserializeBalance(from: raw) { let min = reported - reported*0.005 let max = reported + reported*0.005 - guard (min...max).contains(lskTransaction.amountValue ?? 0) else { - return .inconsistent - } + let amount = lskTransaction.amountValue ?? 0 + return amount <= max && amount >= min } - return .success + return transaction.amountValue == lskTransaction.amountValue } } diff --git a/Adamant/Wallets/Lisk/LskWalletService+Send.swift b/Adamant/Modules/Wallets/Lisk/LskWalletService+Send.swift similarity index 62% rename from Adamant/Wallets/Lisk/LskWalletService+Send.swift rename to Adamant/Modules/Wallets/Lisk/LskWalletService+Send.swift index 712c46887..7e8ba319a 100644 --- a/Adamant/Wallets/Lisk/LskWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Lisk/LskWalletService+Send.swift @@ -25,15 +25,6 @@ extension TransactionEntity: RawTransaction { extension LskWalletService: WalletServiceTwoStepSend { typealias T = TransactionEntity - func transferViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Lisk.transfer) as? LskTransferViewController else { - fatalError("Can't get LskTransferViewController") - } - - vc.service = self - return vc - } - // MARK: Create & Send func createTransaction(recipient: String, amount: Decimal) async throws -> TransactionEntity { // MARK: 1. Prepare @@ -50,7 +41,8 @@ extension LskWalletService: WalletServiceTwoStepSend { fee: self.transactionFee, nonce: wallet.nounce, senderPublicKey: wallet.keyPair.publicKeyString, - recipientAddress: binaryAddress + recipientAddressBase32: recipient, + recipientAddressBinary: binaryAddress ) var signedTransaction = transaction.signed(with: keys, for: self.netHash) @@ -59,15 +51,8 @@ extension LskWalletService: WalletServiceTwoStepSend { } func sendTransaction(_ transaction: TransactionEntity) async throws { - _ = try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - transactionApi.submit(signedTransaction: transaction.requestOptions) { response in - switch response { - case .success: - continuation.resume() - case .error(let error): - continuation.resume(throwing: WalletServiceError.internalError(message: error.message, error: nil)) - } - } - } + _ = try await lskNodeApiService.requestTransactionsApi { api, completion in + api.submit(signedTransaction: transaction.requestOptions, completionHandler: completion) + }.get() } } diff --git a/Adamant/Wallets/Lisk/LskWalletService.swift b/Adamant/Modules/Wallets/Lisk/LskWalletService.swift similarity index 56% rename from Adamant/Wallets/Lisk/LskWalletService.swift rename to Adamant/Modules/Wallets/Lisk/LskWalletService.swift index 3303a4805..48be5c4e9 100644 --- a/Adamant/Wallets/Lisk/LskWalletService.swift +++ b/Adamant/Modules/Wallets/Lisk/LskWalletService.swift @@ -17,19 +17,9 @@ import Web3Core import Combine import CommonKit -class LskWalletService: WalletService { - +final class LskWalletService: WalletService { var wallet: WalletAccount? { return lskWallet } - var walletViewController: WalletViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Lisk.wallet) as? LskWalletViewController else { - fatalError("Can't get LskWalletViewController") - } - - vc.service = self - return vc - } - // MARK: - Notifications let walletUpdatedNotification = Notification.Name("adamant.lskWallet.walletUpdated") let serviceEnabledChanged = Notification.Name("adamant.lskWallet.enabledChanged") @@ -41,24 +31,27 @@ class LskWalletService: WalletService { // MARK: - Dependencies var apiService: ApiService! + var lskNodeApiService: LskNodeApiService! + var lskServiceApiService: LskServiceApiService! var accountService: AccountService! var dialogService: DialogService! - var router: Router! + var vibroService: VibroService! + var coreDataStack: CoreDataStack! // MARK: - Constants var transactionFee: Decimal { - return transactionFeeRaw.asDecimal(exponent: LskWalletService.currencyExponent) + transactionFeeRaw.asDecimal(exponent: LskWalletService.currencyExponent) } - var transactionFeeRaw: BigUInt = BigUInt(integerLiteral: 141000) - private (set) var enabled = true - private (set) var isWarningGasPrice = false - static var currencyLogo = UIImage.asset(named: "lisk_wallet") ?? .init() + @Atomic var transactionFeeRaw: BigUInt = BigUInt(integerLiteral: 141000) + @Atomic private(set) var enabled = true + @Atomic private(set) var isWarningGasPrice = false + static let currencyLogo = UIImage.asset(named: "lisk_wallet") ?? .init() static let kvsAddress = "lsk:address" static let defaultFee: BigUInt = 141000 - var lastHeight: UInt64 = 0 + @Atomic var lastHeight: UInt64 = .zero var tokenSymbol: String { return type(of: self).currencySymbol @@ -68,7 +61,7 @@ class LskWalletService: WalletService { return type(of: self).currencyLogo } - var tokenNetworkSymbol: String { + static var tokenNetworkSymbol: String { return "LSK" } @@ -77,7 +70,7 @@ class LskWalletService: WalletService { } var tokenUnicID: String { - return tokenNetworkSymbol + tokenSymbol + Self.tokenNetworkSymbol + tokenSymbol } var richMessageType: String { @@ -90,24 +83,37 @@ class LskWalletService: WalletService { // MARK: - Properties let transferAvailable: Bool = true - private var initialBalanceCheck = false - - internal var accountApi: Accounts! - internal var transactionApi: Transactions! - internal var serviceApi: Service! - internal var nodeApi: LiskKit.Node! - internal var netHash: String = "" + let netHash = Constants.Nethash.main - private (set) var lskWallet: LskWallet? + @Atomic private(set) var lskWallet: LskWallet? - let defaultDispatchQueue = DispatchQueue(label: "im.adamant.lskWalletService", qos: .utility, attributes: [.concurrent]) + let defaultDispatchQueue = DispatchQueue( + label: "im.adamant.lskWalletService", + qos: .utility, + attributes: [.concurrent] + ) - private let mainnet: Bool - private let nodes: [APINode] - private var subscriptions = Set() + @Atomic private var subscriptions = Set() + + @ObservableValue private(set) var transactions: [TransactionDetails] = [] + @ObservableValue private(set) var hasMoreOldTransactions: Bool = true + var transactionsPublisher: AnyObservable<[TransactionDetails]> { + $transactions.eraseToAnyPublisher() + } + + var hasMoreOldTransactionsPublisher: AnyObservable { + $hasMoreOldTransactions.eraseToAnyPublisher() + } + + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( + coinId: tokenUnicID, + coreDataStack: coreDataStack, + blockchainType: richMessageType + ) + // MARK: - State - private (set) var state: WalletServiceState = .notInitiated + @Atomic private (set) var state: WalletServiceState = .notInitiated private func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { @@ -117,36 +123,18 @@ class LskWalletService: WalletService { state = newState if !silent { - NotificationCenter.default.post(name: serviceStateChanged, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.walletState: state]) + NotificationCenter.default.post( + name: serviceStateChanged, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.walletState: state] + ) } } // MARK: - Delayed KVS save - private var balanceObserver: NSObjectProtocol? + @Atomic private var balanceObserver: NSObjectProtocol? - // MARK: - Logic - convenience init(mainnet: Bool = true) { - let nodes = mainnet ? APIOptions.mainnet.nodes : APIOptions.testnet.nodes - let serviceNode = mainnet ? APIOptions.Service.mainnet.nodes : APIOptions.Service.testnet.nodes - self.init(mainnet: mainnet, nodes: nodes, serviceNode: serviceNode) - } - - convenience init(mainnet: Bool, nodes: [CommonKit.Node], services: [CommonKit.Node]) { - self.init(mainnet: mainnet, nodes: nodes.map { APINode(origin: $0.asString()) }, serviceNode: services.map { APINode(origin: $0.asString()) }) - } - - init(mainnet: Bool, nodes: [APINode], serviceNode: [APINode]) { - self.mainnet = mainnet - self.nodes = nodes - - let client = APIClient(options: APIOptions(nodes: serviceNode, nethash: mainnet ? .mainnet : .testnet, randomNode: true)) - self.serviceApi = Service(client: client, version: .v2) - - setupApi() - - // Notifications + init() { addObservers() } @@ -172,11 +160,21 @@ class LskWalletService: WalletService { .receive(on: OperationQueue.main) .sink { [weak self] _ in self?.lskWallet = nil - self?.initialBalanceCheck = false if let balanceObserver = self?.balanceObserver { NotificationCenter.default.removeObserver(balanceObserver) self?.balanceObserver = nil } + self?.coinStorage.clear() + self?.hasMoreOldTransactions = true + self?.transactions = [] + } + .store(in: &subscriptions) + } + + func addTransactionObserver() { + coinStorage.transactionsPublisher + .sink { [weak self] transactions in + self?.transactions = transactions } .store(in: &subscriptions) } @@ -210,22 +208,31 @@ class LskWalletService: WalletService { } if let balance = try? await getBalance() { - wallet.isBalanceInitialized = true let notification: Notification.Name? + let isRaised = (wallet.balance < balance) && wallet.isBalanceInitialized + if wallet.balance != balance { wallet.balance = balance notification = walletUpdatedNotification - initialBalanceCheck = false - } else if initialBalanceCheck { - initialBalanceCheck = false + } else if !wallet.isBalanceInitialized { notification = walletUpdatedNotification } else { notification = nil } + wallet.isBalanceInitialized = true + + if isRaised { + vibroService.applyVibration(.success) + } + if let notification = notification { - NotificationCenter.default.post(name: notification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet]) + NotificationCenter.default.post( + name: notification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] + ) } } @@ -258,104 +265,26 @@ class LskWalletService: WalletService { throw WalletServiceError.notLogged } - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<(fee: BigUInt, lastHeight: UInt64), Error>) in - serviceApi.getFees { result in - switch result { - case .success(response: let value): - let tempTransaction = TransactionEntity( - amount: 100000000.0, - fee: 0.00141, - nonce: wallet.nounce, - senderPublicKey: wallet.keyPair.publicKeyString, - recipientAddress: wallet.binaryAddress - ).signed( - with: wallet.keyPair, - for: self.netHash - ) - - let feeValue = tempTransaction.getFee(with: value.data.minFeePerByte) - let fee = BigUInt(feeValue) - - continuation.resume(returning: (fee: fee, lastHeight: value.meta.lastBlockHeight)) - case .error(response: let error): - continuation.resume( - throwing: WalletServiceError.remoteServiceError( - message: error.message - ) - ) - } - } - } - } -} - -// MARK: - Nodes -extension LskWalletService { - private func initiateNodes(completion: @escaping (Bool) -> Void) { - if nodes.count > 0 { - netHash = Constants.Nethash.main - let client = APIClient(options: APIOptions(nodes: nodes, nethash: APINethash.mainnet, randomNode: true)) - self.accountApi = Accounts(client: client) - self.transactionApi = Transactions(client: client) - self.nodeApi = LiskKit.Node(client: client) - completion(true) - } else { - self.accountApi = nil - self.transactionApi = nil - self.nodeApi = nil - self.serviceApi = nil - completion(false) - } - } - - private func getAliveNodes(from nodes: [APINode], timeout: TimeInterval, completion: @escaping ([APINode]) -> Void) { - let group = DispatchGroup() - var aliveNodes = [APINode]() + let value = try await lskServiceApiService.requestServiceApi { api, completion in + api.getFees(completionHandler: completion) + }.get() - for node in nodes { - if let url = URL(string: "\(node.origin)/api/node/status") { - - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.timeoutInterval = timeout - - group.enter() // Enter 1 - - AF.request(request).responseData { response in - defer { group.leave() } // Leave 1 - - switch response.result { - case .success: - aliveNodes.append(node) - - case .failure: - break - } - } - } - } + let tempTransaction = TransactionEntity( + amount: 100000000.0, + fee: 0.00141, + nonce: wallet.nounce, + senderPublicKey: wallet.keyPair.publicKeyString, + recipientAddressBase32: wallet.address, + recipientAddressBinary: wallet.binaryAddress + ).signed( + with: wallet.keyPair, + for: self.netHash + ) - group.notify(queue: defaultDispatchQueue) { - completion(aliveNodes) - } - } - - func setupApi() { - if mainnet { - let group = DispatchGroup() - group.enter() - - initiateNodes { _ in - group.leave() - } - - group.wait() - } else { - netHash = Constants.Nethash.test - accountApi = Accounts(client: .testnet) - transactionApi = Transactions(client: .testnet) - nodeApi = LiskKit.Node(client: .testnet) - } + let feeValue = tempTransaction.getFee(with: value.data.minFeePerByte) + let fee = BigUInt(feeValue) + + return (fee: fee, lastHeight: value.meta.lastBlockHeight) } } @@ -382,6 +311,12 @@ extension LskWalletService: InitiatedWithPassphraseService { // MARK: 3. Update let wallet = LskWallet(address: address, keyPair: keyPair, nounce: "", isNewApi: true) self.lskWallet = wallet + + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] + ) } catch { print("\(error)") throw WalletServiceError.accountNotFound @@ -407,7 +342,6 @@ extension LskWalletService: InitiatedWithPassphraseService { } } - service.initialBalanceCheck = true service.setState(.upToDate, silent: true) Task { @@ -485,7 +419,12 @@ extension LskWalletService: SwinjectDependentService { accountService = container.resolve(AccountService.self) apiService = container.resolve(ApiService.self) dialogService = container.resolve(DialogService.self) - router = container.resolve(Router.self) + lskServiceApiService = container.resolve(LskServiceApiService.self) + lskNodeApiService = container.resolve(LskNodeApiService.self) + vibroService = container.resolve(VibroService.self) + coreDataStack = container.resolve(CoreDataStack.self) + + addTransactionObserver() } } @@ -500,70 +439,59 @@ extension LskWalletService { } func getBalance(address: String) async throws -> Decimal { - guard - let accountApi = accountApi, - let address = LiskKit.Crypto.getBinaryAddressFromBase32(address) - else { + guard let address = LiskKit.Crypto.getBinaryAddressFromBase32(address) else { throw WalletServiceError.notLogged } - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - accountApi.accounts(address: address) { [weak lskWallet] response in - switch response { - case .success(response: let response): - if lskWallet?.binaryAddress == address { - lskWallet?.nounce = response.data.nonce - } - - let balance = BigUInt(response.data.balance ?? "0") ?? BigUInt(0) - continuation.resume( - returning: balance.asDecimal( - exponent: LskWalletService.currencyExponent - ) - ) - - case .error(response: let error): - if error == .noNetwork { - continuation.resume(throwing: WalletServiceError.networkError) - } else if error.code == 404 { - continuation.resume(returning: .zero) - } else { - continuation.resume( - throwing: WalletServiceError.remoteServiceError( - message: error.message - ) - ) - } - } + let result = await lskNodeApiService.requestAccountsApi { api, completion in + api.accounts(address: address, completionHandler: completion) + } + + switch result { + case let .success(response): + if lskWallet?.binaryAddress == address { + lskWallet?.nounce = response.data.nonce } + + let balance = BigUInt(response.data.balance ?? "0") ?? BigUInt(0) + return balance.asDecimal(exponent: LskWalletService.currencyExponent) + case let .failure(error): + guard + case let .remoteServiceError(_, lskError) = error, + let lskError = lskError as? APIError, + [404, 500].contains(lskError.code) + else { throw error } + + return .zero } } func handleAccountSuccess(with balance: String?, completion: @escaping (WalletServiceResult) -> Void) { let balance = BigUInt(balance ?? "0") ?? BigUInt(0) - completion(.success(result: balance.asDecimal(exponent: LskWalletService.currencyExponent))) + completion(.success(balance.asDecimal(exponent: LskWalletService.currencyExponent))) } func handleAccountError(with error: APIError, completion: @escaping (WalletServiceResult) -> Void) { if error == .noNetwork { - completion(.failure(error: .networkError)) + completion(.failure(.networkError)) } else { - completion(.failure(error: .remoteServiceError(message: error.message))) + completion(.failure(.remoteServiceError(message: error.message))) } } func getLskAddress(byAdamandAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { Task { - await apiService.get( + let result = await apiService.get( key: LskWalletService.kvsAddress, - sender: address, - completion: completion + sender: address ) + + completion(result) } } func getWalletAddress(byAdamantAddress address: String) async throws -> String { do { - let result = try await apiService.get(key: LskWalletService.kvsAddress, sender: address) + let result = try await apiService.get(key: LskWalletService.kvsAddress, sender: address).get() guard let result = result else { throw WalletServiceError.walletNotInitiated @@ -575,6 +503,22 @@ extension LskWalletService { ) } } + + func loadTransactions(offset: Int, limit: Int) async throws -> Int { + let trs = try await getTransactions(offset: UInt(offset), limit: UInt(limit)) + + guard trs.count > 0 else { + hasMoreOldTransactions = false + return .zero + } + + coinStorage.append(trs) + return trs.count + } + + func getLocalTransactionHistory() -> [TransactionDetails] { + transactions + } } // MARK: - KVS @@ -595,20 +539,20 @@ extension LskWalletService { } Task { - await apiService.store( + let result = await apiService.store( key: LskWalletService.kvsAddress, value: lskAddress, type: .keyValue, sender: adamant.address, keypair: keypair - ) { result in - switch result { - case .success: - completion(.success) - - case .failure(let error): - completion(.failure(error: .apiError(error))) - } + ) + + switch result { + case .success: + completion(.success) + + case .failure(let error): + completion(.failure(error: .apiError(error))) } } } @@ -616,29 +560,21 @@ extension LskWalletService { // MARK: - Transactions extension LskWalletService { - func getTransactions(offset: UInt) async throws -> [Transactions.TransactionModel] { - guard let address = self.lskWallet?.address, - let transactionApi = serviceApi - else { + func getTransactions(offset: UInt, limit: UInt = 100) async throws -> [Transactions.TransactionModel] { + guard let address = self.lskWallet?.address else { throw WalletServiceError.internalError(message: "LSK Wallet: not found", error: nil) } - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<[Transactions.TransactionModel], Error>) in - transactionApi.transactions( + return try await lskServiceApiService.requestServiceApi { api, completion in + api.transactions( + ownerAddress: address, senderIdOrRecipientId: address, - limit: 100, + limit: limit, offset: offset, - sort: APIRequest.Sort("timestamp", direction: .descending) - ) { (response) in - switch response { - case .success(response: let result): - continuation.resume(returning: result) - - case .error(response: let error): - continuation.resume(throwing: WalletServiceError.remoteServiceError(message: error.message)) - } - } - } + sort: APIRequest.Sort("timestamp", direction: .descending), + completionHandler: completion + ) + }.get() } func getTransaction(by hash: String) async throws -> Transactions.TransactionModel { @@ -646,36 +582,28 @@ extension LskWalletService { throw ApiServiceError.internalError(message: "No hash", error: nil) } - guard let api = serviceApi else { - throw ApiServiceError.internalError(message: "Problem with accessing LSK nodes, try later", error: nil) - } + let ownerAddress = wallet?.address - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - api.transactions(id: hash, limit: 1, offset: 0) { (response) in - switch response { - case .success(response: let result): - if let transaction = result.first { - continuation.resume(returning: transaction) - } else { - continuation.resume(throwing: WalletServiceError.remoteServiceError(message: "No transaction") - ) - } - case .error(response: let error): - if error == .noNetwork { - continuation.resume( - throwing: ApiServiceError.networkError(error: error) - ) - } else { - continuation.resume( - throwing: WalletServiceError.remoteServiceError( - message: error.message - ) - ) - } - } - } + let result = try await lskServiceApiService.requestServiceApi { api, completion in + api.transactions( + ownerAddress: ownerAddress, + id: hash, + limit: 1, + offset: 0, + completionHandler: completion + ) + }.get() + + if let transaction = result.first { + return transaction + } else { + throw WalletServiceError.remoteServiceError(message: "No transaction") } } + + func updateStatus(for id: String, status: TransactionStatus?) { + coinStorage.updateStatus(for: id, status: status) + } } // MARK: - PrivateKey generator diff --git a/Adamant/Wallets/Lisk/LskWalletViewController.swift b/Adamant/Modules/Wallets/Lisk/LskWalletViewController.swift similarity index 93% rename from Adamant/Wallets/Lisk/LskWalletViewController.swift rename to Adamant/Modules/Wallets/Lisk/LskWalletViewController.swift index 053c38075..80a91c5da 100644 --- a/Adamant/Wallets/Lisk/LskWalletViewController.swift +++ b/Adamant/Modules/Wallets/Lisk/LskWalletViewController.swift @@ -15,7 +15,7 @@ extension String.adamant { static let sendLsk = String.localized("AccountTab.Row.SendLsk", comment: "Account tab: 'Send LSK tokens' button") } -class LskWalletViewController: WalletViewControllerBase { +final class LskWalletViewController: WalletViewControllerBase { // MARK: Lifecycle override func viewDidLoad() { diff --git a/Adamant/Modules/Wallets/Mappers/SimpleTransactionDetails+Hashable.swift b/Adamant/Modules/Wallets/Mappers/SimpleTransactionDetails+Hashable.swift new file mode 100644 index 000000000..8e51c6956 --- /dev/null +++ b/Adamant/Modules/Wallets/Mappers/SimpleTransactionDetails+Hashable.swift @@ -0,0 +1,28 @@ +// +// SimpleTransactionDetails+Hashable.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 19.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit + +extension Sequence where Element == SimpleTransactionDetails { + func wrappedByHashableId() -> [HashableIDWrapper] { + var identifierTable: [String: Int] = [:] + var result: [HashableIDWrapper] = [] + + forEach { item in + let index = identifierTable[item.txId] ?? .zero + identifierTable[item.txId] = index + 1 + + result.append(.init( + identifier: .init(identifier: item.txId, index: index), + value: item + )) + } + + return result + } +} diff --git a/Adamant/Modules/Wallets/Models/AdamantTransactionDetails.swift b/Adamant/Modules/Wallets/Models/AdamantTransactionDetails.swift new file mode 100644 index 000000000..6ee5b401a --- /dev/null +++ b/Adamant/Modules/Wallets/Models/AdamantTransactionDetails.swift @@ -0,0 +1,59 @@ +// +// AdamantTransactionDetails.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 24.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +protocol AdamantTransactionDetails: TransactionDetails { + /// The identifier of the transaction. + var txId: String { get } + + /// The sender of the transaction. + var senderAddress: String { get } + + /// The reciver of the transaction. + var recipientAddress: String { get } + + /// The date the transaction was sent. + var dateValue: Date? { get } + + /// The amount of currency that was sent. + var amountValue: Decimal? { get } + + /// The amount of fee that taken for transaction process. + var feeValue: Decimal? { get } + + /// The confirmations of the transaction. + var confirmationsValue: String? { get } + + var blockHeight: UInt64? { get } + + /// The block of the transaction. + var blockValue: String? { get } + + var isOutgoing: Bool { get } + + var transactionStatus: TransactionStatus? { get } + + var defaultCurrencySymbol: String? { get } + + var feeCurrencySymbol: String? { get } + + func summary( + with url: String?, + currentValue: String?, + valueAtTimeTxn: String? + ) -> String + + var partnerName: String? { get } + + var comment: String? { get } + + var showToChat: Bool? { get } + + var chatRoom: Chatroom? { get } +} diff --git a/Adamant/Wallets/TransactionDetails.swift b/Adamant/Modules/Wallets/Models/TransactionDetails.swift similarity index 100% rename from Adamant/Wallets/TransactionDetails.swift rename to Adamant/Modules/Wallets/Models/TransactionDetails.swift diff --git a/Adamant/Wallets/TransactionDetailsViewControllerBase.swift b/Adamant/Modules/Wallets/TransactionDetailsViewControllerBase.swift similarity index 98% rename from Adamant/Wallets/TransactionDetailsViewControllerBase.swift rename to Adamant/Modules/Wallets/TransactionDetailsViewControllerBase.swift index d7b8d6ee9..624649463 100644 --- a/Adamant/Wallets/TransactionDetailsViewControllerBase.swift +++ b/Adamant/Modules/Wallets/TransactionDetailsViewControllerBase.swift @@ -147,6 +147,7 @@ class TransactionDetailsViewControllerBase: FormViewController { let currencyInfo: CurrencyInfoService let addressBookService: AddressBookService let accountService: AccountService + let walletService: WalletService? // MARK: - Properties @@ -155,6 +156,13 @@ class TransactionDetailsViewControllerBase: FormViewController { if !isFiatSet { self.updateFiat() } + + guard let id = transaction?.txId else { return } + + walletService?.updateStatus( + for: id, + status: transaction?.transactionStatus + ) } } private lazy var dateFormatter: DateFormatter = { @@ -164,7 +172,7 @@ class TransactionDetailsViewControllerBase: FormViewController { return dateFormatter }() - static let awaitingValueString = "⏱" + static let awaitingValueString = TransactionStatus.notInitiated.localized private lazy var currencyFormatter: NumberFormatter = { return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) @@ -245,12 +253,14 @@ class TransactionDetailsViewControllerBase: FormViewController { dialogService: DialogService, currencyInfo: CurrencyInfoService, addressBookService: AddressBookService, - accountService: AccountService + accountService: AccountService, + walletService: WalletService? ) { self.dialogService = dialogService self.currencyInfo = currencyInfo self.addressBookService = addressBookService self.accountService = accountService + self.walletService = walletService super.init(style: .grouped) } diff --git a/Adamant/Modules/Wallets/TransactionTableViewCell.swift b/Adamant/Modules/Wallets/TransactionTableViewCell.swift new file mode 100644 index 000000000..ed90ed456 --- /dev/null +++ b/Adamant/Modules/Wallets/TransactionTableViewCell.swift @@ -0,0 +1,154 @@ +// +// TransactionTableViewCell.swift +// Adamant +// +// Created by Anokhov Pavel on 08.01.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import CommonKit + +final class TransactionTableViewCell: UITableViewCell { + enum TransactionType { + case income, outcome, myself + + var imageTop: UIImage { + switch self { + case .income: return .asset(named: "transfer-in_top") ?? .init() + case .outcome: return .asset(named: "transfer-out_top") ?? .init() + case .myself: return .asset(named: "transfer-in_top")?.withTintColor(.lightGray) ?? .init() + } + } + + var imageBottom: UIImage { + switch self { + case .income: return .asset(named: "transfer-in_bot") ?? .init() + case .outcome: return .asset(named: "transfer-out_bot") ?? .init() + case .myself: return .asset(named: "transfer-self_bot") ?? .init() + } + } + + var bottomTintColor: UIColor { + switch self { + case .income: return UIColor.adamant.transferIncomeIconBackground + case .outcome: return UIColor.adamant.transferOutcomeIconBackground + case .myself: return UIColor.adamant.transferIncomeIconBackground + } + } + } + + // MARK: - Constants + + static let cellHeightCompact: CGFloat = 90.0 + static let cellFooterLoadingCompact: CGFloat = 30.0 + static let cellHeightFull: CGFloat = 100.0 + + // MARK: - IBOutlets + + @IBOutlet weak var topImageView: UIImageView! + @IBOutlet weak var bottomImageView: UIImageView! + @IBOutlet weak var accountLabel: UILabel! + @IBOutlet weak var addressLabel: UILabel! + @IBOutlet weak var ammountLabel: UILabel! + @IBOutlet weak var dateLabel: UILabel! + + // MARK: - Properties + + var transactionType: TransactionType = .income { + didSet { + topImageView.image = transactionType.imageTop + bottomImageView.image = transactionType.imageBottom + bottomImageView.tintColor = transactionType.bottomTintColor + } + } + + var currencySymbol: String? + + var transaction: SimpleTransactionDetails? { + didSet { + updateUI() + } + } + + // MARK: - Initializers + + override func awakeFromNib() { + transactionType = .income + } + + func updateUI() { + guard let transaction = transaction else { return } + + let partnerId = transaction.isOutgoing + ? transaction.recipientAddress + : transaction.senderAddress + + let transactionType: TransactionTableViewCell.TransactionType + if transaction.recipientAddress == transaction.senderAddress { + transactionType = .myself + } else if transaction.isOutgoing { + transactionType = .outcome + } else { + transactionType = .income + } + + self.transactionType = transactionType + + backgroundColor = .clear + accountLabel.tintColor = UIColor.adamant.primary + ammountLabel.tintColor = UIColor.adamant.primary + + dateLabel.textColor = transaction.transactionStatus?.color ?? .adamant.secondary + + switch transaction.transactionStatus { + case .success, .inconsistent: + if let date = transaction.dateValue { + dateLabel.text = date.humanizedDateTime() + } else { + dateLabel.text = nil + } + case .notInitiated: + dateLabel.text = TransactionDetailsViewControllerBase.awaitingValueString + case .failed: + dateLabel.text = TransactionStatus.failed.localized + case .pending, .registered: + dateLabel.text = TransactionStatus.pending.localized + default: + dateLabel.text = TransactionDetailsViewControllerBase.awaitingValueString + } + + if let partnerName = transaction.partnerName { + accountLabel.text = partnerName + addressLabel.text = partnerId + addressLabel.lineBreakMode = .byTruncatingMiddle + + if addressLabel.isHidden { + addressLabel.isHidden = false + } + } else { + accountLabel.text = partnerId + + if !addressLabel.isHidden { + addressLabel.isHidden = true + } + } + + let amount = transaction.amountValue ?? .zero + ammountLabel.text = AdamantBalanceFormat.full.format(amount, withCurrencySymbol: currencySymbol) + } +} + +// MARK: - TransactionStatus UI +private extension TransactionStatus { + var color: UIColor { + switch self { + case .failed: + return .adamant.danger + case .pending, .registered: + return .adamant.alert + default: + return .adamant.secondary + } + } +} diff --git a/Adamant/Wallets/TransactionTableViewCell.xib b/Adamant/Modules/Wallets/TransactionTableViewCell.xib similarity index 100% rename from Adamant/Wallets/TransactionTableViewCell.xib rename to Adamant/Modules/Wallets/TransactionTableViewCell.xib diff --git a/Adamant/Wallets/TransactionsListViewControllerBase.swift b/Adamant/Modules/Wallets/TransactionsListViewControllerBase.swift similarity index 54% rename from Adamant/Wallets/TransactionsListViewControllerBase.swift rename to Adamant/Modules/Wallets/TransactionsListViewControllerBase.swift index 5becbda9a..ad6cf10f3 100644 --- a/Adamant/Wallets/TransactionsListViewControllerBase.swift +++ b/Adamant/Modules/Wallets/TransactionsListViewControllerBase.swift @@ -9,6 +9,7 @@ import UIKit import CoreData import CommonKit +import Combine extension String.adamant { struct transactionList { @@ -21,6 +22,8 @@ extension String.adamant { } } +private typealias TransactionsDiffableDataSource = UITableViewDiffableDataSource + // Extensions for a generic classes is limited, so delegates implemented right in class declaration class TransactionsListViewControllerBase: UIViewController { let cellIdentifierFull = "cf" @@ -37,13 +40,29 @@ class TransactionsListViewControllerBase: UIViewController { return refreshControl }() + // MARK: - Dependencies + + var walletService: WalletService! + var dialogService: DialogService! + + // MARK: - Proprieties + var taskManager = TaskManager() var isNeedToLoadMoore = true - var isBusy = true + var isBusy = false + var subscriptions = Set() + var transactions: [SimpleTransactionDetails] = [] private(set) lazy var loadingView = LoadingView() + private var limit = 25 + private var offset = 0 + + private lazy var dataSource = TransactionsDiffableDataSource(tableView: tableView, cellProvider: makeCell) + + var currencySymbol: String { walletService.tokenSymbol } + // MARK: - IBOutlets @IBOutlet weak var tableView: UITableView! @IBOutlet weak var emptyLabel: UILabel! @@ -57,30 +76,12 @@ class TransactionsListViewControllerBase: UIViewController { navigationItem.title = String.adamant.transactionList.title emptyLabel.text = String.adamant.transactionList.noTransactionYet - // MARK: Configure tableView - let nib = UINib.init(nibName: "TransactionTableViewCell", bundle: nil) - tableView.register(nib, forCellReuseIdentifier: cellIdentifierFull) - tableView.register(nib, forCellReuseIdentifier: cellIdentifierCompact) - tableView.dataSource = self - tableView.delegate = self - tableView.refreshControl = refreshControl - tableView.tableHeaderView = UIView() - - // MARK: Notifications - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedIn, object: nil, queue: OperationQueue.main) { [weak self] _ in - self?.reloadData() - } - - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: OperationQueue.main) { [weak self] _ in - self?.reloadData() - } - - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAddressBookService.addressBookUpdated, object: nil, queue: OperationQueue.main) { [weak self] _ in - self?.reloadData() - } - + update(walletService.getLocalTransactionHistory()) + configureTableView() setColors() configureLayout() + addObservers() + handleRefresh() } override func viewWillAppear(_ animated: Bool) { @@ -96,6 +97,80 @@ class TransactionsListViewControllerBase: UIViewController { } } + func addObservers() { + NotificationCenter.default + .publisher(for: .AdamantAddressBookService.addressBookUpdated, object: nil) + .receive(on: OperationQueue.main) + .sink { [weak self] _ in + self?.reloadData() + } + .store(in: &subscriptions) + + NotificationCenter.default + .publisher(for: .AdamantAccountService.userLoggedOut, object: nil) + .receive(on: OperationQueue.main) + .sink { [weak self] _ in + self?.reloadData() + } + .store(in: &subscriptions) + + NotificationCenter.default + .publisher(for: .AdamantAccountService.userLoggedIn, object: nil) + .receive(on: OperationQueue.main) + .sink { [weak self] _ in + self?.reloadData() + } + .store(in: &subscriptions) + + walletService.transactionsPublisher + .receive(on: OperationQueue.main) + .sink { [weak self] transactions in + self?.update(transactions) + } + .store(in: &subscriptions) + + walletService.hasMoreOldTransactionsPublisher + .sink { [weak self] isNeedToLoadMoore in + self?.isNeedToLoadMoore = isNeedToLoadMoore + } + .store(in: &subscriptions) + } + + func configureTableView() { + let nib = UINib.init(nibName: "TransactionTableViewCell", bundle: nil) + tableView.register(nib, forCellReuseIdentifier: cellIdentifierFull) + tableView.register(nib, forCellReuseIdentifier: cellIdentifierCompact) + tableView.delegate = self + tableView.refreshControl = refreshControl + tableView.tableHeaderView = UIView() + } + + @MainActor + func update(_ transactions: [TransactionDetails]) { + let transactions = transactions.map { + SimpleTransactionDetails($0) + } + + update(transactions) + } + + @MainActor + func update(_ transactions: [SimpleTransactionDetails]) { + self.transactions = transactions.sorted( + by: { ($0.dateValue ?? Date()) > ($1.dateValue ?? Date()) } + ) + + let list = self.transactions + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.zero]) + snapshot.appendItems(list) + snapshot.reconfigureItems(list) + dataSource.apply(snapshot, animatingDifferences: false) + + guard !isBusy else { return } + self.updateLoadingView(isHidden: true) + } + // MARK: - Other private func setColors() { @@ -112,6 +187,11 @@ class TransactionsListViewControllerBase: UIViewController { } } + func presentLoadingViewIfNeeded() { + guard transactions.count == 0 else { return } + updateLoadingView(isHidden: false) + } + func updateLoadingView(isHidden: Bool) { loadingView.isHidden = isHidden if !isHidden { @@ -121,6 +201,38 @@ class TransactionsListViewControllerBase: UIViewController { } } + @MainActor + func loadData(silent: Bool) { + loadData(offset: offset, silent: true) + } + + @MainActor + func loadData(offset: Int, silent: Bool) { + guard !isBusy else { return } + isBusy = true + Task { + do { + let count = try await walletService.loadTransactions( + offset: offset, + limit: limit + ) + self.offset += count + } catch { + isNeedToLoadMoore = false + + if !silent { + dialogService.showRichError(error: error) + } + } + + isBusy = false + emptyLabel.isHidden = self.transactions.count > 0 + stopBottomIndicator() + refreshControl.endRefreshing() + updateLoadingView(isHidden: true) + }.stored(in: taskManager) + } + // MARK: - To override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { @@ -131,12 +243,21 @@ class TransactionsListViewControllerBase: UIViewController { return TransactionTableViewCell.cellHeightCompact } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - return UITableViewCell(style: .default, reuseIdentifier: "cell") - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 0 + private func makeCell( + tableView: UITableView, + indexPath: IndexPath, + model: SimpleTransactionDetails + ) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierCompact, for: indexPath) as! TransactionTableViewCell + + cell.accessoryType = .disclosureIndicator + cell.separatorInset = indexPath.row == transactions.count - 1 + ? .zero + : UITableView.defaultTransactionsSeparatorInset + + cell.currencySymbol = currencySymbol + cell.transaction = model + return cell } func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { @@ -156,72 +277,18 @@ class TransactionsListViewControllerBase: UIViewController { } @objc func handleRefresh() { - - } - - func loadData(silent: Bool) { - + presentLoadingViewIfNeeded() + emptyLabel.isHidden = true + loadData(offset: .zero, silent: true) } func reloadData() { } - - var currencySymbol: String? } // MARK: - UITableViewDataSource, UITableViewDelegate -extension TransactionsListViewControllerBase: UITableViewDataSource, UITableViewDelegate { - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - // MARK: Cells - - func configureCell( - _ cell: TransactionTableViewCell, - isOutgoing: Bool, - partnerId: String, - partnerName: String?, - amount: Decimal, - date: Date? - ) { - cell.backgroundColor = .clear - cell.accountLabel.tintColor = UIColor.adamant.primary - cell.ammountLabel.tintColor = UIColor.adamant.primary - cell.dateLabel.tintColor = UIColor.adamant.secondary - - if isOutgoing { - cell.transactionType = .outcome - } else { - cell.transactionType = .income - } - - if let partnerName = partnerName { - cell.accountLabel.text = partnerName - cell.addressLabel.text = partnerId - cell.addressLabel.lineBreakMode = .byTruncatingMiddle - - if cell.addressLabel.isHidden { - cell.addressLabel.isHidden = false - } - } else { - cell.accountLabel.text = partnerId - - if !cell.addressLabel.isHidden { - cell.addressLabel.isHidden = true - } - } - - cell.ammountLabel.text = AdamantBalanceFormat.full.format(amount, withCurrencySymbol: currencySymbol) - - if let date = date { - cell.dateLabel.text = date.humanizedDateTime() - } else { - cell.dateLabel.text = nil - } - } - +extension TransactionsListViewControllerBase: UITableViewDelegate { func bottomIndicatorView() -> UIActivityIndicatorView { var activityIndicatorView = UIActivityIndicatorView() @@ -255,3 +322,15 @@ extension TransactionsListViewControllerBase: UITableViewDataSource, UITableView tableView.tableFooterView = nil } } + +// MARK: - TransactionStatus UI +private extension TransactionStatus { + var color: UIColor { + switch self { + case .failed: return .adamant.danger + case .notInitiated, .inconsistent, .noNetwork, .noNetworkFinal, .pending, .registered: + return .adamant.alert + case .success: return .adamant.secondary + } + } +} diff --git a/Adamant/Wallets/TransactionsListViewControllerBase.xib b/Adamant/Modules/Wallets/TransactionsListViewControllerBase.xib similarity index 100% rename from Adamant/Wallets/TransactionsListViewControllerBase.xib rename to Adamant/Modules/Wallets/TransactionsListViewControllerBase.xib diff --git a/Adamant/Wallets/TransferViewControllerBase+Alert.swift b/Adamant/Modules/Wallets/TransferViewControllerBase+Alert.swift similarity index 100% rename from Adamant/Wallets/TransferViewControllerBase+Alert.swift rename to Adamant/Modules/Wallets/TransferViewControllerBase+Alert.swift diff --git a/Adamant/Wallets/TransferViewControllerBase+QR.swift b/Adamant/Modules/Wallets/TransferViewControllerBase+QR.swift similarity index 95% rename from Adamant/Wallets/TransferViewControllerBase+QR.swift rename to Adamant/Modules/Wallets/TransferViewControllerBase+QR.swift index 71f4abd0f..9d05a74dd 100644 --- a/Adamant/Wallets/TransferViewControllerBase+QR.swift +++ b/Adamant/Modules/Wallets/TransferViewControllerBase+QR.swift @@ -105,16 +105,16 @@ extension TransferViewControllerBase: UINavigationControllerDelegate, UIImagePic } let codes = EFQRCode.recognize(cgImage) - if codes.count > 0 { - for aCode in codes { - if handleRawAddress(aCode) { - return - } - } - - dialogService.showWarning(withMessage: String.adamant.newChat.wrongQrError) - } else { + + if codes.contains(where: handleRawAddress) { + vibroService.applyVibration(.medium) + return + } + + if codes.isEmpty { dialogService.showWarning(withMessage: String.adamant.login.noQrError) + } else { + dialogService.showWarning(withMessage: String.adamant.newChat.wrongQrError) } } } @@ -123,6 +123,7 @@ extension TransferViewControllerBase: UINavigationControllerDelegate, UIImagePic extension TransferViewControllerBase: QRCodeReaderViewControllerDelegate { func reader(_ reader: QRCodeReaderViewController, didScanResult result: QRCodeReaderResult) { if handleRawAddress(result.value) { + vibroService.applyVibration(.medium) dismiss(animated: true, completion: nil) } else { dialogService.showWarning(withMessage: String.adamant.newChat.wrongQrError) diff --git a/Adamant/Wallets/TransferViewControllerBase.swift b/Adamant/Modules/Wallets/TransferViewControllerBase.swift similarity index 99% rename from Adamant/Wallets/TransferViewControllerBase.swift rename to Adamant/Modules/Wallets/TransferViewControllerBase.swift index 79b4698c1..35552c5ee 100644 --- a/Adamant/Wallets/TransferViewControllerBase.swift +++ b/Adamant/Modules/Wallets/TransferViewControllerBase.swift @@ -133,10 +133,11 @@ class TransferViewControllerBase: FormViewController { let accountService: AccountService let accountsProvider: AccountsProvider let dialogService: DialogService - let router: Router + let screensFactory: ScreensFactory let currencyInfoService: CurrencyInfoService var increaseFeeService: IncreaseFeeService var chatsProvider: ChatsProvider + let vibroService: VibroService // MARK: - Properties @@ -283,17 +284,19 @@ class TransferViewControllerBase: FormViewController { accountService: AccountService, accountsProvider: AccountsProvider, dialogService: DialogService, - router: Router, + screensFactory: ScreensFactory, currencyInfoService: CurrencyInfoService, - increaseFeeService: IncreaseFeeService + increaseFeeService: IncreaseFeeService, + vibroService: VibroService ) { self.accountService = accountService self.accountsProvider = accountsProvider self.dialogService = dialogService - self.router = router + self.screensFactory = screensFactory self.currencyInfoService = currencyInfoService self.increaseFeeService = increaseFeeService self.chatsProvider = chatsProvider + self.vibroService = vibroService super.init(nibName: nil, bundle: nil) } diff --git a/Adamant/Wallets/WalletAccount.swift b/Adamant/Modules/Wallets/WalletAccount.swift similarity index 100% rename from Adamant/Wallets/WalletAccount.swift rename to Adamant/Modules/Wallets/WalletAccount.swift diff --git a/Adamant/Modules/Wallets/WalletApiService.swift b/Adamant/Modules/Wallets/WalletApiService.swift new file mode 100644 index 000000000..097854e0b --- /dev/null +++ b/Adamant/Modules/Wallets/WalletApiService.swift @@ -0,0 +1,15 @@ +// +// WalletApiService.swift +// Adamant +// +// Created by Andrew G on 20.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +protocol WalletApiService { + var preferredNodeIds: [UUID] { get } + + func healthCheck() +} diff --git a/Adamant/Wallets/WalletService.swift b/Adamant/Modules/Wallets/WalletService.swift similarity index 84% rename from Adamant/Wallets/WalletService.swift rename to Adamant/Modules/Wallets/WalletService.swift index 351eb9a78..45ef7f05e 100644 --- a/Adamant/Wallets/WalletService.swift +++ b/Adamant/Modules/Wallets/WalletService.swift @@ -20,10 +20,7 @@ enum WalletServiceSimpleResult { case failure(error: WalletServiceError) } -enum WalletServiceResult { - case success(result: T) - case failure(error: WalletServiceError) -} +typealias WalletServiceResult = Result // MARK: - Errors @@ -34,7 +31,7 @@ enum WalletServiceError: Error { case accountNotFound case walletNotInitiated case invalidAmount(Decimal) - case remoteServiceError(message: String) + case remoteServiceError(message: String, error: Error?) case apiError(ApiServiceError) case internalError(message: String, error: Error?) case transactionNotFound(reason: String) @@ -60,7 +57,7 @@ extension WalletServiceError: RichError { case .walletNotInitiated: return .localized("WalletServices.SharedErrors.WalletNotInitiated", comment: "Wallet Services: Shared error, user has not yet initiated a specific wallet.") - case .remoteServiceError(let message): + case .remoteServiceError(let message, let error): return String.adamant.sharedErrors.remoteServerError(message: message) case .apiError(let error): @@ -104,6 +101,41 @@ extension WalletServiceError: RichError { return error.level } } + + static func internalError(_ error: InternalAPIError) -> Self { + .internalError(message: error.localizedDescription, error: error) + } + + static func remoteServiceError(message: String? = nil, error: Error? = nil) -> Self { + .remoteServiceError( + message: message ?? error?.localizedDescription ?? .empty, + error: error + ) + } +} + +extension WalletServiceError: HealthCheckableError { + var isNetworkError: Bool { + switch self { + case .networkError: + return true + default: + return false + } + } + + var isRequestCancelledError: Bool { + switch self { + case .requestCancelled: + return true + default: + return false + } + } + + static func noEndpointsError(coin: String) -> WalletServiceError { + .apiError(.noEndpointsError(coin: coin)) + } } extension ApiServiceError { @@ -121,7 +153,7 @@ extension ApiServiceError { case .requestCancelled: return .requestCancelled - case .serverError, .internalError, .commonError: + case .serverError, .internalError, .commonError, .noEndpointsAvailable: return .apiError(self) } } @@ -208,7 +240,7 @@ protocol WalletService: AnyObject { var tokenName: String { get } var tokenLogo: UIImage { get } var tokenUnicID: String { get } - var tokenNetworkSymbol: String { get } + static var tokenNetworkSymbol: String { get } var consistencyMaxTime: Double { get } var tokenContract: String { get } var minBalance: Decimal { get } @@ -217,6 +249,14 @@ protocol WalletService: AnyObject { var defaultOrdinalLevel: Int? { get } var richMessageType: String { get } + var transactionsPublisher: AnyObservable<[TransactionDetails]> { + get + } + + var hasMoreOldTransactionsPublisher: AnyObservable { + get + } + // MARK: Notifications /// Wallet updated. @@ -237,13 +277,13 @@ protocol WalletService: AnyObject { // MARK: Logic func update() - // MARK: Account UI - var walletViewController: WalletViewController { get } - // MARK: Tools func validate(address: String) -> AddressValidationResult func getWalletAddress(byAdamantAddress address: String) async throws -> String func getBalance(address: String) async throws -> Decimal + func loadTransactions(offset: Int, limit: Int) async throws -> Int + func getLocalTransactionHistory() -> [TransactionDetails] + func updateStatus(for id: String, status: TransactionStatus?) } protocol SwinjectDependentService: WalletService { @@ -256,10 +296,6 @@ protocol InitiatedWithPassphraseService: WalletService { func setInitiationFailed(reason: String) } -protocol WalletServiceWithTransfers: WalletService { - func transferListViewController() -> UIViewController -} - // MARK: Send protocol WalletServiceWithSend: WalletService { @@ -277,7 +313,6 @@ protocol WalletServiceWithSend: WalletService { var isSupportIncreaseFee: Bool { get } var isIncreaseFeeEnabled: Bool { get } var defaultIncreaseFee: Decimal { get } - func transferViewController() -> UIViewController } extension WalletServiceWithSend { @@ -313,7 +348,7 @@ protocol WalletServiceSimpleSend: WalletServiceWithSend { amount: Decimal, comments: String, replyToMessageId: String? - ) async throws -> TransactionDetails + ) async throws -> AdamantTransactionDetails } protocol WalletServiceTwoStepSend: WalletServiceWithSend { diff --git a/Adamant/Wallets/WalletViewControllerBase.swift b/Adamant/Modules/Wallets/WalletViewControllerBase.swift similarity index 85% rename from Adamant/Wallets/WalletViewControllerBase.swift rename to Adamant/Modules/Wallets/WalletViewControllerBase.swift index 3214ad88c..bad99840c 100644 --- a/Adamant/Wallets/WalletViewControllerBase.swift +++ b/Adamant/Modules/Wallets/WalletViewControllerBase.swift @@ -50,6 +50,7 @@ class WalletViewControllerBase: FormViewController, WalletViewController { var dialogService: DialogService! var currencyInfoService: CurrencyInfoService! var accountService: AccountService! + var screensFactory: ScreensFactory! // MARK: - Properties, WalletViewController @@ -123,26 +124,26 @@ class WalletViewControllerBase: FormViewController, WalletViewController { cell.height = { height } } - if service is WalletServiceWithTransfers { - balanceRow.cell.selectionStyle = .gray - balanceRow.cellUpdate { (cell, _) in - cell.accessoryType = .disclosureIndicator - }.onCellSelection { [weak self] (_, _) in - guard let service = self?.service as? WalletServiceWithTransfers else { - return - } - - let vc = service.transferListViewController() - if let split = self?.splitViewController { - let details = UINavigationController(rootViewController:vc) - split.showDetailViewController(details, sender: self) - } else { - self?.navigationController?.pushViewController(vc, animated: true ) - } - - if let vc = self, let delegate = vc.delegate { - delegate.walletViewControllerSelectedRow(vc) - } + balanceRow.cell.selectionStyle = .gray + balanceRow.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, _) in + guard + let self = self, + let service = service + else { return } + + let vc = screensFactory.makeTransferListVC(service: service) + + if let split = splitViewController { + let details = UINavigationController(rootViewController:vc) + split.showDetailViewController(details, sender: self) + } else { + navigationController?.pushViewController(vc, animated: true ) + } + + if let delegate = delegate { + delegate.walletViewControllerSelectedRow(self) } } @@ -170,37 +171,33 @@ class WalletViewControllerBase: FormViewController, WalletViewController { cell.textLabel?.attributedText = label } }.onCellSelection { [weak self] (_, _) in - guard let service = self?.service as? WalletServiceWithSend else { - return - } + guard let self = self, let service = service else { return } - let vc = service.transferViewController() - if let v = vc as? TransferViewControllerBase { - v.delegate = self - if ERC20Token.supportedTokens.contains(where: { token in - return token.symbol == service.tokenSymbol - }) { - let ethWallet = self?.accountService.wallets.first { wallet in - return wallet.tokenSymbol == "ETH" - } - v.rootCoinBalance = ethWallet?.wallet?.balance + let vc = screensFactory.makeTransferVC(service: service) + vc.delegate = self + if ERC20Token.supportedTokens.contains(where: { token in + return token.symbol == service.tokenSymbol + }) { + let ethWallet = accountService.wallets.first { wallet in + return wallet.tokenSymbol == "ETH" } + vc.rootCoinBalance = ethWallet?.wallet?.balance } - if let split = self?.splitViewController { + if let split = splitViewController { let details = UINavigationController(rootViewController:vc) split.showDetailViewController(details, sender: self) } else { - if let nav = self?.navigationController { + if let nav = navigationController { nav.pushViewController(vc, animated: true) } else { vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true) + present(vc, animated: true) } } - if let vc = self, let delegate = vc.delegate { - delegate.walletViewControllerSelectedRow(vc) + if let delegate = delegate { + delegate.walletViewControllerSelectedRow(self) } } @@ -436,16 +433,20 @@ class WalletViewControllerBase: FormViewController, WalletViewController { // MARK: - TransferViewControllerDelegate extension WalletViewControllerBase: TransferViewControllerDelegate { - func transferViewController(_ viewController: TransferViewControllerBase, didFinishWithTransfer transfer: TransactionDetails?, detailsViewController: UIViewController?) { - DispatchQueue.onMainAsync { [weak self] in - if let split = self?.splitViewController { + func transferViewController( + _ viewController: TransferViewControllerBase, + didFinishWithTransfer transfer: TransactionDetails?, + detailsViewController: UIViewController? + ) { + DispatchQueue.onMainAsync { [self] in + if let split = splitViewController { if let nav = split.viewControllers.last as? UINavigationController { if let detailsViewController = detailsViewController { var viewControllers = nav.viewControllers viewControllers.removeLast() - if let service = self?.service as? WalletServiceWithTransfers { - viewControllers.append(service.transferListViewController()) + if let service = service { + viewControllers.append(screensFactory.makeTransferListVC(service: service)) } viewControllers.append(detailsViewController) @@ -456,7 +457,7 @@ extension WalletViewControllerBase: TransferViewControllerDelegate { } else { split.showDetailViewController(viewController, sender: nil) } - } else if let nav = self?.navigationController { + } else if let nav = navigationController { if let detailsViewController = detailsViewController { var viewControllers = nav.viewControllers viewControllers.removeLast() @@ -465,12 +466,12 @@ extension WalletViewControllerBase: TransferViewControllerDelegate { } else { nav.popViewController(animated: true) } - } else if self?.presentedViewController == viewController { - self?.dismiss(animated: true, completion: nil) + } else if presentedViewController == viewController { + dismiss(animated: true, completion: nil) if let detailsViewController = detailsViewController { detailsViewController.modalPresentationStyle = .overFullScreen - self?.present(detailsViewController, animated: true, completion: nil) + present(detailsViewController, animated: true, completion: nil) } } } diff --git a/Adamant/Wallets/WalletViewControllerBase.xib b/Adamant/Modules/Wallets/WalletViewControllerBase.xib similarity index 100% rename from Adamant/Wallets/WalletViewControllerBase.xib rename to Adamant/Modules/Wallets/WalletViewControllerBase.xib diff --git a/Adamant/Stories/Shared/WelcomeViewController.swift b/Adamant/Modules/Welcome/WelcomeViewController.swift similarity index 100% rename from Adamant/Stories/Shared/WelcomeViewController.swift rename to Adamant/Modules/Welcome/WelcomeViewController.swift diff --git a/Adamant/Stories/Shared/WelcomeViewController.xib b/Adamant/Modules/Welcome/WelcomeViewController.xib similarity index 100% rename from Adamant/Stories/Shared/WelcomeViewController.xib rename to Adamant/Modules/Welcome/WelcomeViewController.xib diff --git a/Adamant/ServiceProtocols/APICoreProtocol.swift b/Adamant/ServiceProtocols/APICoreProtocol.swift new file mode 100644 index 000000000..6fb495bf2 --- /dev/null +++ b/Adamant/ServiceProtocols/APICoreProtocol.swift @@ -0,0 +1,119 @@ +// +// APICoreProtocol.swift +// Adamant +// +// Created by Andrew G on 30.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation +import Alamofire +import CommonKit +import UIKit + +enum ApiCommands {} + +protocol APICoreProtocol: Actor { + func sendRequestBasic( + node: Node, + path: String, + method: HTTPMethod, + parameters: Parameters, + encoding: APIParametersEncoding + ) async -> APIResponseModel + + /// jsonParameters - arrays and dictionaries are allowed only + func sendRequestBasic( + node: Node, + path: String, + method: HTTPMethod, + jsonParameters: Any + ) async -> APIResponseModel +} + +extension APICoreProtocol { + var emptyParameters: [String: Bool] { [:] } + + func sendRequest( + node: Node, + path: String, + method: HTTPMethod, + parameters: Parameters, + encoding: APIParametersEncoding + ) async -> ApiServiceResult { + await sendRequestBasic( + node: node, + path: path, + method: method, + parameters: parameters, + encoding: encoding + ).result + } + + func sendRequestJsonResponse( + node: Node, + path: String, + method: HTTPMethod, + parameters: Parameters, + encoding: APIParametersEncoding + ) async -> ApiServiceResult { + await sendRequest( + node: node, + path: path, + method: method, + parameters: parameters, + encoding: encoding + ).flatMap { parseJSON(data: $0) } + } + + func sendRequestJsonResponse( + node: Node, + path: String + ) async -> ApiServiceResult { + await sendRequestJsonResponse( + node: node, + path: path, + method: .get, + parameters: emptyParameters, + encoding: .url + ) + } + + func sendRequest( + node: Node, + path: String + ) async -> ApiServiceResult { + await sendRequest( + node: node, + path: path, + method: .get, + parameters: emptyParameters, + encoding: .url + ) + } + + func sendRequestJsonResponse( + node: Node, + path: String, + method: HTTPMethod, + jsonParameters: Any + ) async -> ApiServiceResult { + await sendRequestBasic( + node: node, + path: path, + method: method, + jsonParameters: jsonParameters + ).result.flatMap { parseJSON(data: $0) } + } +} + +private extension APICoreProtocol { + func parseJSON(data: Data) -> ApiServiceResult { + do { + let output = try JSONDecoder().decode(JSON.self, from: data) + return .success(output) + } catch { + return .failure(.internalError(error: InternalAPIError.parsingFailed)) + } + } +} diff --git a/Adamant/ServiceProtocols/AdamantCore/AdamantCore+Extensions.swift b/Adamant/ServiceProtocols/AdamantCore/AdamantCore+Extensions.swift index 7adee88c5..2cebbc0be 100644 --- a/Adamant/ServiceProtocols/AdamantCore/AdamantCore+Extensions.swift +++ b/Adamant/ServiceProtocols/AdamantCore/AdamantCore+Extensions.swift @@ -8,6 +8,7 @@ import Foundation import CommonKit +import BigInt extension AdamantCore { func makeSignedTransaction( @@ -27,7 +28,195 @@ extension AdamantCore { recipientId: transaction.recipientId, amount: transaction.amount, signature: signature, - asset: transaction.asset + asset: transaction.asset, + requesterPublicKey: transaction.requesterPublicKey ) } + + func makeSendMessageTransaction( + senderId: String, + recipientId: String, + keypair: Keypair, + message: String, + type: ChatType, + nonce: String, + amount: Decimal? + ) throws -> UnregisteredTransaction { + let normalizedTransaction = NormalizedTransaction( + type: .chatMessage, + amount: amount ?? .zero, + senderPublicKey: keypair.publicKey, + requesterPublicKey: nil, + date: Date(), + recipientId: recipientId, + asset: TransactionAsset( + chat: ChatAsset(message: message, ownMessage: nonce, type: type), + state: nil, + votes: nil + ) + ) + + guard let transaction = makeSignedTransaction( + transaction: normalizedTransaction, + senderId: senderId, + keypair: keypair + ) else { + throw InternalAPIError.signTransactionFailed + } + + return transaction + } + + func createTransferTransaction( + senderId: String, + recipientId: String, + keypair: Keypair, + amount: Decimal? + ) -> UnregisteredTransaction? { + let normalizedTransaction = NormalizedTransaction( + type: .send, + amount: amount ?? .zero, + senderPublicKey: keypair.publicKey, + requesterPublicKey: nil, + date: .now, + recipientId: recipientId, + asset: .init() + ) + + guard let transaction = makeSignedTransaction( + transaction: normalizedTransaction, + senderId: senderId, + keypair: keypair + ) else { + return nil + } + + return transaction + } +} + +// MARK: - Bytes + +extension UnregisteredTransaction { + func generateId() -> String? { + let hash = bytes.sha256() + + guard hash.count > 7 else { return nil } + + var temp: [UInt8] = [] + + for i in 0..<8 { + temp.insert(hash[7 - i], at: i) + } + + guard let value = bigIntFromBuffer(temp, size: 1) else { + return nil + } + + return String(value) + } +} + +private extension UnregisteredTransaction { + func bigIntFromBuffer(_ buffer: [UInt8], size: Int) -> BigInt? { + if buffer.isEmpty || size <= 0 { + return nil + } + + var chunks: [[UInt8]] = [] + + for i in stride(from: 0, to: buffer.count, by: size) { + let chunk = buffer[i] + chunks.append([chunk]) + } + + let hexStrings = chunks.map { chunk in + return chunk.map { byte in + let hex = String(byte, radix: 16) + return hex.count == 1 ? "0" + hex : hex + }.joined() + } + + let hex = hexStrings.joined() + + return BigInt(hex, radix: 16) + } + + var bytes: [UInt8] { + return typeBytes + + timestampBytes + + senderPublicKeyBytes + + requesterPublicKeyBytes + + recipientIdBytes + + amountBytes + + assetBytes + + signatureBytes + + signSignatureBytes + } + + var typeBytes: [UInt8] { + [UInt8(type.rawValue)] + } + + var timestampBytes: [UInt8] { + ByteBackpacker.pack(UInt32(timestamp), byteOrder: .littleEndian) + } + + var senderPublicKeyBytes: [UInt8] { + senderPublicKey.hexBytes() + } + + var requesterPublicKeyBytes: [UInt8] { + requesterPublicKey?.hexBytes() ?? [] + } + + var recipientIdBytes: [UInt8] { + guard + let value = recipientId?.replacingOccurrences(of: "U", with: ""), + let number = UInt64(value) else { return Bytes(count: 8) } + return ByteBackpacker.pack(number, byteOrder: .bigEndian) + } + + var amountBytes: [UInt8] { + let value = (self.amount.shiftedToAdamant() as NSDecimalNumber).uint64Value + let bytes = ByteBackpacker.pack(value, byteOrder: .littleEndian) + return bytes + } + + var signatureBytes: [UInt8] { + signature.hexBytes() + } + + var signSignatureBytes: [UInt8] { + [] + } + + var assetBytes: [UInt8] { + switch type { + case .chatMessage: + guard let msg = asset.chat?.message, let own = asset.chat?.ownMessage, let type = asset.chat?.type else { return [] } + + return msg.hexBytes() + own.hexBytes() + ByteBackpacker.pack(UInt32(type.rawValue), byteOrder: .littleEndian) + + case .state: + guard let key = asset.state?.key, let value = asset.state?.value, let type = asset.state?.type else { return [] } + + return value.bytes + key.bytes + ByteBackpacker.pack(UInt32(type.rawValue), byteOrder: .littleEndian) + + case .vote: + guard + let votes = asset.votes?.votes + else { return [] } + + var bytes = [UInt8]() + for vote in votes { + bytes += vote.bytes + } + + return bytes + + default: + return [] + } + } } diff --git a/Adamant/ServiceProtocols/ApiService.swift b/Adamant/ServiceProtocols/ApiService.swift index 60c72224b..343aab1b3 100644 --- a/Adamant/ServiceProtocols/ApiService.swift +++ b/Adamant/ServiceProtocols/ApiService.swift @@ -10,91 +10,20 @@ import Foundation import Alamofire import CommonKit -// MARK: - Notifications -extension Notification.Name { - enum ApiService { - static let currentNodeUpdate = Notification.Name("adamant.apiService.currentNodeUpdate") - } -} - -// - MARK: ApiService -protocol ApiService: Actor { - /// Time interval between node (lhs) and client (rhs) - /// Substract this from client time to get server time - var lastRequestTimeDelta: TimeInterval? { get } - - var currentNodes: [Node] { get } - - // MARK: - Async/Await - - func sendRequest( - url: URLConvertible, - method: HTTPMethod, - parameters: Parameters? - ) async throws -> Output - - func sendRequest( - url: URLConvertible, - method: HTTPMethod, - parameters: Parameters?, - encoding: ParameterEncoding - ) async throws -> Output - - func sendRequest( - url: URLConvertible, - method: HTTPMethod, - parameters: Parameters? - ) async throws -> Data - - func sendRequest( - url: URLConvertible, - method: HTTPMethod, - parameters: Parameters?, - encoding: ParameterEncoding - ) async throws -> Data - - func sendRequest(request: DataRequest) async throws -> Data - - // MARK: - Peers - - func getNodeVersion(url: URL, completion: @escaping (ApiServiceResult) -> Void) - - // MARK: - Status - - @discardableResult - func getNodeStatus( - url: URL, - completion: @escaping (ApiServiceResult) -> Void - ) -> DataRequest? - +protocol ApiService: WalletApiService { // MARK: - Accounts - - func getAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) - func getAccount(byPublicKey publicKey: String, completion: @escaping (ApiServiceResult) -> Void) - - func getAccount(byPublicKey publicKey: String) async throws -> AdamantAccount - - func getAccount( - byAddress address: String, - completion: @escaping (ApiServiceResult) -> Void - ) - - func getAccount(byAddress address: String) async throws -> AdamantAccount + func getAccount(byPassphrase passphrase: String) async -> ApiServiceResult + func getAccount(byPublicKey publicKey: String) async -> ApiServiceResult + func getAccount(byAddress address: String) async -> ApiServiceResult // MARK: - Keys - func getPublicKey( - byAddress address: String, - completion: @escaping (ApiServiceResult) -> Void - ) + func getPublicKey(byAddress address: String) async -> ApiServiceResult // MARK: - Transactions - func getTransaction(id: UInt64, completion: @escaping (ApiServiceResult) -> Void) - - func getTransaction(id: UInt64) async throws -> Transaction - - func getTransaction(id: UInt64, withAsset: Bool) async throws -> Transaction + func getTransaction(id: UInt64) async -> ApiServiceResult + func getTransaction(id: UInt64, withAsset: Bool) async -> ApiServiceResult func getTransactions( forAccount: String, @@ -102,7 +31,7 @@ protocol ApiService: Actor { fromHeight: Int64?, offset: Int?, limit: Int? - ) async throws -> [Transaction] + ) async -> ApiServiceResult<[Transaction]> func getTransactions( forAccount account: String, @@ -111,57 +40,37 @@ protocol ApiService: Actor { offset: Int?, limit: Int?, orderByTime: Bool? - ) async throws -> [Transaction] + ) async -> ApiServiceResult<[Transaction]> // MARK: - Chats Rooms - - func getChatRooms( - address: String, - offset: Int?, - completion: @escaping (ApiServiceResult) -> Void - ) func getChatRooms( address: String, offset: Int? - ) async throws -> ChatRooms + ) async -> ApiServiceResult func getChatMessages( address: String, addressRecipient: String, offset: Int?, limit: Int? - ) async throws -> ChatRooms + ) async -> ApiServiceResult // MARK: - Funds - func transferFunds( - sender: String, - recipient: String, - amount: Decimal, - keypair: Keypair, - completion: @escaping (ApiServiceResult) -> Void - ) - func transferFunds( sender: String, recipient: String, amount: Decimal, keypair: Keypair - ) async throws -> UInt64 + ) async -> ApiServiceResult + + func transferFunds( + transaction: UnregisteredTransaction + ) async -> ApiServiceResult // MARK: - States - /// - Returns: Transaction ID - func store( - key: String, - value: String, - type: StateType, - sender: String, - keypair: Keypair, - completion: @escaping (ApiServiceResult) -> Void - ) - /// - Returns: Transaction ID func store( key: String, @@ -169,97 +78,54 @@ protocol ApiService: Actor { type: StateType, sender: String, keypair: Keypair - ) async throws -> UInt64 - - func get(key: String, sender: String, completion: @escaping (ApiServiceResult) -> Void) + ) async -> ApiServiceResult func get( key: String, sender: String - ) async throws -> String? + ) async -> ApiServiceResult // MARK: - Chats - /// Get chat transactions (type 8) - /// - /// - Parameters: - /// - address: Transactions for specified account - /// - height: From this height. Minimal value is 1. func getMessageTransactions( address: String, height: Int64?, - offset: Int?, - completion: @escaping (ApiServiceResult<[Transaction]>) -> Void - ) - - func getMessageTransactions(address: String, - height: Int64?, - offset: Int? - ) async throws -> [Transaction] - - /// Send text message - /// - completion: Contains processed transactionId, if success, or AdamantError, if fails. - /// - Returns: Signed unregistered transaction - @discardableResult - func sendMessage( - senderId: String, - recipientId: String, - keypair: Keypair, - message: String, - type: ChatType, - nonce: String, - amount: Decimal?, - completion: @escaping (ApiServiceResult) -> Void - ) -> UnregisteredTransaction? + offset: Int? + ) async -> ApiServiceResult<[Transaction]> func sendTransaction( path: String, - transaction: UnregisteredTransaction, - completion: @escaping (ApiServiceResult) -> Void - ) - - func createSendTransaction( - senderId: String, - recipientId: String, - keypair: Keypair, - message: String, - type: ChatType, - nonce: String, - amount: Decimal? - ) -> UnregisteredTransaction? + transaction: UnregisteredTransaction + ) async -> ApiServiceResult - func sendTransaction( + func sendMessageTransaction( transaction: UnregisteredTransaction - ) async throws -> UInt64 + ) async -> ApiServiceResult // MARK: - Delegates /// Get delegates - func getDelegates(limit: Int, completion: @escaping (ApiServiceResult<[Delegate]>) -> Void) + func getDelegates(limit: Int) async -> ApiServiceResult<[Delegate]> func getDelegatesWithVotes( for address: String, - limit: Int, - completion: @escaping (ApiServiceResult<[Delegate]>) -> Void - ) + limit: Int + ) async -> ApiServiceResult<[Delegate]> /// Get delegate forge details func getForgedByAccount( - publicKey: String, - completion: @escaping (ApiServiceResult) -> Void - ) + publicKey: String + ) async -> ApiServiceResult /// Get delegate forgeing time func getForgingTime( - for delegate: Delegate, - completion: @escaping (ApiServiceResult) -> Void - ) + for delegate: Delegate + ) async -> ApiServiceResult /// Send vote transaction for delegates func voteForDelegates( from address: String, keypair: Keypair, - votes: [DelegateVote], - completion: @escaping (ApiServiceResult) -> Void - ) + votes: [DelegateVote] + ) async -> ApiServiceResult } diff --git a/Adamant/ServiceProtocols/CoinStorage.swift b/Adamant/ServiceProtocols/CoinStorage.swift new file mode 100644 index 000000000..912281324 --- /dev/null +++ b/Adamant/ServiceProtocols/CoinStorage.swift @@ -0,0 +1,21 @@ +// +// CoinStorage.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 26.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Combine +import CommonKit + +protocol CoinStorageService: AnyObject { + var transactionsPublisher: any Observable<[TransactionDetails]> { + get + } + + func append(_ transaction: TransactionDetails) + func append(_ transactions: [TransactionDetails]) + func clear() + func updateStatus(for transactionId: String, status: TransactionStatus?) +} diff --git a/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift b/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift index 905198748..70f4a5c70 100644 --- a/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift @@ -158,7 +158,7 @@ protocol TransfersProvider: DataProvider, Actor { amount: Decimal, comment: String?, replyToMessageId: String? - ) async throws -> TransactionDetails + ) async throws -> AdamantTransactionDetails // MARK: - Transactions func getTransfer(id: String) -> TransferTransaction? diff --git a/Adamant/ServiceProtocols/DialogService.swift b/Adamant/ServiceProtocols/DialogService.swift index af333d338..cf2d930e4 100644 --- a/Adamant/ServiceProtocols/DialogService.swift +++ b/Adamant/ServiceProtocols/DialogService.swift @@ -35,6 +35,7 @@ enum ShareType { case share case generateQr(encodedContent: String?, sharingTip: String?, withLogo: Bool) case saveToPhotolibrary(image: UIImage) + case partnerQR var localized: String { switch self { @@ -44,7 +45,7 @@ enum ShareType { case .share: return String.adamant.alert.share - case .generateQr: + case .generateQr, .partnerQR: return String.adamant.alert.generateQr case .saveToPhotolibrary: @@ -147,6 +148,15 @@ protocol DialogService: AnyObject { func presentShareAlertFor(string: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, from: UIView?, completion: (() -> Void)?) func presentShareAlertFor(stringForPasteboard: String, stringForShare: String, stringForQR: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, from: UIView?, completion: (() -> Void)?) func presentShareAlertFor(string: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, from: UIBarButtonItem?, completion: (() -> Void)?) + func presentShareAlertFor( + string: String, + types: [ShareType], + excludedActivityTypes: [UIActivity.ActivityType]?, + animated: Bool, + from: UIBarButtonItem?, + completion: (() -> Void)?, + didSelect: ((ShareType) -> Void)? + ) func presentGoToSettingsAlert(title: String?, message: String?) diff --git a/Adamant/ServiceProtocols/HealthCheckService.swift b/Adamant/ServiceProtocols/HealthCheckService.swift deleted file mode 100644 index 260df5bcc..000000000 --- a/Adamant/ServiceProtocols/HealthCheckService.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// HealthCheckService.swift -// Adamant -// -// Created by Андрей on 06.06.2022. -// Copyright © 2022 Adamant. All rights reserved. -// - -import Foundation -import CommonKit - -protocol HealthCheckDelegate: AnyObject { - func healthCheckUpdate() -} - -protocol HealthCheckService: AnyObject { - var nodes: [Node] { get set } - var delegate: HealthCheckDelegate? { get set } - - func healthCheck() -} diff --git a/Adamant/ServiceProtocols/LskApiService.swift b/Adamant/ServiceProtocols/LskApiService.swift deleted file mode 100644 index f850ee421..000000000 --- a/Adamant/ServiceProtocols/LskApiService.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// LskApiServerProtocol.swift -// Adamant -// -// Created by Anton Boyarkin on 12/07/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation -import LiskKit - -// MARK: - Notifications -extension Notification.Name { - struct LskApiService { - /// Raised when user has logged out. - static let userLoggedOut = Notification.Name("adamant.lskApiService.userHasLoggedOut") - - /// Raised when user has successfully logged in. - static let userLoggedIn = Notification.Name("adamant.lskApiService.userHasLoggedIn") - - private init() {} - } -} - -protocol LskApiService: AnyObject { - - var account: LskAccount? { get } - - // MARK: - Transactions - func createTransaction(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) - func sendTransaction(transaction: LocalTransaction, completion: @escaping (ApiServiceResult) -> Void) - - func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) - func getTransactions(_ completion: @escaping (ApiServiceResult<[Transactions.TransactionModel]>) -> Void) - func getTransaction(byHash hash: String, completion: @escaping (ApiServiceResult) -> Void) - - // MARK: - Tools - func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) - func getLskAddress(byAdamandAddress address: String, completion: @escaping (ApiServiceResult) -> Void) -} diff --git a/Adamant/ServiceProtocols/NodesAdditionalParamsStorageProtocol.swift b/Adamant/ServiceProtocols/NodesAdditionalParamsStorageProtocol.swift new file mode 100644 index 000000000..16a5e4df8 --- /dev/null +++ b/Adamant/ServiceProtocols/NodesAdditionalParamsStorageProtocol.swift @@ -0,0 +1,23 @@ +// +// NodesAdditionalParamsStorageProtocol.swift +// Adamant +// +// Created by Andrew G on 18.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit + +// MARK: - SecuredStore keys +extension StoreKey { + enum NodesAdditionalParamsStorage { + static let fastestNodeMode = "nodesAdditionalParamsStorage.fastestNodeMode" + } +} + +protocol NodesAdditionalParamsStorageProtocol { + func isFastestNodeMode(group: NodeGroup) -> Bool + func fastestNodeMode(group: NodeGroup) -> AnyObservable + func setFastestNodeMode(groups: Set, value: Bool) + func setFastestNodeMode(group: NodeGroup, value: Bool) +} diff --git a/Adamant/ServiceProtocols/NodesSource.swift b/Adamant/ServiceProtocols/NodesSource.swift deleted file mode 100644 index b1e5f8a81..000000000 --- a/Adamant/ServiceProtocols/NodesSource.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// NodesSource.swift -// Adamant -// -// Created by Anokhov Pavel on 21.06.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation -import CommonKit - -// MARK: - Notifications -extension Notification.Name { - struct NodesSource { - /// Raised by NodesSource when need to update current node or list of nodes - static let nodesUpdate = Notification.Name("adamant.nodesSource.nodesUpdate") - - private init() {} - } -} - -// MARK: - SecuredStore keys -extension StoreKey { - struct NodesSource { - static let nodes = "nodesSource.nodes" - - private init() {} - } -} - -// MARK: - UserDefaults -extension UserDefaults { - enum NodesSource { - static let preferTheFastestNodeKey = "nodesSource.preferTheFastestNode" - } -} - -protocol NodesSource: AnyObject { - var nodes: [Node] { get set } - var preferTheFastestNode: Bool { get set } - - func setDefaultNodes() - func getAllowedNodes(needWS: Bool) -> [Node] - func healthCheck() - func nodesUpdate() -} diff --git a/Adamant/ServiceProtocols/NodesStorageProtocol.swift b/Adamant/ServiceProtocols/NodesStorageProtocol.swift new file mode 100644 index 000000000..365c5b41b --- /dev/null +++ b/Adamant/ServiceProtocols/NodesStorageProtocol.swift @@ -0,0 +1,81 @@ +// +// NodesStorageProtocol.swift +// Adamant +// +// Created by Andrew G on 30.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +// MARK: - SecuredStore keys +extension StoreKey { + enum NodesStorage { + static let nodes = "nodesStorage.nodes" + } +} + +protocol NodesStorageProtocol { + var nodesWithGroupsPublisher: AnyObservable<[NodeWithGroup]> { get } + + func getNodesPublisher(group: NodeGroup) -> AnyObservable<[Node]> + func addNode(_ node: Node, group: NodeGroup) + func resetNodes(group: NodeGroup) + func removeNode(id: UUID) + + func updateNode( + id: UUID, + scheme: CommonKit.Node.URLScheme?, + host: String?, + isEnabled: Bool?, + wsEnabled: Bool?, + port: Int??, + wsPort: Int??, + version: String??, + height: Int??, + ping: TimeInterval??, + connectionStatus: CommonKit.Node.ConnectionStatus?? + ) +} + +extension NodesStorageProtocol { + func updateNodeStatus(id: UUID, statusInfo: NodeStatusInfo?) { + updateNodeParams( + id: id, + wsEnabled: .some(statusInfo?.wsEnabled ?? false), + wsPort: .some(statusInfo?.wsPort), + version: .some(statusInfo?.version), + height: .some(statusInfo?.height), + ping: .some(statusInfo?.ping) + ) + } + + func updateNodeParams( + id: UUID, + scheme: CommonKit.Node.URLScheme? = nil, + host: String? = nil, + isEnabled: Bool? = nil, + wsEnabled: Bool? = nil, + port: Int?? = nil, + wsPort: Int?? = nil, + version: String?? = nil, + height: Int?? = nil, + ping: TimeInterval?? = nil, + connectionStatus: CommonKit.Node.ConnectionStatus?? = nil + ) { + updateNode( + id: id, + scheme: scheme, + host: host, + isEnabled: isEnabled, + wsEnabled: wsEnabled, + port: port, + wsPort: wsPort, + version: version, + height: height, + ping: ping, + connectionStatus: connectionStatus + ) + } +} diff --git a/Adamant/ServiceProtocols/PartnerQRService.swift b/Adamant/ServiceProtocols/PartnerQRService.swift new file mode 100644 index 000000000..a193cd2d9 --- /dev/null +++ b/Adamant/ServiceProtocols/PartnerQRService.swift @@ -0,0 +1,16 @@ +// +// PartnerQRService.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 28.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +protocol PartnerQRService: AnyObject { + func setIncludeNameEnabled(_ value: Bool) + func isIncludeNameEnabled() -> Bool + func setIncludeURLEnabled(_ value: Bool) + func isIncludeURLEnabled() -> Bool +} diff --git a/Adamant/ServiceProtocols/RichMessageProvider.swift b/Adamant/ServiceProtocols/RichMessageProvider.swift index 53e0e517a..8d8e137e8 100644 --- a/Adamant/ServiceProtocols/RichMessageProvider.swift +++ b/Adamant/ServiceProtocols/RichMessageProvider.swift @@ -10,7 +10,7 @@ import Foundation import MessageKit import UIKit -protocol RichMessageProvider: AnyObject { +protocol RichMessageProvider: WalletService { /// Lowercased!! static var richMessageType: String { get } @@ -28,9 +28,6 @@ protocol RichMessageProvider: AnyObject { var tokenSymbol: String { get } var tokenLogo: UIImage { get } - // MARK: Events - func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) - // MARK: Chats list func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString } diff --git a/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck+Extension.swift b/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck+Extension.swift index 57ad99f64..16d6ae2e7 100644 --- a/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck+Extension.swift +++ b/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck+Extension.swift @@ -10,12 +10,15 @@ import Foundation extension RichMessageProviderWithStatusCheck { func statusWithFilters( - transaction: RichMessageTransaction, + transaction: RichMessageTransaction?, oldPendingAttempts: Int, info: TransactionStatusInfo ) -> TransactionStatus { switch info.status { case .success: + guard let transaction = transaction else { + return info.status + } return consistencyFilter(transaction: transaction, statusInfo: info) ? info.status : .inconsistent diff --git a/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift b/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift index 99400277d..a74d99812 100644 --- a/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift +++ b/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift @@ -13,6 +13,20 @@ struct TransactionStatusInfo { let status: TransactionStatus } +extension TransactionStatusInfo { + init(error: Error) { + switch error { + case ApiServiceError.networkError, + ApiServiceError.noEndpointsAvailable, + WalletServiceError.networkError, + WalletServiceError.apiError(.noEndpointsAvailable): + self.init(sentDate: nil, status: .noNetwork) + default: + self.init(sentDate: nil, status: .pending) + } + } +} + protocol RichMessageProviderWithStatusCheck: RichMessageProvider { - func statusInfoFor(transaction: RichMessageTransaction) async -> TransactionStatusInfo + func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo } diff --git a/Adamant/ServiceProtocols/Router.swift b/Adamant/ServiceProtocols/Router.swift deleted file mode 100644 index 8a357db05..000000000 --- a/Adamant/ServiceProtocols/Router.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Router.swift -// Adamant -// -// Created by Anokhov Pavel on 07.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import Swinject - -// MARK: - Adamant Scene -struct AdamantScene { - let identifier: String - let factory: @MainActor (Resolver) -> UIViewController - - init(identifier: String, factory: @MainActor @escaping (Resolver) -> UIViewController) { - self.identifier = identifier - self.factory = factory - } -} - -// MARK: - Adamant Router -protocol Router: AnyObject { - func get(scene: AdamantScene) -> UIViewController -} diff --git a/Adamant/ServiceProtocols/RichTransactionStatusService.swift b/Adamant/ServiceProtocols/TransactionStatusService.swift similarity index 50% rename from Adamant/ServiceProtocols/RichTransactionStatusService.swift rename to Adamant/ServiceProtocols/TransactionStatusService.swift index 19a340eb3..a4fb8f2d8 100644 --- a/Adamant/ServiceProtocols/RichTransactionStatusService.swift +++ b/Adamant/ServiceProtocols/TransactionStatusService.swift @@ -1,5 +1,5 @@ // -// RichTransactionStatusService.swift +// TransactionStatusService.swift // Adamant // // Created by Andrey Golubenko on 13.01.2023. @@ -8,7 +8,7 @@ import CoreData -protocol RichTransactionStatusService: Actor, AnyObject { - func forceUpdate(transaction: RichMessageTransaction) async +protocol TransactionStatusService: Actor, AnyObject { + func forceUpdate(transaction: CoinTransaction) async func startObserving() } diff --git a/Adamant/ServiceProtocols/VibroService.swift b/Adamant/ServiceProtocols/VibroService.swift new file mode 100644 index 000000000..cbb7dcb9e --- /dev/null +++ b/Adamant/ServiceProtocols/VibroService.swift @@ -0,0 +1,21 @@ +// +// VibroService.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 07.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +// MARK: - Notifications +extension Notification.Name { + struct AdamantVibroService { + static let presentVibrationRow = Notification.Name("adamant.vibroService.presentVibrationRow") + + } +} + +protocol VibroService: AnyObject { + func applyVibration(_ type: AdamantVibroType) +} diff --git a/Adamant/Services/APICore.swift b/Adamant/Services/APICore.swift new file mode 100644 index 000000000..f3748aa34 --- /dev/null +++ b/Adamant/Services/APICore.swift @@ -0,0 +1,103 @@ +// +// APICore.swift +// Adamant +// +// Created by Andrew G on 30.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation +import Alamofire +import CommonKit + +actor APICore: APICoreProtocol { + private let responseQueue = DispatchQueue( + label: "com.adamant.response-queue", + qos: .userInteractive + ) + + private lazy var session: Session = { + let configuration = AF.sessionConfiguration + configuration.waitsForConnectivity = true + configuration.timeoutIntervalForRequest = timeoutInterval + configuration.timeoutIntervalForResource = timeoutInterval + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + return Alamofire.Session.init(configuration: configuration) + }() + + func sendRequestBasic( + node: Node, + path: String, + method: HTTPMethod, + parameters: Parameters, + encoding: APIParametersEncoding + ) async -> APIResponseModel { + do { + let request = session.request( + try buildUrl(node: node, path: path), + method: method, + parameters: parameters.asDictionary(), + encoding: encoding.parametersEncoding, + headers: HTTPHeaders(["Content-Type": "application/json"]) + ) + + return await sendRequest(request: request) + } catch { + return .init( + result: .failure(.internalError(message: error.localizedDescription, error: error)), + data: nil, + code: nil + ) + } + } + + func sendRequestBasic( + node: Node, + path: String, + method: HTTPMethod, + jsonParameters: Any + ) async -> APIResponseModel { + do { + let data = try JSONSerialization.data( + withJSONObject: jsonParameters + ) + + var request = try URLRequest( + url: try buildUrl(node: node, path: path), + method: method + ) + + request.httpBody = data + request.headers.update(.contentType("application/json")) + return await sendRequest(request: AF.request(request)) + } catch { + return .init( + result: .failure(.internalError(message: error.localizedDescription, error: error)), + data: nil, + code: nil + ) + } + } +} + +private extension APICore { + func sendRequest(request: DataRequest) async -> APIResponseModel { + await withCheckedContinuation { continuation in + request.responseData(queue: responseQueue) { response in + continuation.resume(returning: .init( + result: response.result.mapError { .init(error: $0) }, + data: response.data, + code: response.response?.statusCode + )) + } + } + } + + func buildUrl(node: Node, path: String) throws -> URL { + guard let url = node.asURL()?.appendingPathComponent(path, conformingTo: .url) + else { throw InternalAPIError.endpointBuildFailed } + return url + } +} + +private let timeoutInterval: TimeInterval = 15 diff --git a/Adamant/Services/AdamantAccountService.swift b/Adamant/Services/AdamantAccountService.swift index 0df4d036b..76a935b5c 100644 --- a/Adamant/Services/AdamantAccountService.swift +++ b/Adamant/Services/AdamantAccountService.swift @@ -42,17 +42,12 @@ final class AdamantAccountService: AccountService { AdmWalletService(), BtcWalletService(), EthWalletService(), - LskWalletService(mainnet: true, nodes: LskWalletService.nodes, services: LskWalletService.serviceNodes), + LskWalletService(), DogeWalletService(), DashWalletService() ] let erc20WalletServices = ERC20Token.supportedTokens.map { ERC20WalletService(token: $0) } wallets.append(contentsOf: erc20WalletServices) - - //LskWalletService(mainnet: false) - // Testnet - // wallets.append(contentsOf: LskWalletService(mainnet: false)) - return wallets }() @@ -67,58 +62,6 @@ final class AdamantAccountService: AccountService { self.dialogService = dialogService self.securedStore = securedStore - guard let ethWallet = wallets[2] as? EthWalletService else { - fatalError("Failed to get EthWalletService") - } - - guard let node = EthWalletService.nodes.randomElement() else { - fatalError("Failed to get ETH endpoint") - } - - let url = node.asString() - - ethWallet.initiateNetwork(apiUrl: url) { result in - switch result { - case .success: - break - - case .failure(let error): - switch error { - case .networkError: - NotificationCenter.default.addObserver( - forName: Notification.Name.AdamantReachabilityMonitor.reachabilityChanged, - object: nil, queue: nil - ) { notification in - guard (notification.userInfo?[AdamantUserInfoKey.ReachabilityMonitor.connection] as? Bool) == true - else { return } - - ethWallet.initiateNetwork(apiUrl: url) { result in - switch result { - case .success: - NotificationCenter.default.removeObserver( - self, - name: Notification.Name.AdamantReachabilityMonitor.reachabilityChanged, - object: nil - ) - - case .failure(let error): - print(error) - } - } - } - - case .notLogged, .transactionNotFound, .notEnoughMoney, .accountNotFound, .walletNotInitiated, .invalidAmount, .requestCancelled, .dustAmountError: - break - - case .remoteServiceError, .apiError, .internalError: - Task { @MainActor [dialogService] in - dialogService.showRichError(error: error) - } - self.wallets.remove(at: 1) - } - } - } - NotificationCenter.default.addObserver(forName: .AdamantAccountService.forceUpdateBalance, object: nil, queue: OperationQueue.main) { [weak self] _ in self?.update() } @@ -280,31 +223,31 @@ extension AdamantAccountService { } Task { - await apiService.getAccount(byPublicKey: publicKey) { [weak self] result in - switch result { - case .success(let account): - guard let acc = self?.account, acc.address == account.address else { - // User has logged out, we not interested anymore - self?.state = .notLogged - return - } - - if loggedAccount.balance != account.balance { - self?.account = account - NotificationCenter.default.post(name: Notification.Name.AdamantAccountService.accountDataUpdated, object: self) - } - - self?.state = .loggedIn - completion?(.success(account: account, alert: nil)) - - if let adm = self?.wallets.first(where: { $0 is AdmWalletService }) { - adm.update() - } - - case .failure(let error): - completion?(.failure(.apiError(error: error))) - self?.state = prevState + let result = await apiService.getAccount(byPublicKey: publicKey) + + switch result { + case .success(let account): + guard let acc = self.account, acc.address == account.address else { + // User has logged out, we not interested anymore + state = .notLogged + return + } + + if loggedAccount.balance != account.balance { + self.account = account + NotificationCenter.default.post(name: Notification.Name.AdamantAccountService.accountDataUpdated, object: self) } + + state = .loggedIn + completion?(.success(account: account, alert: nil)) + + if let adm = wallets.first(where: { $0 is AdmWalletService }) { + adm.update() + } + + case .failure(let error): + completion?(.failure(.apiError(error: error))) + state = prevState } } @@ -417,7 +360,7 @@ extension AdamantAccountService { state = .isLoggingIn do { - let account = try await apiService.getAccount(byPublicKey: keypair.publicKey) + let account = try await apiService.getAccount(byPublicKey: keypair.publicKey).get() self.account = account self.keypair = keypair diff --git a/Adamant/Services/AdamantAddressBookService.swift b/Adamant/Services/AdamantAddressBookService.swift index 3aea23d20..c53e89829 100644 --- a/Adamant/Services/AdamantAddressBookService.swift +++ b/Adamant/Services/AdamantAddressBookService.swift @@ -288,7 +288,7 @@ final class AdamantAddressBookService: AddressBookService { type: .keyValue, sender: address, keypair: keypair - ) + ).get() return id } catch let error as ApiServiceError { @@ -313,7 +313,7 @@ final class AdamantAddressBookService: AddressBookService { let address = loggedAccount.address do { - let rawValue = try await apiService.get(key: addressBookKey, sender: address) + let rawValue = try await apiService.get(key: addressBookKey, sender: address).get() guard let value = rawValue, let object = value.toDictionary(), let message = object["message"] as? String, diff --git a/Adamant/Services/AdamantAuthentication.swift b/Adamant/Services/AdamantAuthentication.swift index 406312d42..601bb7cfc 100644 --- a/Adamant/Services/AdamantAuthentication.swift +++ b/Adamant/Services/AdamantAuthentication.swift @@ -9,7 +9,7 @@ import Foundation import LocalAuthentication -class AdamantAuthentication: LocalAuthentication { +final class AdamantAuthentication: LocalAuthentication { var biometryType: BiometryType { let context = LAContext() var error: NSError? diff --git a/Adamant/Services/AdamantCellFactory.swift b/Adamant/Services/AdamantCellFactory.swift index d6e1dca2c..296d13b89 100644 --- a/Adamant/Services/AdamantCellFactory.swift +++ b/Adamant/Services/AdamantCellFactory.swift @@ -8,7 +8,7 @@ import UIKit -class AdamantCellFactory: CellFactory { +final class AdamantCellFactory: CellFactory { func nib(for sharedCell: SharedCell) -> UINib? { /* UINib.init actually can throw an exception do { diff --git a/Adamant/Services/AdamantCoinStorageService.swift b/Adamant/Services/AdamantCoinStorageService.swift new file mode 100644 index 000000000..403be4182 --- /dev/null +++ b/Adamant/Services/AdamantCoinStorageService.swift @@ -0,0 +1,190 @@ +// +// AdamantCoinStorageService.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 26.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation +import CoreData +import Combine +import CommonKit + +final class AdamantCoinStorageService: NSObject, CoinStorageService { + + // MARK: Proprieties + + private let blockchainType: String + private let coinId: String + private let coreDataStack: CoreDataStack + private lazy var transactionController = getTransactionController() + private var subscriptions = Set() + + @ObservableValue private var transactions: [TransactionDetails] = [] + + var transactionsPublisher: any Observable<[TransactionDetails]> { + $transactions + } + + // MARK: Init + + init(coinId: String, coreDataStack: CoreDataStack, blockchainType: String) { + self.coinId = coinId + self.coreDataStack = coreDataStack + self.blockchainType = blockchainType + super.init() + + try? transactionController.performFetch() + transactions = transactionController.fetchedObjects ?? [] + + setupObserver() + } + + func append(_ transaction: TransactionDetails) { + append([transaction]) + } + + func append(_ transactions: [TransactionDetails]) { + let privateContext = coreDataStack.container.viewContext + privateContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType) + + var coinTransactions: [CoinTransaction] = [] + + transactions.forEach { transaction in + let isExist = self.transactions.contains { tx in + tx.txId == transaction.txId + } + let isLocalExist = coinTransactions.contains { tx in + tx.txId == transaction.txId + } + guard !isExist, !isLocalExist else { return } + + let coinTransaction = CoinTransaction(context: privateContext) + coinTransaction.amount = NSDecimalNumber(decimal: transaction.amountValue ?? 0) + coinTransaction.date = (transaction.dateValue ?? Date()) as NSDate + coinTransaction.recipientId = transaction.recipientAddress + coinTransaction.senderId = transaction.senderAddress + coinTransaction.isOutgoing = transaction.isOutgoing + coinTransaction.coinId = coinId + coinTransaction.transactionId = transaction.txId + coinTransaction.transactionStatus = transaction.transactionStatus + coinTransaction.blockchainType = blockchainType + coinTransaction.fee = NSDecimalNumber(decimal: transaction.feeValue ?? 0) + + coinTransactions.append(coinTransaction) + } + + try? privateContext.save() + } + + func updateStatus(for transactionId: String, status: TransactionStatus?) { + let privateContext = coreDataStack.container.viewContext + + guard let transaction = getTransactionFromDB( + id: transactionId, + context: privateContext + ) else { return } + + transaction.transactionStatus = status + try? privateContext.save() + } + + func clear() { + transactions = [] + } +} + +private extension AdamantCoinStorageService { + func setupObserver() { + NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, + object: coreDataStack.container.viewContext + ) + .sink { [weak self] notification in + let changes = notification.managedObjectContextChanges(of: CoinTransaction.self) + + if let inserted = changes.inserted, !inserted.isEmpty { + let filteredInserted: [TransactionDetails] = inserted.filter { + $0.coinId == self?.coinId + } + self?.transactions.append(contentsOf: filteredInserted) + } + + if let updated = changes.updated, !updated.isEmpty { + let filteredUpdated = updated.filter { $0.coinId == self?.coinId } + + filteredUpdated.forEach { coinTransaction in + guard let index = self?.transactions.firstIndex(where: { + $0.txId == coinTransaction.txId + }) + else { return } + + self?.transactions[index] = coinTransaction + } + } + } + .store(in: &subscriptions) + } + + func getTransactionController() -> NSFetchedResultsController { + let request: NSFetchRequest = NSFetchRequest( + entityName: CoinTransaction.entityCoinName + ) + request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + NSPredicate(format: "coinId = %@", coinId) + ]) + request.sortDescriptors = [ + NSSortDescriptor(key: "date", ascending: true), + NSSortDescriptor(key: "transactionId", ascending: true) + ] + + return NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: coreDataStack.container.viewContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + } + + /// Search transaction in local storage + /// + /// - Parameter id: Transacton ID + /// - Returns: Transaction, if found + func getTransactionFromDB(id: String, context: NSManagedObjectContext) -> CoinTransaction? { + let request = NSFetchRequest(entityName: CoinTransaction.entityCoinName) + request.predicate = NSPredicate(format: "transactionId == %@", String(id)) + request.fetchLimit = 1 + + do { + let result = try context.fetch(request) + return result.first + } catch { + return nil + } + } +} + +struct ManagedObjectContextChanges { + var updated: Set? + var inserted: Set? + var deleted: Set? +} + +extension Notification { + func managedObjectContextChanges(of type: Object.Type) -> ManagedObjectContextChanges { + return ManagedObjectContextChanges( + updated: objects(forKey: NSUpdatedObjectsKey), + inserted: objects(forKey: NSInsertedObjectsKey), + deleted: objects(forKey: NSDeletedObjectsKey)) + } + + private func objects(forKey key: String) -> Set? { + guard let userInfo = userInfo else { + assertionFailure() + return nil + } + let objects = (userInfo[key] as? Set) ?? [] + return Set(objects.compactMap { $0 as? Object }) + } +} diff --git a/Adamant/Services/AdamantCurrencyInfoService.swift b/Adamant/Services/AdamantCurrencyInfoService.swift index 2ec4c0f0a..957e01fad 100644 --- a/Adamant/Services/AdamantCurrencyInfoService.swift +++ b/Adamant/Services/AdamantCurrencyInfoService.swift @@ -18,7 +18,7 @@ extension StoreKey { } // MARK: - Service -class AdamantCurrencyInfoService: CurrencyInfoService { +final class AdamantCurrencyInfoService: CurrencyInfoService { // MARK: - API private lazy var infoServiceUrl: URL = { return URL(string: AdamantResources.coinsInfoSrvice)! diff --git a/Adamant/Services/AdamantDialogService.swift b/Adamant/Services/AdamantDialogService.swift index 43490e161..e0f833ccd 100644 --- a/Adamant/Services/AdamantDialogService.swift +++ b/Adamant/Services/AdamantDialogService.swift @@ -15,14 +15,14 @@ import CommonKit @MainActor final class AdamantDialogService: DialogService { // MARK: Dependencies - private let router: Router + private let vibroService: VibroService private let popupManager = PopupManager() private let mailDelegate = MailDelegate() + private weak var window: UIWindow? - // Configure notifications - nonisolated init(router: Router) { - self.router = router + nonisolated init(vibroService: VibroService) { + self.vibroService = vibroService } func setup(window: UIWindow) { @@ -85,10 +85,12 @@ extension AdamantDialogService { } func showSuccess(withMessage message: String) { + vibroService.applyVibration(.success) popupManager.showSuccessAlert(message: message) } func showWarning(withMessage message: String) { + vibroService.applyVibration(.error) popupManager.showWarningAlert(message: message) } @@ -101,6 +103,7 @@ extension AdamantDialogService { supportEmail: Bool, error: Error? = nil ) { + vibroService.applyVibration(.error) popupManager.showAdvancedAlert(model: .init( icon: .asset(named: "error") ?? .init(), title: .adamant.alert.error, @@ -108,7 +111,7 @@ extension AdamantDialogService { secondaryButton: supportEmail ? .init( title: AdamantResources.supportEmail, - action: .init(id: .zero) { [weak self] in + action: .init(id: .empty) { [weak self] in self?.sendErrorEmail(errorDescription: message) self?.popupManager.dismissAdvancedAlert() } @@ -116,7 +119,7 @@ extension AdamantDialogService { : nil, primaryButton: .init( title: .adamant.alert.ok, - action: .init(id: .zero) { [weak popupManager] in + action: .init(id: .empty) { [weak popupManager] in popupManager?.dismissAdvancedAlert() } ) @@ -262,6 +265,33 @@ extension AdamantDialogService { present(alert, animated: animated, completion: completion) } + func presentShareAlertFor( + string: String, + types: [ShareType], + excludedActivityTypes: [UIActivity.ActivityType]?, + animated: Bool, + from: UIBarButtonItem?, + completion: (() -> Void)?, + didSelect: ((ShareType) -> Void)? + ) { + let source: UIAlertController.SourceView? = from.map { .barButtonItem($0) } + + let alert = createShareAlertFor( + stringForPasteboard: string, + stringForShare: string, + stringForQR: string, + types: types, + excludedActivityTypes: excludedActivityTypes, + animated: animated, + from: source, + completion: completion, + didSelect: didSelect + ) + + alert.modalPresentationStyle = .overFullScreen + present(alert, animated: animated, completion: completion) + } + func presentShareAlertFor(string: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, from: UIView?, completion: (() -> Void)?) { let source: UIAlertController.SourceView? = from.map { .view($0) } @@ -297,7 +327,8 @@ extension AdamantDialogService { excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, from: UIAlertController.SourceView?, - completion: (() -> Void)? + completion: (() -> Void)?, + didSelect: ((ShareType) -> Void)? = nil ) -> UIAlertController { let alert = UIAlertController( title: nil, @@ -306,7 +337,17 @@ extension AdamantDialogService { source: from ) - addActions(to: alert, stringForPasteboard: stringForPasteboard, stringForShare: stringForShare, stringForQR: stringForQR, types: types, excludedActivityTypes: excludedActivityTypes, from: from, completion: completion) + addActions( + to: alert, + stringForPasteboard: stringForPasteboard, + stringForShare: stringForShare, + stringForQR: stringForQR, + types: types, + excludedActivityTypes: excludedActivityTypes, + from: from, + completion: completion, + didSelect: didSelect + ) return alert } @@ -319,7 +360,8 @@ extension AdamantDialogService { types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, from: UIAlertController.SourceView?, - completion: (() -> Void)? + completion: (() -> Void)?, + didSelect: ((ShareType) -> Void)? = nil ) { for type in types { switch type { @@ -327,10 +369,12 @@ extension AdamantDialogService { alert.addAction(UIAlertAction(title: type.localized , style: .default) { [weak self] _ in UIPasteboard.general.string = stringForPasteboard self?.showToastMessage(String.adamant.alert.copiedToPasteboardNotification) + didSelect?(.copyToPasteboard) }) case .share: alert.addAction(UIAlertAction(title: type.localized, style: .default) { [weak self] _ in + didSelect?(.share) let vc = UIActivityViewController(activityItems: [stringForShare], applicationActivities: nil) vc.excludedActivityTypes = excludedActivityTypes @@ -356,20 +400,24 @@ extension AdamantDialogService { case .generateQr(let encodedContent, let sharingTip, let withLogo): alert.addAction(UIAlertAction(title: type.localized, style: .default) { [weak self] _ in - switch AdamantQRTools.generateQrFrom(string: encodedContent ?? stringForQR, withLogo: withLogo) { + guard let self = self else { return } + + didSelect?(.generateQr(encodedContent: encodedContent, sharingTip: sharingTip, withLogo: withLogo)) + + switch AdamantQRTools.generateQrFrom( + string: encodedContent ?? stringForQR, + withLogo: withLogo + ) { case .success(let qr): - guard let vc = self?.router.get(scene: AdamantScene.Shared.shareQr) as? ShareQrViewController else { - fatalError("Can't find ShareQrViewController") - } - + let vc = ShareQrViewController(dialogService: self) vc.qrCode = qr vc.sharingTip = sharingTip vc.excludedActivityTypes = excludedActivityTypes vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: completion) + present(vc, animated: true, completion: completion) case .failure(error: let error): - self?.showError( + showError( withMessage: error.localizedDescription, supportEmail: true, error: error @@ -379,9 +427,17 @@ extension AdamantDialogService { case .saveToPhotolibrary(let image): let action = UIAlertAction(title: type.localized, style: .default) { [weak self] _ in + didSelect?(.saveToPhotolibrary(image: image)) UIImageWriteToSavedPhotosAlbum(image, self, #selector(self?.image(_:didFinishSavingWithError:contextInfo:)), nil) } + alert.addAction(action) + + case .partnerQR: + let action = UIAlertAction(title: type.localized, style: .default) { [didSelect] _ in + didSelect?(.partnerQR) + } + alert.addAction(action) } } diff --git a/Adamant/Services/AdamantHealthCheckService.swift b/Adamant/Services/AdamantHealthCheckService.swift deleted file mode 100644 index 5d5a15c5d..000000000 --- a/Adamant/Services/AdamantHealthCheckService.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// AdamantHealthCheckService.swift -// Adamant -// -// Created by Андрей on 06.06.2022. -// Copyright © 2022 Adamant. All rights reserved. -// - -import Foundation -import Alamofire -import CommonKit - -final class AdamantHealthCheckService: HealthCheckService { - // MARK: - Dependencies - - private let apiService: ApiService - - // MARK: - Properties - - @Atomic private var _nodes = [Node]() - @Atomic private var currentRequests = Set() - private let notifyingQueue = DispatchQueue(label: "com.adamant.health-check-notification") - - weak var delegate: HealthCheckDelegate? - - init(apiService: ApiService) { - self.apiService = apiService - } - - var nodes: [Node] { - get { _nodes } - set { _nodes = newValue } - } - - // MARK: - Tools - - func healthCheck() { - updateNodesAvailability() - - nodes.filter { $0.isEnabled && !isRequestInProgress(for: $0) }.forEach { node in - Task { await updateNodeStatus(node: node) } - } - } - - private func isRequestInProgress(for node: Node) -> Bool { - guard let nodeURL = node.asURL() else { - return false - } - - return currentRequests.contains(nodeURL) - } - - private func updateNodesAvailability() { - let workingNodes = nodes.filter { $0.isWorking } - - let actualHeightsRange = getActualNodeHeightsRange( - heights: workingNodes.compactMap { $0.status?.height } - ) - - for node in workingNodes { - node.connectionStatus = node.status?.height.map { height in - actualHeightsRange?.contains(height) ?? false - ? .allowed - : .synchronizing - } ?? .synchronizing - } - - notifyingQueue.async { [weak delegate] in - delegate?.healthCheckUpdate() - } - } - - private func updateNodeStatus(node: Node) async { - guard let nodeURL = node.asURL() else { - node.connectionStatus = .offline - node.status = nil - return - } - - let startTimestamp = Date().timeIntervalSince1970 - currentRequests.insert(nodeURL) - - await apiService.getNodeStatus(url: nodeURL) { [weak self] result in - self?.currentRequests.remove(nodeURL) - - switch result { - case let .success(status): - node.status = Node.Status( - status: status, - ping: Date().timeIntervalSince1970 - startTimestamp - ) - if !node.isWorking { - node.connectionStatus = .synchronizing - } - node.wsPort = status.wsClient?.port - self?.updateNodesAvailability() - case let .failure(error): - self?.processError(error: error, node: node) - } - } - } - - private func processError(error: ApiServiceError, node: Node) { - switch error { - case .requestCancelled: - break - case .networkError, .serverError, .internalError, .notLogged, .accountNotFound, .commonError: - node.connectionStatus = .offline - node.status = nil - updateNodesAvailability() - } - } -} - -private extension Node { - var isWorking: Bool { - switch connectionStatus { - case .allowed, .synchronizing: - return true - case .offline, .none: - return false - } - } -} - -private extension Node.Status { - init(status: NodeStatus, ping: TimeInterval) { - self.init( - ping: ping, - wsEnabled: status.wsClient?.enabled ?? false, - height: status.network?.height, - version: status.version?.version - ) - } -} - -private struct NodeHeightsInterval { - let range: ClosedRange - var count: Int -} - -private func getActualNodeHeightsRange(heights: [Int]) -> ClosedRange? { - guard heights.count > 2 else { return heights.max().map { $0...$0 } } - - let heights = heights.sorted() - var bestInterval: NodeHeightsInterval? - - for i in heights.indices { - var currentInterval = NodeHeightsInterval( - range: heights[i] - nodeHeightEpsilon ... heights[i] + nodeHeightEpsilon, - count: 1 - ) - - for j in stride(from: i + 1, to: heights.endIndex, by: 1) { - guard currentInterval.range.contains(heights[j]) else { break } - currentInterval.count += 1 - } - - for j in stride(from: i - 1, through: 0, by: -1) { - guard currentInterval.range.contains(heights[j]) else { break } - currentInterval.count += 1 - } - - if currentInterval.count >= bestInterval?.count ?? 0 { - bestInterval = currentInterval - } - } - - return bestInterval?.range -} - -private let nodeHeightEpsilon = 10 diff --git a/Adamant/Services/AdamantNodesSource.swift b/Adamant/Services/AdamantNodesSource.swift deleted file mode 100644 index 252895bf1..000000000 --- a/Adamant/Services/AdamantNodesSource.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// AdamantNodesSource.swift -// Adamant -// -// Created by Anokhov Pavel on 21.06.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation -import CommonKit - -final class AdamantNodesSource: NodesSource { - // MARK: - Dependencies - - private let apiService: ApiService - private let healthCheckService: HealthCheckService - private let securedStore: SecuredStore - - // MARK: - Properties - - @Atomic private var _nodes: [Node] = [] { - didSet { - healthCheckService.nodes = nodes - nodesUpdate() - } - } - - @Atomic private var _preferTheFastestNode = preferTheFastestNodeDefault { - didSet { - savePreferTheFastestNode(preferTheFastestNode) - - guard preferTheFastestNode else { return } - sendNodesUpdateNotification() - } - } - - var nodes: [Node] { - get { _nodes } - set { _nodes = newValue } - } - - var preferTheFastestNode: Bool { - get { _preferTheFastestNode } - set { _preferTheFastestNode = newValue } - } - - private let defaultNodesGetter: () -> [Node] - @Atomic private var timer: Timer? - @Atomic private var savedNodes: [Node] = [] - - // MARK: - Ctor - - init( - apiService: ApiService, - healthCheckService: HealthCheckService, - securedStore: SecuredStore, - defaultNodesGetter: @escaping () -> [Node] - ) { - self.apiService = apiService - self.healthCheckService = healthCheckService - self.securedStore = securedStore - self.defaultNodesGetter = defaultNodesGetter - - NotificationCenter.default.addObserver( - forName: Notification.Name.AdamantReachabilityMonitor.reachabilityChanged, - object: nil, - queue: nil - ) { [weak self] _ in - self?.healthCheck() - } - - let savedPreferTheFastestNode = UserDefaults.standard.object( - forKey: UserDefaults.NodesSource.preferTheFastestNodeKey - ) as? Bool - - let preferTheFastestNode = savedPreferTheFastestNode ?? preferTheFastestNodeDefault - - if savedPreferTheFastestNode == nil { - savePreferTheFastestNode(preferTheFastestNodeDefault) - } - - self.preferTheFastestNode = preferTheFastestNode - healthCheckService.delegate = self - healthCheckService.nodes = nodes - setHealthCheckTimer() - loadNodes() - } - - deinit { - timer?.invalidate() - } - - // MARK: - Tools - - func setDefaultNodes() { - nodes = defaultNodesGetter() - } - - func getAllowedNodes(needWS: Bool) -> [Node] { - healthCheckService.nodes.getAllowedNodes( - sortedBySpeedDescending: preferTheFastestNode, - needWS: needWS - ) - } - - func nodesUpdate() { - healthCheck() - saveNodes() - } - - func healthCheck() { - healthCheckService.healthCheck() - } - - private func savePreferTheFastestNode(_ newValue: Bool) { - UserDefaults.standard.set( - newValue, - forKey: UserDefaults.NodesSource.preferTheFastestNodeKey - ) - } - - private func sendNodesUpdateNotification() { - NotificationCenter.default.post( - name: Notification.Name.NodesSource.nodesUpdate, - object: self, - userInfo: [:] - ) - } - - private func saveNodes() { - $savedNodes.mutate { - guard nodes != $0 else { return } - securedStore.set(nodes, for: StoreKey.NodesSource.nodes) - $0 = securedStore.get(StoreKey.NodesSource.nodes) ?? [] - } - } - - private func loadNodes() { - savedNodes = securedStore.get(StoreKey.NodesSource.nodes) ?? defaultNodesGetter() - nodes = securedStore.get(StoreKey.NodesSource.nodes) ?? defaultNodesGetter() - } - - private func setHealthCheckTimer() { - timer = Timer.scheduledTimer( - withTimeInterval: regularHealthCheckTimeInteval, - repeats: true - ) { [weak healthCheckService] _ in - healthCheckService?.healthCheck() - } - } -} - -extension AdamantNodesSource: HealthCheckDelegate { - func healthCheckUpdate() { - sendNodesUpdateNotification() - saveNodes() - } -} - -private let regularHealthCheckTimeInteval: TimeInterval = 300 -private let preferTheFastestNodeDefault = true diff --git a/Adamant/Services/AdamantPartnerQRService.swift b/Adamant/Services/AdamantPartnerQRService.swift new file mode 100644 index 000000000..93b878d79 --- /dev/null +++ b/Adamant/Services/AdamantPartnerQRService.swift @@ -0,0 +1,72 @@ +// +// PartnerQRService.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 28.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation +import CommonKit +import Combine + +final class AdamantPartnerQRService: PartnerQRService { + + // MARK: Dependencies + + let securedStore: SecuredStore + + // MARK: Proprieties + + @Atomic private var notificationsSet: Set = [] + + // MARK: Lifecycle + + init(securedStore: SecuredStore) { + self.securedStore = securedStore + + NotificationCenter.default + .publisher(for: .AdamantAccountService.userLoggedOut) + .sink { [weak self] _ in + self?.userLoggedOut() + } + .store(in: ¬ificationsSet) + } + + // MARK: Notification actions + + private func userLoggedOut() { + setIncludeNameEnabled(true) + setIncludeURLEnabled(true) + } + + // MARK: Update data + + func setIncludeNameEnabled(_ value: Bool) { + securedStore.set(value, for: StoreKey.partnerQR.includeNameEnabled) + } + + func isIncludeNameEnabled() -> Bool { + guard let result: Bool = securedStore.get( + StoreKey.partnerQR.includeNameEnabled + ) else { + return true + } + + return result + } + + func setIncludeURLEnabled(_ value: Bool) { + securedStore.set(value, for: StoreKey.partnerQR.includeURLEnabled) + } + + func isIncludeURLEnabled() -> Bool { + guard let result: Bool = securedStore.get( + StoreKey.partnerQR.includeURLEnabled + ) else { + return true + } + + return result + } +} diff --git a/Adamant/Services/AdamantPushNotificationsTokenService.swift b/Adamant/Services/AdamantPushNotificationsTokenService.swift index bcc5b0422..00131f3aa 100644 --- a/Adamant/Services/AdamantPushNotificationsTokenService.swift +++ b/Adamant/Services/AdamantPushNotificationsTokenService.swift @@ -46,16 +46,16 @@ final class AdamantPushNotificationsTokenService: PushNotificationsTokenService func sendTokenDeletionTransactions() { for transaction in getTokenDeletionTransactions() { Task { - await apiService.sendTransaction( - path: AdamantApiService.ApiCommands.Chats.processTransaction, + let result = await apiService.sendTransaction( + path: ApiCommands.Chats.processTransaction, transaction: transaction - ) { [weak self] result in - switch result { - case .success, .failure(.accountNotFound), .failure(.notLogged): - self?.removeTokenDeletionTransaction(transaction) - case .failure(.internalError), .failure(.networkError), .failure(.requestCancelled), .failure(.serverError), .failure(.commonError): - break - } + ) + + switch result { + case .success, .failure(.accountNotFound), .failure(.notLogged): + removeTokenDeletionTransaction(transaction) + case .failure(.internalError), .failure(.networkError), .failure(.requestCancelled), .failure(.serverError), .failure(.commonError), .failure(.noEndpointsAvailable): + break } } } @@ -116,16 +116,14 @@ private extension AdamantPushNotificationsTokenService { return completion() } - removeCurrentToken(keypair: keypair) { - Task { [weak self] in - await self?.sendMessageToANS( - keypair: keypair, - encodedPayload: encodedPayload - ) { success in - defer { completion() } - guard success else { return } - self?.setTokenToStorage(newToken) - } + removeCurrentToken(keypair: keypair) { [weak self] in + self?.sendMessageToANS( + keypair: keypair, + encodedPayload: encodedPayload + ) { success in + defer { completion() } + guard success else { return } + self?.setTokenToStorage(newToken) } } } @@ -142,17 +140,15 @@ private extension AdamantPushNotificationsTokenService { setTokenToStorage(nil) - Task { - var transaction: UnregisteredTransaction? - - transaction = await sendMessageToANS( - keypair: keypair, - encodedPayload: encodedPayload - ) { [weak self] success in - defer { completion() } - guard !success, let self = self, let transaction = transaction else { return } - self.addTokenDeletionTransaction(transaction) - } + var transaction: UnregisteredTransaction? + + transaction = sendMessageToANS( + keypair: keypair, + encodedPayload: encodedPayload + ) { [weak self] success in + defer { completion() } + guard !success, let self = self, let transaction = transaction else { return } + self.addTokenDeletionTransaction(transaction) } } @@ -181,8 +177,8 @@ private extension AdamantPushNotificationsTokenService { keypair: Keypair, encodedPayload: EncodedPayload, completion: @escaping (_ success: Bool) -> Void - ) async -> UnregisteredTransaction? { - await apiService.sendMessage( + ) -> UnregisteredTransaction? { + guard let messageTransaction = try? adamantCore.makeSendMessageTransaction( senderId: AdamantUtilities.generateAddress(publicKey: keypair.publicKey), recipientId: AdamantResources.contacts.ansAddress, keypair: keypair, @@ -190,14 +186,18 @@ private extension AdamantPushNotificationsTokenService { type: ChatType.signal, nonce: encodedPayload.nonce, amount: nil - ) { result in - switch result { + ) else { return nil } + + Task { + switch await apiService.sendMessageTransaction(transaction: messageTransaction) { case .success: completion(true) case .failure: completion(false) } } + + return messageTransaction } } diff --git a/Adamant/Services/AdamantVibroService.swift b/Adamant/Services/AdamantVibroService.swift new file mode 100644 index 000000000..9cb8e4be5 --- /dev/null +++ b/Adamant/Services/AdamantVibroService.swift @@ -0,0 +1,35 @@ +// +// AdamantVibroService.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 07.09.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation +import UIKit + +final class AdamantVibroService: VibroService { + func applyVibration(_ type: AdamantVibroType) { + switch type { + case .light: + UIImpactFeedbackGenerator(style: .light).impactOccurred() + case .rigid: + UIImpactFeedbackGenerator(style: .rigid).impactOccurred() + case .heavy: + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() + case .medium: + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + case .soft: + UIImpactFeedbackGenerator(style: .soft).impactOccurred() + case .success: + UINotificationFeedbackGenerator().notificationOccurred(.success) + case .warning: + UINotificationFeedbackGenerator().notificationOccurred(.warning) + case .error: + UINotificationFeedbackGenerator().notificationOccurred(.error) + case .selection: + UISelectionFeedbackGenerator().selectionChanged() + } + } +} diff --git a/Adamant/Services/ApiService/AdamantApi+Accounts.swift b/Adamant/Services/ApiService/AdamantApi+Accounts.swift index 38c3f641b..7b8c6bd71 100644 --- a/Adamant/Services/ApiService/AdamantApi+Accounts.swift +++ b/Adamant/Services/ApiService/AdamantApi+Accounts.swift @@ -9,7 +9,7 @@ import Foundation import CommonKit -extension AdamantApiService.ApiCommands { +extension ApiCommands { static let Accounts = ( root: "/api/accounts", getPublicKey: "/api/accounts/getPublicKey", @@ -20,91 +20,52 @@ extension AdamantApiService.ApiCommands { // MARK: - Accounts extension AdamantApiService { /// Get account by passphrase. - func getAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) { - // MARK: 1. Get keypair from passphrase + func getAccount(byPassphrase passphrase: String) async -> ApiServiceResult { guard let keypair = adamantCore.createKeypairFor(passphrase: passphrase) else { - completion(.failure(.accountNotFound)) - return + return .failure(.accountNotFound) } - // MARK: 2. Send - getAccount(byPublicKey: keypair.publicKey, completion: completion) + return await getAccount(byPublicKey: keypair.publicKey) } /// Get account by publicKey - func getAccount(byPublicKey publicKey: String, completion: @escaping (ApiServiceResult) -> Void) { - sendRequest( - path: ApiCommands.Accounts.root, - queryItems: [URLQueryItem(name: "publicKey", value: publicKey)], - completion: makeCompletionWrapper(publicKey: publicKey, completion: completion) - ) - } - - /// Get account by publicKey - func getAccount(byPublicKey publicKey: String) async throws -> AdamantAccount { - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - sendRequest( + func getAccount(byPublicKey publicKey: String) async -> ApiServiceResult { + switch await request({ apiCore, node in + let response: ApiServiceResult> = await apiCore.sendRequestJsonResponse( + node: node, path: ApiCommands.Accounts.root, - queryItems: [URLQueryItem(name: "publicKey", value: publicKey)], - completion: makeCompletionWrapper(publicKey: publicKey) { response in - switch response { - case .success(let t): - continuation.resume(returning: t) - case .failure(let apiServiceError): - continuation.resume(throwing: apiServiceError) - } - } + method: .get, + parameters: ["publicKey": publicKey], + encoding: .url ) + + return response.flatMap { $0.resolved() } + }) { + case let .success(value): + return .success(value) + case let .failure(error): + switch error { + case .accountNotFound: + return .success(.makeEmptyAccount(publicKey: publicKey)) + default: + return .failure(error) + } } } - func getAccount(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { - sendRequest( - path: ApiCommands.Accounts.root, - queryItems: [URLQueryItem(name: "address", value: address)], - completion: makeCompletionWrapper(publicKey: nil, completion: completion) - ) - } - - func getAccount(byAddress address: String) async throws -> AdamantAccount { - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - sendRequest( + func getAccount(byAddress address: String) async -> ApiServiceResult { + await request { apiCore, node in + let response: ApiServiceResult< + ServerModelResponse + > = await apiCore.sendRequestJsonResponse( + node: node, path: ApiCommands.Accounts.root, - queryItems: [URLQueryItem(name: "address", value: address)], - completion: makeCompletionWrapper(publicKey: nil) { response in - switch response { - case .success(let t): - continuation.resume(returning: t) - case .failure(let apiServiceError): - continuation.resume(throwing: apiServiceError) - } - } + method: .get, + parameters: ["address": address], + encoding: .url ) - } - } -} - -private func makeCompletionWrapper( - publicKey: String?, - completion: @escaping (ApiServiceResult) -> Void -) -> (ApiServiceResult>) -> Void { - { serverResponse in - switch serverResponse { - case .success(let response): - if let model = response.model { - completion(.success(model)) - return - } - - let error = AdamantApiService.translateServerError(response.error) - guard let publicKey = publicKey, error == .accountNotFound else { - completion(.failure(error)) - return - } - completion(.success(.makeEmptyAccount(publicKey: publicKey))) - case .failure(let error): - completion(.failure(.networkError(error: error))) + return response.flatMap { $0.resolved() } } } } diff --git a/Adamant/Services/ApiService/AdamantApi+Chats.swift b/Adamant/Services/ApiService/AdamantApi+Chats.swift index 7c0a348c3..1ec2c866e 100644 --- a/Adamant/Services/ApiService/AdamantApi+Chats.swift +++ b/Adamant/Services/ApiService/AdamantApi+Chats.swift @@ -10,7 +10,7 @@ import Foundation import UIKit import CommonKit -extension AdamantApiService.ApiCommands { +extension ApiCommands { static let Chats = ( root: "/api/chats", get: "/api/chats/get", @@ -21,220 +21,65 @@ extension AdamantApiService.ApiCommands { } extension AdamantApiService { - func getMessageTransactions(address: String, height: Int64?, offset: Int?, completion: @escaping (ApiServiceResult<[Transaction]>) -> Void) { - // MARK: 1. Prepare params - var queryItems: [URLQueryItem] = [URLQueryItem(name: "isIn", value: address), - URLQueryItem(name: "orderBy", value: "timestamp:desc")] - if let height = height, height > 0 { queryItems.append(URLQueryItem(name: "fromHeight", value: String(height))) } - if let offset = offset { queryItems.append(URLQueryItem(name: "offset", value: String(offset))) } - - // MARK: 2. Send - sendRequest( - path: ApiCommands.Chats.get, - queryItems: queryItems - ) { (serverResponse: ApiServiceResult>) in - switch serverResponse { - case .success(let response): - if let collection = response.collection { - completion(.success(collection)) - } else { - let error = AdamantApiService.translateServerError(response.error) - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - } - } - func getMessageTransactions( address: String, height: Int64?, offset: Int? - ) async throws -> [Transaction] { - // MARK: 1. Prepare params - var queryItems: [URLQueryItem] = [URLQueryItem(name: "isIn", value: address), - URLQueryItem(name: "orderBy", value: "timestamp:desc")] - if let height = height, height > 0 { queryItems.append(URLQueryItem(name: "fromHeight", value: String(height))) } - if let offset = offset { queryItems.append(URLQueryItem(name: "offset", value: String(offset))) } + ) async -> ApiServiceResult<[Transaction]> { + var parameters = [ + "isIn": address, + "orderBy": "timestamp:desc" + ] - // MARK: 2. Send - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<[Transaction], Error>) in - sendRequest( - path: ApiCommands.Chats.get, - queryItems: queryItems, - waitsForConnectivity: true - ) { (serverResponse: ApiServiceResult>) in - switch serverResponse { - case .success(let response): - if let collection = response.collection { - continuation.resume(returning: collection) - } else { - let error = AdamantApiService.translateServerError(response.error) - continuation.resume(throwing: error) - } - - case .failure(let error): - continuation.resume(throwing: ApiServiceError.networkError(error: error)) - } - } - } - } - - @discardableResult - func sendMessage( - senderId: String, - recipientId: String, - keypair: Keypair, - message: String, - type: ChatType, - nonce: String, - amount: Decimal?, - completion: @escaping (ApiServiceResult) -> Void - ) -> UnregisteredTransaction? { - let normalizedTransaction = NormalizedTransaction( - type: .chatMessage, - amount: amount ?? .zero, - senderPublicKey: keypair.publicKey, - requesterPublicKey: nil, - date: lastRequestTimeDelta.map { Date().addingTimeInterval(-$0) } ?? Date(), - recipientId: recipientId, - asset: TransactionAsset( - chat: ChatAsset(message: message, ownMessage: nonce, type: type), - state: nil, - votes: nil - ) - ) - - guard let transaction = adamantCore.makeSignedTransaction( - transaction: normalizedTransaction, - senderId: senderId, - keypair: keypair - ) else { - completion(.failure(InternalError.signTransactionFailed.apiServiceErrorWith(error: nil))) - return nil + if let height = height, height > .zero { + parameters["fromHeight"] = String(height) } - sendTransaction(path: ApiCommands.Chats.processTransaction, transaction: transaction) { response in - switch response { - case .success(let response): - if let id = response.transactionId { - completion(.success(id)) - } else { - let error = AdamantApiService.translateServerError(response.error) - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } + if let offset = offset { + parameters["offset"] = String(offset) } - return transaction - } - - func createSendTransaction( - senderId: String, - recipientId: String, - keypair: Keypair, - message: String, - type: ChatType, - nonce: String, - amount: Decimal? - ) -> UnregisteredTransaction? { - let normalizedTransaction = NormalizedTransaction( - type: .chatMessage, - amount: amount ?? .zero, - senderPublicKey: keypair.publicKey, - requesterPublicKey: nil, - date: lastRequestTimeDelta.map { Date().addingTimeInterval(-$0) } ?? Date(), - recipientId: recipientId, - asset: TransactionAsset( - chat: ChatAsset(message: message, ownMessage: nonce, type: type), - state: nil, - votes: nil + let response: ApiServiceResult> + response = await request { [parameters] service, node in + await service.sendRequestJsonResponse( + node: node, + path: ApiCommands.Chats.get, + method: .get, + parameters: parameters, + encoding: .url ) - ) - - guard let transaction = adamantCore.makeSignedTransaction( - transaction: normalizedTransaction, - senderId: senderId, - keypair: keypair - ) else { - return nil } - return transaction + return response.flatMap { $0.resolved() } } - func sendTransaction( + func sendMessageTransaction( transaction: UnregisteredTransaction - ) async throws -> UInt64 { - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - sendTransaction(path: ApiCommands.Chats.processTransaction, transaction: transaction) { response in - switch response { - case .success(let response): - if let id = response.transactionId { - continuation.resume(returning: id) - } else { - let error = AdamantApiService.translateServerError(response.error) - continuation.resume(throwing: error) - } - - case .failure(let error): - continuation.resume(throwing: ApiServiceError.networkError(error: error)) - } - } - } - } - - // new api - - func getChatRooms(address: String, offset: Int?, completion: @escaping (ApiServiceResult) -> Void) { - // MARK: 1. Prepare params - var queryItems: [URLQueryItem] = [] - if let offset = offset { queryItems.append(URLQueryItem(name: "offset", value: String(offset))) } - queryItems.append(URLQueryItem(name: "limit", value: "20")) - - // MARK: 2. Send - sendRequest( - path: ApiCommands.Chats.getChatRooms + "/\(address)", - queryItems: queryItems - ) { (serverResponse: ApiServiceResult) in - switch serverResponse { - case .success(let response): - completion(.success(response)) - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - } + ) async -> ApiServiceResult { + await sendTransaction( + path: ApiCommands.Chats.processTransaction, + transaction: transaction + ) } func getChatRooms( address: String, offset: Int? - ) async throws -> ChatRooms { - // MARK: 1. Prepare params - var queryItems: [URLQueryItem] = [] - if let offset = offset { queryItems.append(URLQueryItem(name: "offset", value: String(offset))) } - queryItems.append(URLQueryItem(name: "limit", value: "20")) + ) async -> ApiServiceResult { + var parameters = ["limit": "20"] + + if let offset = offset { + parameters["offset"] = String(offset) + } - // MARK: 2. Send - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - sendRequest( + return await request { [parameters] service, node in + await service.sendRequestJsonResponse( + node: node, path: ApiCommands.Chats.getChatRooms + "/\(address)", - queryItems: queryItems, - waitsForConnectivity: true - ) { (serverResponse: ApiServiceResult) in - switch serverResponse { - case .success(let response): - continuation.resume(returning: response) - case .failure(let error): - continuation.resume(throwing: ApiServiceError.networkError(error: error)) - } - } + method: .get, + parameters: parameters, + encoding: .url + ) } } @@ -243,31 +88,25 @@ extension AdamantApiService { addressRecipient: String, offset: Int?, limit: Int? - ) async throws -> ChatRooms { - // MARK: 1. Prepare params - var queryItems: [URLQueryItem] = [] + ) async -> ApiServiceResult { + var parameters: [String: String] = [:] + if let offset = offset { - queryItems.append(URLQueryItem(name: "offset", value: String(offset))) + parameters["offset"] = String(offset) } if let limit = limit { - queryItems.append(URLQueryItem(name: "limit", value: String(limit))) + parameters["limit"] = String(limit) } - // MARK: 2. Send - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - sendRequest( + return await request { [parameters] service, node in + await service.sendRequestJsonResponse( + node: node, path: ApiCommands.Chats.getChatRooms + "/\(address)/\(addressRecipient)", - queryItems: queryItems, - waitsForConnectivity: true - ) { (serverResponse: ApiServiceResult) in - switch serverResponse { - case .success(let response): - continuation.resume(returning: response) - case .failure(let error): - continuation.resume(throwing: ApiServiceError.networkError(error: error)) - } - } + method: .get, + parameters: parameters, + encoding: .url + ) } } } diff --git a/Adamant/Services/ApiService/AdamantApi+Delegates.swift b/Adamant/Services/ApiService/AdamantApi+Delegates.swift index 8383a97de..0ce14a3f2 100644 --- a/Adamant/Services/ApiService/AdamantApi+Delegates.swift +++ b/Adamant/Services/ApiService/AdamantApi+Delegates.swift @@ -10,7 +10,7 @@ import Foundation import UIKit import CommonKit -extension AdamantApiService.ApiCommands { +extension ApiCommands { static let Delegates = ( root: "/api/delegates", getDelegates: "/api/delegates", @@ -23,149 +23,127 @@ extension AdamantApiService.ApiCommands { } extension AdamantApiService { - func getDelegates(limit: Int, completion: @escaping (ApiServiceResult<[Delegate]>) -> Void) { - self.getDelegates(limit: limit, offset: 0, currentDelegates: [Delegate](), completion: completion) + func getDelegates(limit: Int) async -> ApiServiceResult<[Delegate]> { + await getDelegates(limit: limit, offset: .zero, currentDelegates: [Delegate]()) } - func getDelegates(limit: Int, offset: Int, currentDelegates: [Delegate], completion: @escaping (ApiServiceResult<[Delegate]>) -> Void) { - sendRequest( - path: ApiCommands.Delegates.getDelegates, - queryItems: [ - URLQueryItem(name: "limit", value: String(limit)), - URLQueryItem(name: "offset", value: String(offset)) - ], - method: .get - ) { (serverResponse: ApiServiceResult>) in - switch serverResponse { - case .success(let delegates): - if let delegates = delegates.collection { - var currentDelegates = currentDelegates - currentDelegates.append(contentsOf: delegates) - - if delegates.count < limit { - completion(.success(currentDelegates)) - } else { - self.getDelegates(limit: limit, offset: offset+limit, currentDelegates: currentDelegates, completion: completion) - } - } else { - completion(.failure(.serverError(error: "No delegates"))) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } + func getDelegates( + limit: Int, + offset: Int, + currentDelegates: [Delegate] + ) async -> ApiServiceResult<[Delegate]> { + let response: ApiServiceResult> + response = await request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: ApiCommands.Delegates.getDelegates, + method: .get, + parameters: ["limit": String(limit), "offset": String(offset)], + encoding: .url + ) + } + + let result = response.flatMap { $0.resolved() } + guard let delegates = try? result.get() else { return result } + let currentDelegates = currentDelegates + delegates + + if delegates.count < limit { + return .success(currentDelegates) + } else { + return await getDelegates( + limit: limit, + offset: offset + limit, + currentDelegates: currentDelegates + ) } } - func getDelegatesWithVotes(for address: String, limit: Int, completion: @escaping (ApiServiceResult<[Delegate]>) -> Void) { - self.getVotes(for: address) { (result) in - switch result { - case .success(let delegates): - let votes = delegates.map({ (delegate) -> String in - return delegate.address - }) + func getDelegatesWithVotes(for address: String, limit: Int) async -> ApiServiceResult<[Delegate]> { + let response = await getVotes(for: address) + + switch response { + case let .success(delegates): + let votes = delegates.map { $0.address } + let delegatesResponse = await getDelegates(limit: limit) + + return delegatesResponse.map { delegates in + var delegatesWithVotes = [Delegate]() - self.getDelegates(limit: limit, completion: { (result) in - switch result { - case .success(let delegates): - var delegatesWithVotes = [Delegate]() - delegates.forEach({ (delegate) in - delegate.voted = votes.contains(delegate.address) - delegatesWithVotes.append(delegate) - }) - - completion(.success(delegatesWithVotes)) - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - }) + delegates.forEach { delegate in + delegate.voted = votes.contains(delegate.address) + delegatesWithVotes.append(delegate) + } - case .failure(let error): - completion(.failure(.networkError(error: error))) + return delegatesWithVotes } + case let .failure(error): + return .failure(error) } } - func getForgedByAccount(publicKey: String, completion: @escaping (ApiServiceResult) -> Void) { - sendRequest( - path: ApiCommands.Delegates.getForgedByAccount, - queryItems: [URLQueryItem(name: "generatorPublicKey", value: publicKey)], - method: .get - ) { (serverResponse: ApiServiceResult) in - switch serverResponse { - case .success(let details): - completion(.success(details)) - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } + func getForgedByAccount(publicKey: String) async -> ApiServiceResult { + await request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: ApiCommands.Delegates.getForgedByAccount, + method: .get, + parameters: ["generatorPublicKey": publicKey], + encoding: .url + ) } } - func getForgingTime(for delegate: Delegate, completion: @escaping (ApiServiceResult) -> Void) { - getNextForgers { (result) in - switch result { - case .success(let nextForgers): - var forgingTime = -1 - if let fIndex = nextForgers.delegates.firstIndex(of: delegate.publicKey) { - forgingTime = fIndex * 10 - } - completion(.success(forgingTime)) - - case .failure(let error): - completion(.failure(.networkError(error: error))) + func getForgingTime(for delegate: Delegate) async -> ApiServiceResult { + await getNextForgers().map { nextForgers in + var forgingTime = -1 + if let fIndex = nextForgers.delegates.firstIndex(of: delegate.publicKey) { + forgingTime = fIndex * 10 } + return forgingTime } } - private func getDelegatesCount(completion: @escaping (ApiServiceResult) -> Void) { - sendRequest( - path: ApiCommands.Delegates.getDelegatesCount, - method: .get - ) { (serverResponse: ApiServiceResult) in - completion(serverResponse) + private func getDelegatesCount() async -> ApiServiceResult { + await request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: ApiCommands.Delegates.getDelegatesCount + ) } } - private func getNextForgers(completion: @escaping (ApiServiceResult) -> Void) { - sendRequest( - path: ApiCommands.Delegates.getNextForgers, - queryItems: [URLQueryItem(name: "limit", value: "\(101)")], - method: .get - ) { (serverResponse: ApiServiceResult) in - completion(serverResponse) + private func getNextForgers() async -> ApiServiceResult { + await request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: ApiCommands.Delegates.getNextForgers, + method: .get, + parameters: ["limit": "\(101)"], + encoding: .url + ) } } - func getVotes(for address: String, completion: @escaping (ApiServiceResult<[Delegate]>) -> Void) { - sendRequest( - path: ApiCommands.Delegates.votes, - queryItems: [URLQueryItem(name: "address", value: address)], - method: .get - ) { (serverResponse: ApiServiceResult>) in - switch serverResponse { - case .success(let delegates): - completion(.success(delegates.collection ?? [])) - case .failure(let error): - completion(.failure(.networkError(error: error))) - } + func getVotes(for address: String) async -> ApiServiceResult<[Delegate]> { + let response: ApiServiceResult> + response = await request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: ApiCommands.Delegates.votes, + method: .get, + parameters: ["address": address], + encoding: .url + ) } + + return response.map { $0.collection ?? .init() } } func voteForDelegates( from address: String, keypair: Keypair, - votes: [DelegateVote], - completion: @escaping (ApiServiceResult) -> Void - ) { - Task { @MainActor in - sendingMsgTaskId = UIApplication.shared.beginBackgroundTask { [weak self] in - guard let self = self else { return } - UIApplication.shared.endBackgroundTask(self.sendingMsgTaskId) - self.sendingMsgTaskId = UIBackgroundTaskIdentifier.invalid - } - } - + votes: [DelegateVote] + ) async -> ApiServiceResult { // MARK: 0. Prepare var votesOrdered = votes _ = votesOrdered.partition { @@ -180,10 +158,10 @@ extension AdamantApiService { // MARK: 1. Create and sign transaction let transaction = NormalizedTransaction( type: .vote, - amount: 0, + amount: .zero, senderPublicKey: keypair.publicKey, requesterPublicKey: nil, - date: Date(), + date: .now, recipientId: address, asset: TransactionAsset(votes: votesAsset) ) @@ -193,57 +171,29 @@ extension AdamantApiService { senderId: address, keypair: keypair ) else { - completion(.failure(.internalError(message: "Failed to sign transaction", error: nil))) - return + return .failure(.internalError(error: InternalAPIError.signTransactionFailed)) } - sendDelegateVoteTransaction( + return await sendDelegateVoteTransaction( path: ApiCommands.Delegates.votes, transaction: transaction - ) { serverResponse in - switch serverResponse { - case .success(let response): - if response.success { - completion(.success(1)) - } else { - completion(.failure(.serverError(error: response.error ?? ""))) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - - Task { @MainActor [weak self] in - guard let self = self else { return } - UIApplication.shared.endBackgroundTask(self.sendingMsgTaskId) - self.sendingMsgTaskId = UIBackgroundTaskIdentifier.invalid - } - } + ) } // MARK: - Private methods - private func getBlocks(completion: @escaping (ApiServiceResult<[Block]>) -> Void) { - sendRequest( - path: ApiCommands.Delegates.getBlocks, - queryItems: [ - URLQueryItem(name: "orderBy", value: "height:desc"), - URLQueryItem(name: "limit", value: "\(101)") - ], - method: .get - ) { (serverResponse: ApiServiceResult>) in - switch serverResponse { - case .success(let blocks): - if let blocks = blocks.collection { - completion(.success(blocks)) - } else { - completion(.failure(.serverError(error: "No delegates"))) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } + private func getBlocks() async -> ApiServiceResult<[Block]> { + let response: ApiServiceResult> = await request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: ApiCommands.Delegates.getBlocks, + method: .get, + parameters: ["orderBy": "height:desc", "limit": "\(101)"], + encoding: .url + ) } + + return response.flatMap { $0.resolved() } } private func getRoundDelegates(delegates: [String], height: UInt64) -> [String] { diff --git a/Adamant/Services/ApiService/AdamantApi+Keys.swift b/Adamant/Services/ApiService/AdamantApi+Keys.swift index fb6c7ab7f..2226421f4 100644 --- a/Adamant/Services/ApiService/AdamantApi+Keys.swift +++ b/Adamant/Services/ApiService/AdamantApi+Keys.swift @@ -10,26 +10,17 @@ import Foundation import CommonKit extension AdamantApiService { - func getPublicKey( - byAddress address: String, - completion: @escaping (ApiServiceResult) -> Void - ) { - sendRequest( - path: ApiCommands.Accounts.getPublicKey, - queryItems: [URLQueryItem(name: "address", value: address)] - ) { (serverResponse: ApiServiceResult) in - switch serverResponse { - case .success(let response): - if let publicKey = response.publicKey { - completion(.success(publicKey)) - } else { - let error = AdamantApiService.translateServerError(response.error) - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } + func getPublicKey(byAddress address: String) async -> ApiServiceResult { + let response: ApiServiceResult = await request { service, node in + await service.sendRequestJsonResponse( + node: node, + path: ApiCommands.Accounts.getPublicKey, + method: .get, + parameters: ["address": address], + encoding: .url + ) } + + return response.flatMap { $0.resolved() } } } diff --git a/Adamant/Services/ApiService/AdamantApi+Peers.swift b/Adamant/Services/ApiService/AdamantApi+Peers.swift deleted file mode 100644 index 5d7175a1d..000000000 --- a/Adamant/Services/ApiService/AdamantApi+Peers.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// AdamantApi+Peers.swift -// Adamant -// -// Created by Anokhov Pavel on 21.06.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantApiService.ApiCommands { - static let Peers = ( - root: "/api/peers", - version: "/api/peers/version" - ) -} - -// MARK: - Peers -extension AdamantApiService { - func getNodeVersion(url: URL, completion: @escaping (ApiServiceResult) -> Void) { - // MARK: 1. Prepare - let endpoint: URL - do { - endpoint = try buildUrl(url: url, path: ApiCommands.Peers.version) - } catch { - let err = InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) - completion(.failure(err)) - return - } - - // MARK: 2. Make request - sendRequest(url: endpoint, method: .get) { (serverResponse: ApiServiceResult) in - switch serverResponse { - case .success(let version): - completion(.success(version)) - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - } - } -} diff --git a/Adamant/Services/ApiService/AdamantApi+States.swift b/Adamant/Services/ApiService/AdamantApi+States.swift index c491519c3..4800bb41b 100644 --- a/Adamant/Services/ApiService/AdamantApi+States.swift +++ b/Adamant/Services/ApiService/AdamantApi+States.swift @@ -10,7 +10,7 @@ import Foundation import UIKit import CommonKit -extension AdamantApiService.ApiCommands { +extension ApiCommands { static let States = ( root: "/api/states", get: "/api/states/get", @@ -19,90 +19,21 @@ extension AdamantApiService.ApiCommands { } extension AdamantApiService { - static let KvsFee: Decimal = 0.001 - func store( - key: String, - value: String, - type: StateType, - sender: String, - keypair: Keypair, - completion: @escaping (ApiServiceResult) -> Void - ) { - Task { @MainActor in - sendingMsgTaskId = UIApplication.shared.beginBackgroundTask { [weak self] in - guard let self = self else { return } - UIApplication.shared.endBackgroundTask(self.sendingMsgTaskId) - self.sendingMsgTaskId = UIBackgroundTaskIdentifier.invalid - } - } - - // MARK: Create and sign transaction - let transaction = NormalizedTransaction( - type: .state, - amount: .zero, - senderPublicKey: keypair.publicKey, - requesterPublicKey: nil, - date: Date(), - recipientId: nil, - asset: TransactionAsset(state: StateAsset(key: key, value: value, type: .keyValue)) - ) - - guard let transaction = adamantCore.makeSignedTransaction( - transaction: transaction, - senderId: sender, - keypair: keypair - ) else { - completion(.failure(.internalError(message: "Failed to sign transaction", error: nil))) - return - } - - // MARK: Send - sendTransaction(path: ApiCommands.States.store, transaction: transaction) { serverResponse in - switch serverResponse { - case .success(let response): - if let id = response.transactionId { - completion(.success(id)) - } else { - completion(ApiServiceResult.success(0)) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - - Task { @MainActor [weak self] in - self?.sendingMsgTaskId = UIApplication.shared.beginBackgroundTask { - guard let self = self else { return } - UIApplication.shared.endBackgroundTask(self.sendingMsgTaskId) - self.sendingMsgTaskId = UIBackgroundTaskIdentifier.invalid - } - } - } - } - func store( key: String, value: String, type: StateType, sender: String, keypair: Keypair - ) async throws -> UInt64 { - Task { @MainActor in - self.sendingMsgTaskId = UIApplication.shared.beginBackgroundTask { - UIApplication.shared.endBackgroundTask(self.sendingMsgTaskId) - self.sendingMsgTaskId = UIBackgroundTaskIdentifier.invalid - } - } - - // MARK: Create and sign transaction + ) async -> ApiServiceResult { let transaction = NormalizedTransaction( type: .state, amount: .zero, senderPublicKey: keypair.publicKey, requesterPublicKey: nil, - date: Date(), + date: .now, recipientId: nil, asset: TransactionAsset(state: StateAsset(key: key, value: value, type: .keyValue)) ) @@ -112,78 +43,38 @@ extension AdamantApiService { senderId: sender, keypair: keypair ) else { - throw ApiServiceError.internalError(message: "Failed to sign transaction", error: nil) + return .failure(.internalError(error: InternalAPIError.signTransactionFailed)) } // MARK: Send - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - sendTransaction(path: ApiCommands.States.store, transaction: transaction) { [weak self] serverResponse in - switch serverResponse { - case .success(let response): - if let id = response.transactionId { - continuation.resume(returning: id) - } else { - continuation.resume(returning: 0) - } - - case .failure(let error): - continuation.resume(throwing: ApiServiceError.networkError(error: error)) - } - - guard let self = self else { return } - Task { @MainActor in - UIApplication.shared.endBackgroundTask(self.sendingMsgTaskId) - self.sendingMsgTaskId = UIBackgroundTaskIdentifier.invalid - } - } - } + return await sendTransaction( + path: ApiCommands.States.store, + transaction: transaction + ) } - func get(key: String, sender: String, completion: @escaping (ApiServiceResult) -> Void) { + func get(key: String, sender: String) async -> ApiServiceResult { // MARK: 1. Prepare - let queryItems = [URLQueryItem(name: "senderId", value: sender), - URLQueryItem(name: "orderBy", value: "timestamp:desc"), - URLQueryItem(name: "key", value: key)] + let parameters = [ + "senderId": sender, + "orderBy": "timestamp:desc", + "key": key + ] - // MARK: 2. Send - sendRequest( - path: ApiCommands.States.get, - queryItems: queryItems - ) { (serverResponse: ApiServiceResult>) in - switch serverResponse { - case .success(let response): - if let collection = response.collection { - if collection.count > 0, let value = collection.first?.asset.state?.value { - completion(.success(value)) - } else { - completion(.success(nil)) - } - } else { - let error = AdamantApiService.translateServerError(response.error) - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - } - } - - func get( - key: String, - sender: String - ) async throws -> String? { - return try await withUnsafeThrowingContinuation { - (continuation: UnsafeContinuation) in - get(key: key, sender: sender) { result in - switch result { - case .success(let data): - continuation.resume(returning: data) - case .failure(let error): - continuation.resume(throwing: error) - } - } + let response: ApiServiceResult> + response = await request { [parameters] core, node in + await core.sendRequestJsonResponse( + node: node, + path: ApiCommands.States.get, + method: .get, + parameters: parameters, + encoding: .url + ) } + + return response + .flatMap { $0.resolved() } + .map { $0.first?.asset.state?.value } } } diff --git a/Adamant/Services/ApiService/AdamantApi+Status.swift b/Adamant/Services/ApiService/AdamantApi+Status.swift deleted file mode 100644 index e0e3101c1..000000000 --- a/Adamant/Services/ApiService/AdamantApi+Status.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// AdamantApi+Status.swift -// Adamant -// -// Created by Андрей on 10.05.2022. -// Copyright © 2022 Adamant. All rights reserved. -// - -import Foundation -import Alamofire - -extension AdamantApiService.ApiCommands { - static let status = "/api/node/status" -} - -extension AdamantApiService { - @discardableResult - func getNodeStatus( - url: URL, - completion: @escaping (ApiServiceResult) -> Void - ) -> DataRequest? { - // MARK: 1. Prepare - let endpoint: URL - do { - endpoint = try buildUrl(url: url, path: ApiCommands.status) - } catch { - let err = InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) - completion(.failure(err)) - return nil - } - - // MARK: 2. Make request - return sendRequest(url: endpoint, method: .get, completion: completion) - } -} diff --git a/Adamant/Services/ApiService/AdamantApi+Transactions.swift b/Adamant/Services/ApiService/AdamantApi+Transactions.swift index 50c9289a0..f63400128 100644 --- a/Adamant/Services/ApiService/AdamantApi+Transactions.swift +++ b/Adamant/Services/ApiService/AdamantApi+Transactions.swift @@ -9,7 +9,7 @@ import Foundation import CommonKit -extension AdamantApiService.ApiCommands { +extension ApiCommands { static let Transactions = ( root: "/api/transactions", getTransaction: "/api/transactions/get", @@ -21,79 +21,61 @@ extension AdamantApiService.ApiCommands { extension AdamantApiService { func sendTransaction( path: String, - transaction: UnregisteredTransaction, - completion: @escaping (ApiServiceResult) -> Void - ) { - sendRequest( - path: path, - method: .post, - body: ["transaction": transaction], - completion: completion - ) + transaction: UnregisteredTransaction + ) async -> ApiServiceResult { + let response: ApiServiceResult = await request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: path, + method: .post, + parameters: ["transaction": transaction], + encoding: .json + ) + } + + return response.flatMap { $0.resolved() } } func sendDelegateVoteTransaction( path: String, - transaction: UnregisteredTransaction, - completion: @escaping (ApiServiceResult) -> Void - ) { - sendRequest( - path: path, - method: .post, - body: transaction, - completion: completion - ) - } - - func getTransaction(id: UInt64, completion: @escaping (ApiServiceResult) -> Void) { - sendRequest( - path: ApiCommands.Transactions.getTransaction, - queryItems: [URLQueryItem(name: "id", value: String(id))] - ) { (serverResponse: ApiServiceResult>) in - switch serverResponse { - case .success(let response): - if let model = response.model { - completion(.success(model)) - } else { - let error = AdamantApiService.translateServerError(response.error) - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } + transaction: UnregisteredTransaction + ) async -> ApiServiceResult { + let response: ApiServiceResult = await request { core, node in + await core.sendRequestJsonResponse( + node: node, + path: path, + method: .post, + parameters: transaction, + encoding: .json + ) + } + + return response.flatMap { + guard let error = $0.error else { return .success($0.success) } + return .failure(.serverError(error: error)) } } - func getTransaction(id: UInt64) async throws -> Transaction { - try await getTransaction(id: id, withAsset: false) + func getTransaction(id: UInt64) async -> ApiServiceResult { + await getTransaction(id: id, withAsset: false) } - func getTransaction(id: UInt64, withAsset: Bool) async throws -> Transaction { - var queryItems = [ - URLQueryItem(name: "id", value: String(id)), - URLQueryItem(name: "returnAsset", value: withAsset ? "1" : "0") - ] - - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - sendRequest( + func getTransaction(id: UInt64, withAsset: Bool) async -> ApiServiceResult { + let response: ApiServiceResult> + response = await request { core, node in + await core.sendRequestJsonResponse( + node: node, path: ApiCommands.Transactions.getTransaction, - queryItems: queryItems - ) { (serverResponse: ApiServiceResult>) in - switch serverResponse { - case .success(let response): - if let model = response.model { - continuation.resume(returning: model) - } else { - let error = AdamantApiService.translateServerError(response.error) - continuation.resume(throwing: error) - } - - case .failure(let error): - continuation.resume(throwing: ApiServiceError.networkError(error: error)) - } - } + method: .get, + parameters: [ + "id": String(id), + "returnAsset": withAsset ? "1" : "0" + ], + encoding: .url + ) } + + return response.flatMap { $0.resolved() } } func getTransactions( @@ -102,8 +84,8 @@ extension AdamantApiService { fromHeight: Int64?, offset: Int?, limit: Int? - ) async throws -> [Transaction] { - try await getTransactions( + ) async -> ApiServiceResult<[Transaction]> { + await getTransactions( forAccount: account, type: type, fromHeight: fromHeight, @@ -120,8 +102,7 @@ extension AdamantApiService { offset: Int?, limit: Int?, orderByTime: Bool? - ) async throws -> [Transaction] { - + ) async -> ApiServiceResult<[Transaction]> { var queryItems = [URLQueryItem(name: "inId", value: account)] if type == .send { @@ -131,9 +112,13 @@ extension AdamantApiService { queryItems.append(URLQueryItem(name: "and:type", value: String(type.rawValue))) } - if let limit = limit { queryItems.append(URLQueryItem(name: "limit", value: String(limit))) } + if let limit = limit { + queryItems.append(URLQueryItem(name: "limit", value: String(limit))) + } - if let offset = offset { queryItems.append(URLQueryItem(name: "offset", value: String(offset))) } + if let offset = offset { + queryItems.append(URLQueryItem(name: "offset", value: String(offset))) + } if let fromHeight = fromHeight, fromHeight > 0 { queryItems.append(URLQueryItem(name: "and:fromHeight", value: String(fromHeight))) @@ -143,24 +128,17 @@ extension AdamantApiService { queryItems.append(URLQueryItem(name: "orderBy", value: "timestamp:desc")) } - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<[Transaction], Error>) in - sendRequest( + let response: ApiServiceResult> + response = await request { [queryItems] core, node in + await core.sendRequestJsonResponse( + node: node, path: ApiCommands.Transactions.root, - queryItems: queryItems - ) { (serverResponse: ApiServiceResult>) in - switch serverResponse { - case .success(let response): - if let collection = response.collection { - continuation.resume(returning: collection) - } else { - let error = AdamantApiService.translateServerError(response.error) - continuation.resume(throwing: error) - } - - case .failure(let error): - continuation.resume(throwing: ApiServiceError.networkError(error: error)) - } - } + method: .get, + parameters: core.emptyParameters, + encoding: .forceQueryItems(queryItems) + ) } + + return response.flatMap { $0.resolved() } } } diff --git a/Adamant/Services/ApiService/AdamantApi+Transfers.swift b/Adamant/Services/ApiService/AdamantApi+Transfers.swift index 2e10d2ca1..e2c9cf7b3 100644 --- a/Adamant/Services/ApiService/AdamantApi+Transfers.swift +++ b/Adamant/Services/ApiService/AdamantApi+Transfers.swift @@ -8,58 +8,29 @@ import Foundation import CommonKit +import CryptoSwift +import BigInt extension AdamantApiService { - func transferFunds(sender: String, recipient: String, amount: Decimal, keypair: Keypair, completion: @escaping (ApiServiceResult) -> Void) { - let normalizedTransaction = NormalizedTransaction( - type: .send, - amount: amount, - senderPublicKey: keypair.publicKey, - requesterPublicKey: nil, - date: lastRequestTimeDelta.map { Date().addingTimeInterval(-$0) } ?? Date(), - recipientId: recipient, - asset: .init() - ) - - guard let transaction = adamantCore.makeSignedTransaction( - transaction: normalizedTransaction, - senderId: sender, - keypair: keypair - ) else { - completion(.failure(InternalError.signTransactionFailed.apiServiceErrorWith(error: nil))) - return - } - - sendTransaction( + func transferFunds(transaction: UnregisteredTransaction) async -> ApiServiceResult { + return await sendTransaction( path: ApiCommands.Transactions.processTransaction, transaction: transaction - ) { response in - switch response { - case .success(let result): - if let id = result.transactionId { - completion(.success(id)) - } else { - completion(.failure(.internalError(message: result.error ?? "Unknown Error", error: nil))) - } - - case .failure(let error): - completion(.failure(error)) - } - } + ) } - + func transferFunds( sender: String, recipient: String, amount: Decimal, keypair: Keypair - ) async throws -> UInt64 { + ) async -> ApiServiceResult { let normalizedTransaction = NormalizedTransaction( type: .send, amount: amount, senderPublicKey: keypair.publicKey, requesterPublicKey: nil, - date: lastRequestTimeDelta.map { Date().addingTimeInterval(-$0) } ?? Date(), + date: .now, recipientId: recipient, asset: .init() ) @@ -69,31 +40,9 @@ extension AdamantApiService { senderId: sender, keypair: keypair ) else { - throw InternalError.signTransactionFailed.apiServiceErrorWith(error: nil) + return .failure(.internalError(error: InternalAPIError.signTransactionFailed)) } - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - sendTransaction( - path: ApiCommands.Transactions.processTransaction, - transaction: transaction - ) { response in - switch response { - case .success(let result): - if let id = result.transactionId { - continuation.resume(returning: id) - } else { - continuation.resume( - throwing: ApiServiceError.internalError( - message: result.error ?? "Unknown Error", - error: nil - ) - ) - } - - case .failure(let error): - continuation.resume(throwing: error) - } - } - } + return await transferFunds(transaction: transaction) } } diff --git a/Adamant/Services/ApiService/AdamantApiCore.swift b/Adamant/Services/ApiService/AdamantApiCore.swift new file mode 100644 index 000000000..1a4861d62 --- /dev/null +++ b/Adamant/Services/ApiService/AdamantApiCore.swift @@ -0,0 +1,49 @@ +// +// AdamantApiCore.swift +// Adamant +// +// Created by Andrew G on 31.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation +import Alamofire +import CommonKit + +extension ApiCommands { + static let status = "/api/node/status" + static let version = "/api/peers/version" +} + +final class AdamantApiCore { + let apiCore: APICoreProtocol + + init(apiCore: APICoreProtocol) { + self.apiCore = apiCore + } + + func getNodeStatus(node: Node) async -> ApiServiceResult { + await apiCore.sendRequestJsonResponse( + node: node, + path: ApiCommands.status + ) + } +} + +extension AdamantApiCore: BlockchainHealthCheckableService { + func getStatusInfo(node: Node) async -> ApiServiceResult { + let startTimestamp = Date.now.timeIntervalSince1970 + let statusResponse = await getNodeStatus(node: node) + let ping = Date.now.timeIntervalSince1970 - startTimestamp + + return statusResponse.map { statusDto in + .init( + ping: ping, + height: statusDto.network?.height ?? .zero, + wsEnabled: statusDto.wsClient?.enabled ?? false, + wsPort: statusDto.wsClient?.port, + version: statusDto.version?.version + ) + } + } +} diff --git a/Adamant/Services/ApiService/AdamantApiService.swift b/Adamant/Services/ApiService/AdamantApiService.swift index 6c13ff585..035a0e3cb 100644 --- a/Adamant/Services/ApiService/AdamantApiService.swift +++ b/Adamant/Services/ApiService/AdamantApiService.swift @@ -6,469 +6,36 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Alamofire import CommonKit -import Combine +import Foundation -actor AdamantApiService: ApiService { - // MARK: - Shared constants - - enum ApiCommands {} - - enum InternalError: Error { - case endpointBuildFailed - case signTransactionFailed - case parsingFailed - case unknownError - case noNodesAvailable - - func apiServiceErrorWith(error: Error?) -> ApiServiceError { - return .internalError(message: self.localized, error: error) - } - - var localized: String { - switch self { - case .endpointBuildFailed: - return .localized("ApiService.InternalError.EndpointBuildFailed", comment: "Serious internal error: Failed to build endpoint url") - - case .signTransactionFailed: - return .localized("ApiService.InternalError.FailedTransactionSigning", comment: "Serious internal error: Failed to sign transaction") - - case .parsingFailed: - return .localized("ApiService.InternalError.ParsingFailed", comment: "Serious internal error: Error parsing response") - - case .unknownError: - return String.adamant.sharedErrors.unknownError - - case .noNodesAvailable: - return .localized("ApiService.InternalError.NoNodesAvailable", comment: "Serious internal error: No nodes available") - } - } - } - - // MARK: - Dependencies - +final class AdamantApiService { let adamantCore: AdamantCore + let service: BlockchainHealthCheckWrapper - weak var nodesSource: NodesSource? { - didSet { - updateCurrentNodes() - } - } - - // MARK: - Properties - - @MainActor - var sendingMsgTaskId: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid - - private(set) var lastRequestTimeDelta: TimeInterval? - private var subscriptions = Set() - private let timeOutInterval: TimeInterval = 15 - - private(set) var currentNodes: [Node] = [] { - didSet { - guard oldValue != currentNodes else { return } - sendCurrentNodeUpdateNotification() - } - } - - private let defaultResponseDispatchQueue = DispatchQueue( - label: "com.adamant.response-queue", - qos: .userInteractive - ) - - private lazy var manager: Session = { - let configuration = AF.sessionConfiguration - configuration.waitsForConnectivity = true - configuration.timeoutIntervalForRequest = timeOutInterval - configuration.timeoutIntervalForResource = timeOutInterval - let manager = Alamofire.Session.init(configuration: configuration) - return manager - }() - - // MARK: - Init - - init(adamantCore: AdamantCore) { - self.adamantCore = adamantCore - Task { await setupSubscriptions() } - } - - // MARK: - Tools - - func buildUrl(url: URL, path: String, queryItems: [URLQueryItem]? = nil) throws -> URL { - guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - throw ApiServiceError.internalError(message: "Failed to build URL from \(url)", error: nil) - } - - components.path = path - components.queryItems = queryItems - - return try components.asURL() - } - - func sendRequest( - path: String, - queryItems: [URLQueryItem]? = nil, - method: HTTPMethod = .get, - waitsForConnectivity: Bool = false, - completion: @escaping (ApiServiceResult) -> Void - ) { - sendRequest( - path: path, - queryItems: queryItems, - method: method, - body: Optional.none, - waitsForConnectivity: waitsForConnectivity, - completion: completion - ) - } - - func sendRequest( - path: String, - queryItems: [URLQueryItem]? = nil, - method: HTTPMethod = .get, - body: Body? = nil, - waitsForConnectivity: Bool = false, - completion: @escaping (ApiServiceResult) -> Void + init( + healthCheckWrapper: BlockchainHealthCheckWrapper, + adamantCore: AdamantCore ) { - guard !currentNodes.isEmpty else { - let error = InternalError.endpointBuildFailed.apiServiceErrorWith( - error: InternalError.noNodesAvailable - ) - completion(.failure(error)) - return - } - - var needNodesUpdate = false - - sendSafeRequest( - nodes: currentNodes, - path: path, - queryItems: queryItems, - method: method, - body: body, - waitsForConnectivity: waitsForConnectivity, - onFailure: { node in - node.connectionStatus = .offline - needNodesUpdate = true - }, - completion: { [weak nodesSource] in - completion($0) - guard needNodesUpdate else { return } - nodesSource?.nodesUpdate() - } - ) - - updateCurrentNodes() - } - - @discardableResult - func sendRequest( - url: URLConvertible, - method: HTTPMethod = .get, - waitsForConnectivity: Bool = false, - completion: @escaping (ApiServiceResult) -> Void - ) -> DataRequest { - sendRequest( - url: url, - method: method, - body: Optional.none, - waitsForConnectivity: waitsForConnectivity, - completion: completion - ) - } - - private func createRequest( - url: URLConvertible, - method: HTTPMethod, - parameters: Parameters?, - encoding: ParameterEncoding, - waitsForConnectivity: Bool, - headers: HTTPHeaders? - ) -> DataRequest { - return manager.request( - url, - method: method, - parameters: parameters, - encoding: encoding, - headers: headers - ) - } - - func sendRequest(request: DataRequest) async throws -> Data { - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - request.responseData(queue: defaultResponseDispatchQueue) { response in - switch response.result { - case .success(let data): - continuation.resume(returning: data) - - case .failure(let error): - continuation.resume(throwing: ApiServiceError.init(error: error)) - } - } - } - } - - @discardableResult - func sendRequest( - url: URLConvertible, - method: HTTPMethod = .get, - body: Body? = nil, - waitsForConnectivity: Bool = false, - completion: @escaping (ApiServiceResult) -> Void - ) -> DataRequest { - let request = createRequest( - url: url, - method: method, - parameters: body?.asDictionary, - encoding: JSONEncoding.default, - waitsForConnectivity: waitsForConnectivity, - headers: HTTPHeaders(["Content-Type": "application/json"]) - ) - - Task { - do { - let data = try await sendRequest(request: request) - - do { - let model = try JSONDecoder().decode(Output.self, from: data) - - if let timestampResponse = model as? ServerResponseWithTimestamp { - let nodeDate = AdamantUtilities.decodeAdamant(timestamp: timestampResponse.nodeTimestamp) - lastRequestTimeDelta = Date().timeIntervalSince(nodeDate) - } - - completion(.success(model)) - } catch { - completion(.failure(InternalError.parsingFailed.apiServiceErrorWith(error: error))) - } - } catch let error as ApiServiceError { - completion(.failure(error)) - } catch { - completion(.failure(.init(error: error))) - } - } - - return request - } - - func sendRequest( - url: URLConvertible, - method: HTTPMethod, - parameters: Parameters? - ) async throws -> Output { - try await sendRequest( - url: url, - method: method, - parameters: parameters, - encoding: URLEncoding.default - ) - } - - func sendRequest( - url: URLConvertible, - method: HTTPMethod, - parameters: Parameters?, - encoding: ParameterEncoding - ) async throws -> Output { - let data = try await sendRequest( - url: url, - method: method, - parameters: parameters, - encoding: encoding - ) - - do { - let model = try JSONDecoder().decode(Output.self, from: data) - return model - } catch { - throw InternalError.parsingFailed.apiServiceErrorWith(error: error) - } - } - - func sendRequest( - url: URLConvertible, - method: HTTPMethod, - parameters: Parameters? - ) async throws -> Data { - try await sendRequest( - url: url, - method: method, - parameters: parameters, - encoding: URLEncoding.default - ) - } - - func sendRequest( - url: URLConvertible, - method: HTTPMethod, - parameters: Parameters?, - encoding: ParameterEncoding - ) async throws -> Data { - return try await sendRequest( - url: url, - method: method, - parameters: parameters, - encoding: encoding, - waitsForConnectivity: false - ) - } - - private func sendRequest( - url: URLConvertible, - method: HTTPMethod, - parameters: Parameters?, - encoding: ParameterEncoding, - waitsForConnectivity: Bool - ) async throws -> Data { - let request = createRequest( - url: url, - method: method, - parameters: parameters, - encoding: encoding, - waitsForConnectivity: waitsForConnectivity, - headers: HTTPHeaders(["Content-Type": "application/json"]) - ) - - return try await sendRequest(request: request) + service = healthCheckWrapper + self.adamantCore = adamantCore } - static func translateServerError(_ error: String?) -> ApiServiceError { - guard let error = error else { - return InternalError.unknownError.apiServiceErrorWith(error: nil) - } - - switch error { - case "Account not found": - return .accountNotFound - - default: - return .serverError(error: error) + func request( + _ request: @Sendable (APICoreProtocol, Node) async -> ApiServiceResult + ) async -> ApiServiceResult { + await service.request { admApiCore, node in + await request(admApiCore.apiCore, node) } } - - func setupWeakDeps(nodesSource: NodesSource) { - self.nodesSource = nodesSource - } } -private extension AdamantApiService { - /// On failure this method doesn't call completion, it just goes to next node. Completion called on success or on last node failure. - func sendSafeRequest( - nodes: [Node], - path: String, - queryItems: [URLQueryItem]?, - method: HTTPMethod, - body: Body?, - waitsForConnectivity: Bool = false, - onFailure: @escaping (Node) -> Void, - completion: @escaping (ApiServiceResult) -> Void - ) { - guard let node = nodes.first else { - completion(.failure(.networkError(error: InternalError.unknownError))) - return - } - - let url: URL - do { - url = try buildUrl(node: node, path: path, queryItems: queryItems) - } catch { - let err = InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) - completion(.failure(err)) - return - } - - sendRequest( - url: url, - method: method, - body: body, - waitsForConnectivity: waitsForConnectivity, - completion: makeSafeRequestCompletion( - nodes: nodes, - path: path, - queryItems: queryItems, - method: method, - body: body, - waitsForConnectivity: waitsForConnectivity, - onFailure: onFailure, - completion: completion - ) - ) - } - - func makeSafeRequestCompletion( - nodes: [Node], - path: String, - queryItems: [URLQueryItem]?, - method: HTTPMethod, - body: Body?, - waitsForConnectivity: Bool = false, - onFailure: @escaping (Node) -> Void, - completion: @escaping (ApiServiceResult) -> Void - ) -> (ApiServiceResult) -> Void { - { [weak self] result in - switch result { - case .success: - completion(result) - case let .failure(error): - switch error { - case .networkError: - var nodes = nodes - onFailure(nodes.removeFirst()) - Task { [weak self, nodes] in - await self?.sendSafeRequest( - nodes: nodes, - path: path, - queryItems: queryItems, - method: method, - body: body, - waitsForConnectivity: waitsForConnectivity, - onFailure: onFailure, - completion: completion - ) - } - case .accountNotFound, .internalError, .notLogged, .serverError, .requestCancelled, .commonError: - completion(result) - } - } - } - } - - func updateCurrentNodes() { - currentNodes = nodesSource?.getAllowedNodes(needWS: false) ?? [] +extension AdamantApiService: ApiService { + var preferredNodeIds: [UUID] { + service.preferredNodeIds } - func sendCurrentNodeUpdateNotification() { - NotificationCenter.default.post( - name: Notification.Name.ApiService.currentNodeUpdate, - object: self, - userInfo: nil - ) - } - - func buildUrl(node: Node, path: String, queryItems: [URLQueryItem]? = nil) throws -> URL { - guard let url = node.asURL() else { throw InternalError.endpointBuildFailed } - return try buildUrl(url: url, path: path, queryItems: queryItems) - } - - func setupSubscriptions() { - NotificationCenter.default - .publisher(for: .NodesSource.nodesUpdate, object: nil) - .sink { _ in Task { [weak self] in await self?.updateCurrentNodes() } } - .store(in: &subscriptions) - } -} - -private extension ApiServiceError { - init(error: Error) { - let afError = error as? AFError - - switch afError { - case .explicitlyCancelled: - self = .requestCancelled - default: - self = .networkError(error: error) - } + func healthCheck() { + service.healthCheck() } } diff --git a/Adamant/Services/BlockchainHealthCheckWrapper.swift b/Adamant/Services/BlockchainHealthCheckWrapper.swift new file mode 100644 index 000000000..e5733b9ec --- /dev/null +++ b/Adamant/Services/BlockchainHealthCheckWrapper.swift @@ -0,0 +1,154 @@ +// +// BlockchainHealthCheckWrapper.swift +// Adamant +// +// Created by Andrew G on 22.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +protocol BlockchainHealthCheckableService { + associatedtype Error: HealthCheckableError + + func getStatusInfo(node: Node) async -> Result +} + +final class BlockchainHealthCheckWrapper< + Service: BlockchainHealthCheckableService +>: HealthCheckWrapper { + private let nodesStorage: NodesStorageProtocol + private let updateNodesAvailabilityLock = NSLock() + + @Atomic private var currentRequests = Set() + + init( + service: Service, + nodesStorage: NodesStorageProtocol, + nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol, + nodeGroup: NodeGroup + ) { + self.nodesStorage = nodesStorage + + super.init( + service: service, + normalUpdateInterval: nodeGroup.normalUpdateInterval, + crucialUpdateInterval: nodeGroup.crucialUpdateInterval, + nodeGroup: nodeGroup + ) + + nodesStorage + .getNodesPublisher(group: nodeGroup) + .sink { [weak self] in self?.nodes = $0 } + .store(in: &subscriptions) + + nodesAdditionalParamsStorage + .fastestNodeMode(group: nodeGroup) + .sink { [weak self] in self?.fastestNodeMode = $0 } + .store(in: &subscriptions) + } + + override func healthCheck() { + super.healthCheck() + + Task { + updateNodesAvailability() + await withTaskGroup(of: Void.self, returning: Void.self) { group in + nodes.filter { $0.isEnabled }.forEach { node in + group.addTask { [weak self] in + guard let self = self, !currentRequests.contains(node.id) else { return } + await updateNodeStatusInfo(node: node) + } + } + + await group.waitForAll() + } + } + } +} + +private extension BlockchainHealthCheckWrapper { + func updateNodeStatusInfo(node: Node) async { + currentRequests.insert(node.id) + defer { currentRequests.remove(node.id) } + + let statusInfo = await service.getStatusInfo(node: node) + nodesStorage.updateNodeStatus(id: node.id, statusInfo: try? statusInfo.get()) + + switch statusInfo { + case .success: + updateNodesAvailability(forceInclude: node.id) + case let .failure(error): + guard !error.isRequestCancelledError else { return } + nodesStorage.updateNodeParams(id: node.id, connectionStatus: .offline) + updateNodesAvailability() + } + } + + func updateNodesAvailability(forceInclude: UUID? = nil) { + updateNodesAvailabilityLock.lock() + defer { updateNodesAvailabilityLock.unlock() } + + let workingNodes = nodes.filter { + $0.isEnabled && ($0.isWorkingStatus || $0.id == forceInclude) + } + + let actualHeightsRange = getActualNodeHeightsRange( + heights: workingNodes.compactMap { $0.height }, + group: nodeGroup + ) + + workingNodes.forEach { node in + let status: Node.ConnectionStatus? = node.height.map { height in + actualHeightsRange?.contains(height) ?? false + ? .allowed + : .synchronizing + } ?? .none + + nodesStorage.updateNodeParams( + id: node.id, + connectionStatus: status + ) + } + } +} + +private extension Node { + var isWorkingStatus: Bool { + switch connectionStatus { + case .allowed, .synchronizing, .none: + return isEnabled + case .offline: + return false + } + } +} + +private struct NodeHeightsInterval { + let range: ClosedRange + var count: Int +} + +private func getActualNodeHeightsRange(heights: [Int], group: NodeGroup) -> ClosedRange? { + let heights = heights.sorted() + var bestInterval: NodeHeightsInterval? + + for i in heights.indices { + var currentInterval = NodeHeightsInterval( + range: heights[i] ... heights[i] + group.nodeHeightEpsilon - 1, + count: 1 + ) + + for j in i + 1 ..< heights.endIndex { + guard currentInterval.range.contains(heights[j]) else { break } + currentInterval.count += 1 + } + + if currentInterval.count >= bestInterval?.count ?? .zero { + bestInterval = currentInterval + } + } + + return bestInterval?.range +} diff --git a/Adamant/Services/DataProviders/AdamantAccountsProvider.swift b/Adamant/Services/DataProviders/AdamantAccountsProvider.swift index 80399e897..a0dda4a56 100644 --- a/Adamant/Services/DataProviders/AdamantAccountsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantAccountsProvider.swift @@ -222,7 +222,7 @@ extension AdamantAccountsProvider { switch validation { case .valid: do { - var account = try await apiService.getAccount(byAddress: address) + var account = try await apiService.getAccount(byAddress: address).get() guard account.publicKey != nil else { account.publicKey = "dummy\(address)" account.isDummy = true diff --git a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift index 17f63f640..b6a4f7d1a 100644 --- a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift +++ b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift @@ -234,7 +234,10 @@ actor AdamantChatTransactionService: ChatTransactionService { let transfer: TransferTransaction if let trs = getTransfer(id: String(transaction.id), context: context) { transfer = trs - transfer.confirmations = transaction.confirmations + // TODO: Fix it later. (Server side) + if transfer.confirmations < transaction.confirmations { + transfer.confirmations = transaction.confirmations + } transfer.statusEnum = .delivered transfer.blockId = transaction.blockId } else { @@ -284,6 +287,7 @@ private extension AdamantChatTransactionService { trs.richContent = richContent trs.richType = type + trs.blockchainType = type trs.transactionStatus = richProviders[type] != nil ? .notInitiated : nil trs.additionalType = .base @@ -348,6 +352,7 @@ private extension AdamantChatTransactionService { trs.richContent = richContent trs.richType = type + trs.blockchainType = type trs.transactionStatus = richProviders[type] != nil ? .notInitiated : nil trs.additionalType = .reply diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider+backgroundFetch.swift b/Adamant/Services/DataProviders/AdamantChatsProvider+backgroundFetch.swift index 8515260cf..202e67d36 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider+backgroundFetch.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider+backgroundFetch.swift @@ -38,7 +38,7 @@ extension AdamantChatsProvider: BackgroundFetchService { address: address, height: lastHeight, offset: nil - ) + ).get() guard transactions.count > 0 else { return .noData } diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift b/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift index 9b30aca53..1fd62a949 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift @@ -226,6 +226,7 @@ extension AdamantChatsProvider { transaction.senderId = sender.address transaction.type = Int16(ChatType.message.rawValue) transaction.isOutgoing = false + transaction.isFake = true transaction.message = text transaction.isUnread = unread transaction.silentNotification = silent diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index b518b95ee..a67fa77ce 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -415,7 +415,7 @@ extension AdamantChatsProvider { func apiGetChatrooms(address: String, offset: Int?) async throws -> ChatRooms? { do { - let chatrooms = try await apiService.getChatRooms(address: address, offset: offset) + let chatrooms = try await apiService.getChatRooms(address: address, offset: offset).get() return chatrooms } catch let error as ApiServiceError { guard case .networkError = error else { @@ -428,6 +428,14 @@ extension AdamantChatsProvider { } func getChatMessages(with addressRecipient: String, offset: Int?) async { + await getChatMessages(with: addressRecipient, offset: offset, loadedCount: .zero) + } + + func getChatMessages( + with addressRecipient: String, + offset: Int?, + loadedCount: Int + ) async { guard let address = accountService.account?.address, let privateKey = accountService.keypair?.privateKey else { return @@ -467,7 +475,8 @@ extension AdamantChatsProvider { result: result, chatroom: chatroom, offset: offset, - addressRecipient: addressRecipient + addressRecipient: addressRecipient, + loadedCount: loadedCount ) } @@ -475,30 +484,40 @@ extension AdamantChatsProvider { result: (reactionsCount: Int, totalCount: Int), chatroom: ChatRooms?, offset: Int?, - addressRecipient: String + addressRecipient: String, + loadedCount: Int ) async { let messageCount = chatroom?.messages?.count ?? 0 let minRectionsCount = result.totalCount * minReactionsProcent / 100 - if result.reactionsCount >= minRectionsCount { - let offset = (offset ?? 0) + messageCount - - let loadedCount = chatLoadedMessages[addressRecipient] ?? 0 - chatLoadedMessages[addressRecipient] = loadedCount + messageCount + let newLoadedCount = loadedCount + (result.totalCount - result.reactionsCount) + + guard result.reactionsCount > minRectionsCount, + newLoadedCount < chatTransactionsLimit + else { + setChatDoneStatus( + for: addressRecipient, + messageCount: messageCount, + maxCount: chatroom?.count + ) - return await getChatMessages( - with: addressRecipient, - offset: offset + NotificationCenter.default.post( + name: .AdamantChatsProvider.initiallyLoadedMessages, + object: addressRecipient ) + return } - setChatDoneStatus( - for: addressRecipient, - messageCount: messageCount, - maxCount: chatroom?.count - ) + let offset = (offset ?? 0) + messageCount + + let loadedCount = chatLoadedMessages[addressRecipient] ?? 0 + chatLoadedMessages[addressRecipient] = loadedCount + messageCount - NotificationCenter.default.post(name: .AdamantChatsProvider.initiallyLoadedMessages, object: addressRecipient) + return await getChatMessages( + with: addressRecipient, + offset: offset, + loadedCount: newLoadedCount + ) } func setChatDoneStatus( @@ -525,7 +544,7 @@ extension AdamantChatsProvider { addressRecipient: addressRecipient, offset: offset, limit: limit - ) + ).get() return chatrooms } catch let error as ApiServiceError { guard case .networkError = error else { @@ -650,7 +669,7 @@ extension AdamantChatsProvider { case .accountNotFound: err = .accountNotFound(address) - case .serverError, .commonError: + case .serverError, .commonError, .noEndpointsAvailable: err = .serverError(error) case .internalError(let message, _): @@ -918,6 +937,7 @@ extension AdamantChatsProvider { transaction.richType = richType transaction.additionalType = additionalType transaction.richContentSerialized = richContentSerialized + transaction.blockchainType = richType transaction.transactionStatus = richProviders[richType] != nil ? .notInitiated : nil @@ -1107,14 +1127,18 @@ extension AdamantChatsProvider { } // MARK: 1. Find. Destroy. Save. + + let chatroom = message.chatroom + let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.parent = stack.container.viewContext privateContext.delete(privateContext.object(with: message.objectID)) - do { try privateContext.save() - return + if let chatroom = chatroom { + await updateLastTransactionForChatrooms([chatroom]) + } } catch { throw ChatsProviderError.internalError(error) } @@ -1150,7 +1174,7 @@ extension AdamantChatsProvider { } // MARK: 2. Create - let signedTransaction = await apiService.createSendTransaction( + let signedTransaction = try? adamantCore.makeSendMessageTransaction( senderId: senderId, recipientId: recipientId, keypair: keypair, @@ -1161,7 +1185,7 @@ extension AdamantChatsProvider { ) guard let signedTransaction = signedTransaction else { - throw ChatsProviderError.internalError(AdamantError(message: AdamantApiService.InternalError.signTransactionFailed.localized)) + throw ChatsProviderError.internalError(AdamantError(message: InternalAPIError.signTransactionFailed.localizedDescription)) } unconfirmedTransactionsBySignature.append(signedTransaction.signature) @@ -1169,7 +1193,7 @@ extension AdamantChatsProvider { // MARK: 3. Send do { - let id = try await apiService.sendTransaction(transaction: signedTransaction) + let id = try await apiService.sendMessageTransaction(transaction: signedTransaction).get() // Update ID with recieved, add to unconfirmed transactions. transaction.transactionId = String(id) @@ -1197,6 +1221,10 @@ extension AdamantChatsProvider { throw ChatsProviderError.notLogged case .serverError(let e), .commonError(let e): throw ChatsProviderError.serverError(AdamantError(message: e)) + case .noEndpointsAvailable: + throw ChatsProviderError.serverError(AdamantError( + message: error.localizedDescription + )) case .internalError(let message, _): throw ChatsProviderError.internalError(AdamantError(message: message)) case .requestCancelled: @@ -1333,7 +1361,7 @@ extension AdamantChatsProvider { address: senderId, height: height, offset: offset - ) + ).get() if transactions.count == 0 { return @@ -1675,7 +1703,7 @@ extension AdamantChatsProvider { if context.hasChanges { try context.save() - await updateContext(rooms: rooms) + await updateLastTransactionForChatrooms(rooms) } } catch { print(error) @@ -1697,13 +1725,14 @@ extension AdamantChatsProvider { receivedLastHeight = height } - @MainActor func updateContext(rooms: [Chatroom]) async { + @MainActor + func updateLastTransactionForChatrooms(_ rooms: [Chatroom]) { let viewContextChatrooms = Set(rooms).compactMap { self.stack.container.viewContext.object(with: $0.objectID) as? Chatroom } for chatroom in viewContextChatrooms { - await chatroom.updateLastTransaction() + chatroom.updateLastTransaction() } } } @@ -1792,10 +1821,10 @@ extension AdamantChatsProvider { } func markChatAsRead(chatroom: Chatroom) { - let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - privateContext.parent = self.stack.container.viewContext - chatroom.markAsReaded() - try? privateContext.save() + chatroom.managedObjectContext?.perform { + chatroom.markAsReaded() + try? chatroom.managedObjectContext?.save() + } } private func onConnectionToTheInternetRestored() { diff --git a/Adamant/Services/DataProviders/AdamantTransfersProvider+backgroundFetch.swift b/Adamant/Services/DataProviders/AdamantTransfersProvider+backgroundFetch.swift index 83f0a8f86..6af87ce7b 100644 --- a/Adamant/Services/DataProviders/AdamantTransfersProvider+backgroundFetch.swift +++ b/Adamant/Services/DataProviders/AdamantTransfersProvider+backgroundFetch.swift @@ -40,7 +40,7 @@ extension AdamantTransfersProvider: BackgroundFetchService { fromHeight: lastHeight, offset: 0, limit: 100 - ) + ).get() let total = transactions.filter({$0.recipientId == address}).count diff --git a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift index e99abedd0..d2a923b68 100644 --- a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift +++ b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift @@ -278,7 +278,7 @@ extension AdamantTransfersProvider { case .accountNotFound: err = .accountNotFound(address: address) - case .serverError, .commonError: + case .serverError, .commonError, .noEndpointsAvailable: err = .serverError(error) case .internalError(let message, _): @@ -378,7 +378,7 @@ extension AdamantTransfersProvider { amount: Decimal, comment: String?, replyToMessageId: String? - ) async throws -> TransactionDetails { + ) async throws -> AdamantTransactionDetails { let comment = comment ?? "" if !comment.isEmpty || replyToMessageId != nil { return try await transferFundsInternal( @@ -400,7 +400,7 @@ extension AdamantTransfersProvider { amount: Decimal, comment: String, replyToMessageId: String? - ) async throws -> TransactionDetails { + ) async throws -> AdamantTransactionDetails { // MARK: 0. Prepare guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { throw TransfersProviderError.notLogged @@ -505,7 +505,7 @@ extension AdamantTransfersProvider { ? .message : .richMessage - let signedTransaction = await apiService.createSendTransaction( + let signedTransaction = try? adamantCore.makeSendMessageTransaction( senderId: loggedAccount.address, recipientId: recipient, keypair: keypair, @@ -517,13 +517,13 @@ extension AdamantTransfersProvider { guard let signedTransaction = signedTransaction else { throw TransfersProviderError.internalError( - message: AdamantApiService.InternalError.signTransactionFailed.localized, + message: InternalAPIError.signTransactionFailed.localizedDescription, error: nil ) } do { - let id = try await apiService.sendTransaction(transaction: signedTransaction) + let id = try await apiService.sendMessageTransaction(transaction: signedTransaction).get() transaction.transactionId = String(id) await chatsProvider?.addUnconfirmed(transactionId: id, managedObjectId: transaction.objectID) @@ -555,7 +555,7 @@ extension AdamantTransfersProvider { private func transferFundsInternal( toAddress recipient: String, amount: Decimal - ) async throws -> TransactionDetails { + ) async throws -> AdamantTransactionDetails { // MARK: 0. Prepare guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { throw TransfersProviderError.notLogged @@ -621,6 +621,21 @@ extension AdamantTransfersProvider { } // MARK: 2. Create transaction + let signedTransaction = adamantCore.createTransferTransaction( + senderId: loggedAccount.address, + recipientId: recipient, + keypair: keypair, + amount: amount + ) + + guard let signedTransaction = signedTransaction else { + throw TransfersProviderError.internalError( + message: InternalAPIError.signTransactionFailed.localizedDescription, + error: InternalAPIError.signTransactionFailed + ) + } + + let locallyID = signedTransaction.generateId() ?? UUID().uuidString let transaction = TransferTransaction(context: context) transaction.amount = amount as NSDecimalNumber transaction.date = Date() as NSDate @@ -631,9 +646,9 @@ extension AdamantTransfersProvider { transaction.showsChatroom = false transaction.fee = Self.transferFee as NSDecimalNumber - transaction.transactionId = UUID().uuidString + transaction.transactionId = locallyID transaction.blockId = nil - transaction.chatMessageId = UUID().uuidString + transaction.chatMessageId = locallyID transaction.statusEnum = MessageStatus.pending // MARK: 3. Chatroom @@ -667,11 +682,8 @@ extension AdamantTransfersProvider { // MARK: 5. Send do { let id = try await apiService.transferFunds( - sender: loggedAccount.address, - recipient: recipient, - amount: amount, - keypair: keypair - ) + transaction: signedTransaction + ).get() transaction.transactionId = String(id) @@ -686,7 +698,7 @@ extension AdamantTransfersProvider { ) } - if let trs = self.stack.container.viewContext.object(with: transaction.objectID) as? TransactionDetails { + if let trs = self.stack.container.viewContext.object(with: transaction.objectID) as? AdamantTransactionDetails { return trs } else { throw TransfersProviderError.internalError( @@ -752,7 +764,7 @@ extension AdamantTransfersProvider { } do { - let transaction = try await apiService.getTransaction(id: intId) + let transaction = try await apiService.getTransaction(id: intId).get() guard transfer.confirmations != transaction.confirmations else { return @@ -816,7 +828,7 @@ extension AdamantTransfersProvider { fromHeight: fromHeight, offset: offset, limit: self.apiTransactions - ) + ).get() guard transactions.count > 0 else { return @@ -864,7 +876,7 @@ extension AdamantTransfersProvider { offset: offset, limit: limit, orderByTime: orderByTime - ) + ).get() guard transactions.count > 0 else { return 0 @@ -1056,7 +1068,7 @@ extension AdamantTransfersProvider { Set(unreadTransactions.compactMap { $0.chatroom }).forEach { $0.hasUnreadMessages = true } } } - + // MARK: 6. Dump transactions to viewContext do { let rooms = transfers.compactMap { $0.chatroom } @@ -1071,12 +1083,12 @@ extension AdamantTransfersProvider { } @MainActor func updateContext(rooms: [Chatroom]) async { - let viewContextChatrooms = Set(rooms).compactMap { - self.stack.container.viewContext.object(with: $0.objectID) as? Chatroom - } - - for chatroom in viewContextChatrooms { - await chatroom.updateLastTransaction() - } - } + let viewContextChatrooms = Set(rooms).compactMap { + self.stack.container.viewContext.object(with: $0.objectID) as? Chatroom + } + + for chatroom in viewContextChatrooms { + chatroom.updateLastTransaction() + } + } } diff --git a/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift b/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift index 16c96b00f..44ef0bb4c 100644 --- a/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift +++ b/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift @@ -9,7 +9,7 @@ import Foundation import CoreData -class InMemoryCoreDataStack: CoreDataStack { +final class InMemoryCoreDataStack: CoreDataStack { let container: NSPersistentContainer init(modelUrl url: URL) throws { diff --git a/Adamant/Services/HealthCheckWrapper.swift b/Adamant/Services/HealthCheckWrapper.swift new file mode 100644 index 000000000..562e1c977 --- /dev/null +++ b/Adamant/Services/HealthCheckWrapper.swift @@ -0,0 +1,151 @@ +// +// HealthCheckWrapper.swift +// Adamant +// +// Created by Andrew G on 22.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import Foundation +import Combine + +protocol HealthCheckableError: Error { + var isNetworkError: Bool { get } + var isRequestCancelledError: Bool { get } + + static func noEndpointsError(coin: String) -> Self +} + +class HealthCheckWrapper { + @ObservableValue var nodes: [Node] = .init() + + let service: Service + let normalUpdateInterval: TimeInterval + let crucialUpdateInterval: TimeInterval + let nodeGroup: NodeGroup + + @Atomic var fastestNodeMode = true + @Atomic var healthCheckTimerSubscription: AnyCancellable? + @Atomic var subscriptions: Set = .init() + + @ObservableValue private var allowedNodes: [Node] = .init() + + var preferredNodeIds: [UUID] { + fastestNodeMode + ? [allowedNodes.first?.id].compactMap { $0 } + : [] + } + + init( + service: Service, + normalUpdateInterval: TimeInterval, + crucialUpdateInterval: TimeInterval, + nodeGroup: NodeGroup + ) { + self.service = service + self.normalUpdateInterval = normalUpdateInterval + self.crucialUpdateInterval = crucialUpdateInterval + self.nodeGroup = nodeGroup + + $nodes + .removeDuplicates { !$0.doesNeedHealthCheck($1) } + .sink { [weak self] _ in self?.healthCheck() } + .store(in: &subscriptions) + + $nodes + .removeDuplicates() + .sink { [weak self] in + self?.allowedNodes = $0.getAllowedNodes( + sortedBySpeedDescending: true, + needWS: false + ) + } + .store(in: &subscriptions) + + $allowedNodes + .map { $0.isEmpty } + .removeDuplicates() + .sink { [weak self] _ in self?.updateHealthCheckTimerSubscription() } + .store(in: &subscriptions) + + NotificationCenter.default + .publisher(for: .AdamantReachabilityMonitor.reachabilityChanged, object: nil) + .compactMap { + $0.userInfo?[AdamantUserInfoKey.ReachabilityMonitor.connection] as? Bool + } + .removeDuplicates() + .filter { $0 == true } + .sink { [weak self] _ in self?.healthCheck() } + .store(in: &subscriptions) + } + + func request( + _ request: @Sendable (Service, Node) async -> Result + ) async -> Result { + var lastConnectionError = allowedNodes.isEmpty + ? Error.noEndpointsError(coin: nodeGroup.name) + : nil + + let nodesList = allowedNodes.isEmpty + ? nodes.filter { $0.isEnabled }.shuffled() + : fastestNodeMode + ? allowedNodes + : allowedNodes.shuffled() + + for node in nodesList { + let response = await request(service, node) + + switch response { + case .success: + return response + case let .failure(error): + guard error.isNetworkError else { return response } + lastConnectionError = error + } + } + + if lastConnectionError != nil { healthCheck() } + return .failure(lastConnectionError ?? Error.noEndpointsError(coin: nodeGroup.name)) + } + + func healthCheck() { + updateHealthCheckTimerSubscription() + } +} + +private extension HealthCheckWrapper { + func updateHealthCheckTimerSubscription() { + healthCheckTimerSubscription = Timer.publish( + every: allowedNodes.isEmpty + ? crucialUpdateInterval + : normalUpdateInterval, + on: .main, + in: .default + ).autoconnect().sink { [weak self] _ in + self?.healthCheck() + } + } +} + +private extension Node { + func doesNeedHealthCheck(_ node: Node) -> Bool { + scheme != node.scheme || + host != node.host || + isEnabled != node.isEnabled || + port != node.port + } +} + +private extension Sequence where Element == Node { + func doesNeedHealthCheck(_ nodes: Nodes) -> Bool where Nodes.Element == Self.Element { + let firstNodes = Dictionary(uniqueKeysWithValues: map { ($0.id, $0) }) + let secondNodes = Dictionary(uniqueKeysWithValues: nodes.map { ($0.id, $0) }) + + guard Set(firstNodes.keys) == Set(secondNodes.keys) else { return true } + + return firstNodes.contains { id, firstNode in + secondNodes[id]?.doesNeedHealthCheck(firstNode) ?? true + } + } +} diff --git a/Adamant/Services/NodesAdditionalParamsStorage.swift b/Adamant/Services/NodesAdditionalParamsStorage.swift new file mode 100644 index 000000000..9dad62d1f --- /dev/null +++ b/Adamant/Services/NodesAdditionalParamsStorage.swift @@ -0,0 +1,62 @@ +// +// NodesAdditionalParamsStorage.swift +// Adamant +// +// Created by Andrew G on 18.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation +import CommonKit +import Combine + +final class NodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol { + @Atomic private var fastestNodeModeValues: ObservableValue<[NodeGroup: Bool]> + + private let securedStore: SecuredStore + private var subscription: AnyCancellable? + + func isFastestNodeMode(group: NodeGroup) -> Bool { + fastestNodeModeValues.wrappedValue[group] ?? group.defaultFastestNodeMode + } + + func fastestNodeMode(group: NodeGroup) -> AnyObservable { + fastestNodeModeValues + .map { $0[group] ?? group.defaultFastestNodeMode } + .removeDuplicates() + .eraseToAnyPublisher() + } + + func setFastestNodeMode(groups: Set, value: Bool) { + $fastestNodeModeValues.mutate { dict in + groups.forEach { + dict.wrappedValue[$0] = value + } + } + } + + func setFastestNodeMode(group: NodeGroup, value: Bool) { + fastestNodeModeValues.wrappedValue[group] = value + } + + init(securedStore: SecuredStore) { + self.securedStore = securedStore + + _fastestNodeModeValues = .init(wrappedValue: .init( + wrappedValue: securedStore.get( + StoreKey.NodesAdditionalParamsStorage.fastestNodeMode + ) ?? [:] + )) + + subscription = fastestNodeModeValues.removeDuplicates().sink { [weak self] in + guard let self = self, subscription != nil else { return } + saveFastestNodeMode($0) + } + } +} + +private extension NodesAdditionalParamsStorage { + func saveFastestNodeMode(_ dict: [NodeGroup: Bool]) { + securedStore.set(dict, for: StoreKey.NodesAdditionalParamsStorage.fastestNodeMode) + } +} diff --git a/Adamant/Services/NodesStorage.swift b/Adamant/Services/NodesStorage.swift new file mode 100644 index 000000000..c2566a59c --- /dev/null +++ b/Adamant/Services/NodesStorage.swift @@ -0,0 +1,143 @@ +// +// NodesStorage.swift +// Adamant +// +// Created by Andrew G on 30.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import CommonKit +import Foundation +import Combine + +final class NodesStorage: NodesStorageProtocol { + @Atomic private var items: ObservableValue<[NodeWithGroup]> + + var nodesWithGroupsPublisher: AnyObservable<[NodeWithGroup]> { + items.removeDuplicates().eraseToAnyPublisher() + } + + private var subscription: AnyCancellable? + private let securedStore: SecuredStore + + func getNodesPublisher(group: NodeGroup) -> AnyObservable<[Node]> { + items + .map { $0.filter { $0.group == group }.map { $0.node } } + .removeDuplicates() + .eraseToAnyPublisher() + } + + func addNode(_ node: Node, group: NodeGroup) { + items.wrappedValue.append(.init(group: group, node: node)) + } + + func removeNode(id: UUID) { + $items.mutate { items in + guard let index = items.wrappedValue.getIndex(id: id) else { return } + items.wrappedValue.remove(at: index) + } + } + + func updateNode( + id: UUID, + scheme: CommonKit.Node.URLScheme?, + host: String?, + isEnabled: Bool?, + wsEnabled: Bool?, + port: Int??, + wsPort: Int??, + version: String??, + height: Int??, + ping: TimeInterval??, + connectionStatus: CommonKit.Node.ConnectionStatus?? + ) { + $items.mutate { items in + guard + let index = items.wrappedValue.getIndex(id: id), + var node = items.wrappedValue[safe: index]?.node + else { return } + + scheme.map { node.scheme = $0 } + host.map { node.host = $0 } + wsEnabled.map { node.wsEnabled = $0 } + port.map { node.port = $0 } + wsPort.map { node.wsPort = $0 } + version.map { node.version = $0 } + height.map { node.height = $0 } + ping.map { node.ping = $0 } + connectionStatus.map { node.connectionStatus = $0 } + + if let isEnabled = isEnabled { + node.isEnabled = isEnabled + + if !isEnabled { + node.connectionStatus = nil + } + } + + items.wrappedValue[index].node = node + } + } + + func resetNodes(group: NodeGroup) { + $items.mutate { items in + items.wrappedValue = items.wrappedValue.filter { + $0.group != group + } + + items.wrappedValue += Self.defaultItems(group: group) + } + } + + init(securedStore: SecuredStore) { + self.securedStore = securedStore + + _items = .init(wrappedValue: .init( + wrappedValue: securedStore.get(StoreKey.NodesStorage.nodes) ?? Self.defaultItems + )) + + subscription = items.removeDuplicates().sink { [weak self] in + guard let self = self, subscription != nil else { return } + saveNodes(nodes: $0) + } + } +} + +private extension NodesStorage { + static func defaultItems(group: NodeGroup) -> [NodeWithGroup] { + switch group { + case .btc: + return BtcWalletService.nodes.map { .init(group: .btc, node: $0) } + case .eth: + return EthWalletService.nodes.map { .init(group: .eth, node: $0) } + case .lskNode: + return LskWalletService.nodes.map { .init(group: .lskNode, node: $0) } + case .lskService: + return LskWalletService.serviceNodes.map { .init(group: .lskService, node: $0) } + case .doge: + return DogeWalletService.nodes.map { .init(group: .doge, node: $0) } + case .dash: + return DashWalletService.nodes.map { .init(group: .dash, node: $0) } + case .adm: + return AdmWalletService.nodes.map { .init(group: .adm, node: $0) } + } + } + + static var defaultItems: [NodeWithGroup] { + NodeGroup.allCases.flatMap { Self.defaultItems(group: $0) } + } + + func saveNodes(nodes: [NodeWithGroup]) { + securedStore.set(nodes, for: StoreKey.NodesStorage.nodes) + } +} + +private extension Array where Element == NodeWithGroup { + func getNode(id: UUID) -> Node? { + first { $0.node.id == id }?.node + } + + func getIndex(id: UUID) -> Int? { + firstIndex { $0.node.id == id } + } +} diff --git a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift index 702c1e1f6..9c82550fc 100644 --- a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift +++ b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift @@ -131,7 +131,7 @@ private extension AdamantRichTransactionReplyService { } func getTransactionFromAPI(by id: UInt64) async throws -> Transaction { - try await apiService.getTransaction(id: id, withAsset: true) + try await apiService.getTransaction(id: id, withAsset: true).get() } func getReplyMessage(from transaction: Transaction) throws -> String { diff --git a/Adamant/Services/RichTransactionStatusService/AdamantRichTransactionStatusService.swift b/Adamant/Services/RichTransactionStatusService/AdamantTransactionStatusService.swift similarity index 71% rename from Adamant/Services/RichTransactionStatusService/AdamantRichTransactionStatusService.swift rename to Adamant/Services/RichTransactionStatusService/AdamantTransactionStatusService.swift index 3af57b333..9cc3a93a0 100644 --- a/Adamant/Services/RichTransactionStatusService/AdamantRichTransactionStatusService.swift +++ b/Adamant/Services/RichTransactionStatusService/AdamantTransactionStatusService.swift @@ -1,5 +1,5 @@ // -// AdamantRichTransactionStatusService.swift +// AdamantTransactionStatusService.swift // Adamant // // Created by Andrey Golubenko on 13.01.2023. @@ -10,9 +10,10 @@ import CoreData import Combine import CommonKit -actor AdamantRichTransactionStatusService: NSObject, RichTransactionStatusService { +actor AdamantTransactionStatusService: NSObject, TransactionStatusService { private let richProviders: [String: RichMessageProviderWithStatusCheck] private let coreDataStack: CoreDataStack + private let nodesStorage: NodesStorageProtocol private lazy var controller = getRichTransactionsController() private var networkSubscription: AnyCancellable? @@ -21,15 +22,17 @@ actor AdamantRichTransactionStatusService: NSObject, RichTransactionStatusServic init( coreDataStack: CoreDataStack, - richProviders: [String: RichMessageProviderWithStatusCheck] + richProviders: [String: RichMessageProviderWithStatusCheck], + nodesStorage: NodesStorageProtocol ) { self.coreDataStack = coreDataStack self.richProviders = richProviders + self.nodesStorage = nodesStorage super.init() Task { await setupNetworkSubscription() } } - func forceUpdate(transaction: RichMessageTransaction) async { + func forceUpdate(transaction: CoinTransaction) async { setStatus(for: transaction, status: .notInitiated) guard @@ -41,7 +44,7 @@ actor AdamantRichTransactionStatusService: NSObject, RichTransactionStatusServic setStatus( for: transaction, status: provider.statusWithFilters( - transaction: transaction, + transaction: transaction as? RichMessageTransaction, oldPendingAttempts: oldPendingAttempts[id]?.wrappedValue ?? .zero, info: await provider.statusInfoFor(transaction: transaction) ) @@ -55,7 +58,7 @@ actor AdamantRichTransactionStatusService: NSObject, RichTransactionStatusServic } } -extension AdamantRichTransactionStatusService: NSFetchedResultsControllerDelegate { +extension AdamantTransactionStatusService: NSFetchedResultsControllerDelegate { nonisolated func controller( _: NSFetchedResultsController, didChange object: Any, @@ -63,22 +66,30 @@ extension AdamantRichTransactionStatusService: NSFetchedResultsControllerDelegat for type: NSFetchedResultsChangeType, newIndexPath _: IndexPath? ) { - guard let transaction = object as? RichMessageTransaction else { return } + guard let transaction = object as? CoinTransaction else { return } Task { await processCoreDataChange(type: type, transaction: transaction) } } } -private extension AdamantRichTransactionStatusService { +private extension AdamantTransactionStatusService { func setupNetworkSubscription() { networkSubscription = NotificationCenter.default .publisher(for: .AdamantReachabilityMonitor.reachabilityChanged) .compactMap { $0.userInfo?[AdamantUserInfoKey.ReachabilityMonitor.connection] as? Bool } - .removeDuplicates() - .sink { connected in - guard connected else { return } + .combineLatest(makeNodesAvailabilitySubscription()) + .removeDuplicates { $0.0 == $1.0 && $0.1 == $1.1 } + .filter { $0.0 } + .sink { _ in Task { [weak self] in await self?.reloadNoNetworkTransactions() } } } + + func makeNodesAvailabilitySubscription() -> some Observable<[UUID]> { + nodesStorage + .nodesWithGroupsPublisher + .map { $0.compactMap { $0.node.isEnabled ? $0.node.id : nil } } + .removeDuplicates() + } func reloadNoNetworkTransactions() { let transactions = controller.fetchedObjects?.filter { @@ -96,13 +107,15 @@ private extension AdamantRichTransactionStatusService { transactions?.forEach { transaction in setStatus(for: transaction, status: .noNetwork) + Task { await forceUpdate(transaction: transaction) } add(transaction: transaction) } } - func add(transaction: RichMessageTransaction) { + func add(transaction: CoinTransaction) { guard - let provider = getProvider(for: transaction) + let provider = getProvider(for: transaction), + transaction.transactionStatus != .success else { return } let id = transaction.transactionId @@ -110,7 +123,7 @@ private extension AdamantRichTransactionStatusService { let oldPendingAttempts = oldPendingAttempts[id] ?? .init(wrappedValue: .zero) self.oldPendingAttempts[id] = oldPendingAttempts - let publisher = RichTransactionStatusPublisher( + let publisher = TransactionStatusPublisher( provider: provider, transaction: transaction, oldPendingAttempts: oldPendingAttempts @@ -123,18 +136,17 @@ private extension AdamantRichTransactionStatusService { } } - func remove(transaction: RichMessageTransaction) { + func remove(transaction: CoinTransaction) { let id = transaction.transactionId subscriptions[id] = nil } - func getProvider(for transaction: RichMessageTransaction) -> RichMessageProviderWithStatusCheck? { - guard let transfer = transaction.transfer else { return nil } - return richProviders[transfer.type] + func getProvider(for transaction: CoinTransaction) -> RichMessageProviderWithStatusCheck? { + return richProviders[transaction.blockchainType] } func setStatus( - for transaction: RichMessageTransaction, + for transaction: CoinTransaction, status: TransactionStatus ) { let privateContext = NSManagedObjectContext( @@ -144,15 +156,19 @@ private extension AdamantRichTransactionStatusService { privateContext.parent = coreDataStack.container.viewContext let transaction = privateContext.object(with: transaction.objectID) - as? RichMessageTransaction + as? CoinTransaction + + guard let transaction = transaction else { + return + } - transaction?.transactionStatus = status + transaction.transactionStatus = status try? privateContext.save() } // MARK: Core Data - func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: RichMessageTransaction) { + func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: CoinTransaction) { switch type { case .insert, .update: add(transaction: transaction) @@ -165,9 +181,9 @@ private extension AdamantRichTransactionStatusService { } } - func getRichTransactionsController() -> NSFetchedResultsController { - let request: NSFetchRequest = NSFetchRequest( - entityName: RichMessageTransaction.entityName + func getRichTransactionsController() -> NSFetchedResultsController { + let request: NSFetchRequest = NSFetchRequest( + entityName: CoinTransaction.entityCoinName ) request.sortDescriptors = [] diff --git a/Adamant/Services/RichTransactionStatusService/RichTransactionStatusPublisher.swift b/Adamant/Services/RichTransactionStatusService/TransactionStatusPublisher.swift similarity index 83% rename from Adamant/Services/RichTransactionStatusService/RichTransactionStatusPublisher.swift rename to Adamant/Services/RichTransactionStatusService/TransactionStatusPublisher.swift index a8f42e643..32f0479b9 100644 --- a/Adamant/Services/RichTransactionStatusService/RichTransactionStatusPublisher.swift +++ b/Adamant/Services/RichTransactionStatusService/TransactionStatusPublisher.swift @@ -10,18 +10,18 @@ import Foundation import Combine import CommonKit -struct RichTransactionStatusPublisher: Publisher { +struct TransactionStatusPublisher: Publisher { typealias Output = TransactionStatus typealias Failure = Never let provider: RichMessageProviderWithStatusCheck - let transaction: RichMessageTransaction + let transaction: CoinTransaction let oldPendingAttempts: ObservableValue func receive( subscriber: S ) where S: Subscriber, Failure == S.Failure, Output == S.Input { - let subscription = RichTransactionStatusSubscription( + let subscription = TransactionStatusSubscription( provider: provider, transaction: transaction, oldPendingAttempts: oldPendingAttempts, diff --git a/Adamant/Services/RichTransactionStatusService/RichTransactionStatusSubscription.swift b/Adamant/Services/RichTransactionStatusService/TransactionStatusSubscription.swift similarity index 86% rename from Adamant/Services/RichTransactionStatusService/RichTransactionStatusSubscription.swift rename to Adamant/Services/RichTransactionStatusService/TransactionStatusSubscription.swift index c5194951f..af9e610bb 100644 --- a/Adamant/Services/RichTransactionStatusService/RichTransactionStatusSubscription.swift +++ b/Adamant/Services/RichTransactionStatusService/TransactionStatusSubscription.swift @@ -10,12 +10,11 @@ import Combine import Foundation import CommonKit -actor RichTransactionStatusSubscription: Subscription where +actor TransactionStatusSubscription: Subscription where StatusSubscriber.Input == TransactionStatus, - StatusSubscriber.Failure == Never -{ + StatusSubscriber.Failure == Never { private let provider: RichMessageProviderWithStatusCheck - private let transaction: RichMessageTransaction + private let transaction: CoinTransaction private let taskManager = TaskManager() private var subscriber: StatusSubscriber? @@ -27,7 +26,7 @@ actor RichTransactionStatusSubscription: Subscript init( provider: RichMessageProviderWithStatusCheck, - transaction: RichMessageTransaction, + transaction: CoinTransaction, oldPendingAttempts: ObservableValue, subscriber: StatusSubscriber ) { @@ -55,8 +54,8 @@ actor RichTransactionStatusSubscription: Subscript break } - status = await provider.statusWithFilters( - transaction: transaction, + status = provider.statusWithFilters( + transaction: transaction as? RichMessageTransaction, oldPendingAttempts: oldPendingAttempts, info: await provider.statusInfoFor(transaction: transaction) ) @@ -69,7 +68,7 @@ actor RichTransactionStatusSubscription: Subscript } } -private extension RichTransactionStatusSubscription { +private extension TransactionStatusSubscription { enum State { case new case old @@ -84,7 +83,7 @@ private extension RichTransactionStatusSubscription { case .registered: return .registered case .pending, .notInitiated, .noNetwork: - guard let sentDate = transaction.sentDate else { return .final } + guard let sentDate = transaction.dateValue else { return .final } let sentInterval = Date.now.timeIntervalSince1970 - sentDate.timeIntervalSince1970 let oldTxInterval = TimeInterval( diff --git a/Adamant/Services/SocketService/AdamantSocketService.swift b/Adamant/Services/SocketService/AdamantSocketService.swift index 3d384d63b..5899bda83 100644 --- a/Adamant/Services/SocketService/AdamantSocketService.swift +++ b/Adamant/Services/SocketService/AdamantSocketService.swift @@ -9,16 +9,11 @@ import Foundation import SocketIO import CommonKit +import Combine final class AdamantSocketService: SocketService { - - // MARK: - Dependencies - - weak var nodesSource: NodesSource? { - didSet { - refreshNode() - } - } + private let nodesStorage: NodesStorageProtocol + private let nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol // MARK: - Properties @@ -26,7 +21,7 @@ final class AdamantSocketService: SocketService { didSet { currentUrl = currentNode?.asSocketURL() - guard oldValue !== currentNode else { return } + guard oldValue?.id != currentNode?.id else { return } sendCurrentNodeUpdateNotification() } } @@ -49,20 +44,25 @@ final class AdamantSocketService: SocketService { @Atomic private var socket: SocketIOClient? @Atomic private var currentAddress: String? @Atomic private var currentHandler: ((ApiServiceResult) -> Void)? + @Atomic private var subscriptions = Set() let defaultResponseDispatchQueue = DispatchQueue( label: "com.adamant.response-queue", qos: .utility ) - init() { - NotificationCenter.default.addObserver( - forName: Notification.Name.NodesSource.nodesUpdate, - object: nil, - queue: nil - ) { [weak self] _ in - self?.refreshNode() - } + init( + nodesStorage: NodesStorageProtocol, + nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol + ) { + self.nodesAdditionalParamsStorage = nodesAdditionalParamsStorage + self.nodesStorage = nodesStorage + + nodesStorage + .getNodesPublisher(group: .adm) + .combineLatest(nodesAdditionalParamsStorage.fastestNodeMode(group: .adm)) + .sink { [weak self] in self?.updateCurrentNode(nodes: $0.0, fastestNode: $0.1) } + .store(in: &subscriptions) } // MARK: - Tools @@ -97,10 +97,6 @@ final class AdamantSocketService: SocketService { manager = nil } - private func refreshNode() { - currentNode = nodesSource?.getAllowedNodes(needWS: true).first - } - private func handleTransaction(data: [Any]) { guard let data = data.first, @@ -126,4 +122,19 @@ final class AdamantSocketService: SocketService { userInfo: nil ) } + + private func updateCurrentNode(nodes: [Node], fastestNode: Bool) { + let allowedNodes = nodes.getAllowedNodes( + sortedBySpeedDescending: fastestNode, + needWS: true + ) + + guard !fastestNode else { + currentNode = allowedNodes.first + return + } + + guard currentNode.map({ !allowedNodes.contains($0) }) ?? true else { return } + currentNode = allowedNodes.randomElement() + } } diff --git a/Adamant/Services/SwinjectedRouter.swift b/Adamant/Services/SwinjectedRouter.swift deleted file mode 100644 index 2446552e1..000000000 --- a/Adamant/Services/SwinjectedRouter.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// SwinjectedRouter.swift -// Adamant -// -// Created by Anokhov Pavel on 07.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import Swinject - -final class SwinjectedRouter: Router { - weak var container: Container? - - @MainActor func get(scene: AdamantScene) -> UIViewController { - return scene.factory(container!) - } -} diff --git a/Adamant/Stories/Shared/ButtonsStripe.xib b/Adamant/SharedViews/ButtonsStripe.xib similarity index 100% rename from Adamant/Stories/Shared/ButtonsStripe.xib rename to Adamant/SharedViews/ButtonsStripe.xib diff --git a/Adamant/Stories/Shared/ButtonsStripeView.swift b/Adamant/SharedViews/ButtonsStripeView.swift similarity index 99% rename from Adamant/Stories/Shared/ButtonsStripeView.swift rename to Adamant/SharedViews/ButtonsStripeView.swift index b8c046577..98e9d175f 100644 --- a/Adamant/Stories/Shared/ButtonsStripeView.swift +++ b/Adamant/SharedViews/ButtonsStripeView.swift @@ -61,7 +61,7 @@ protocol ButtonsStripeViewDelegate: AnyObject { } // MARK: - View -class ButtonsStripeView: UIView { +final class ButtonsStripeView: UIView { // MARK: IBOutlet @IBOutlet weak var stripeStackView: UIStackView! diff --git a/Adamant/SharedViews/FullscreenAlertView.swift b/Adamant/SharedViews/FullscreenAlertView.swift index 71c7c80f8..87ac2d84c 100644 --- a/Adamant/SharedViews/FullscreenAlertView.swift +++ b/Adamant/SharedViews/FullscreenAlertView.swift @@ -9,7 +9,7 @@ import UIKit import CommonKit -class FullscreenAlertView: UIView { +final class FullscreenAlertView: UIView { // MARK: IBOutlets diff --git a/Adamant/SharedViews/SwipeableView.swift b/Adamant/SharedViews/SwipeableView.swift index 80afb5e62..d74db0338 100644 --- a/Adamant/SharedViews/SwipeableView.swift +++ b/Adamant/SharedViews/SwipeableView.swift @@ -9,7 +9,7 @@ import UIKit import SnapKit -class SwipeableView: UIView { +final class SwipeableView: UIView { // MARK: Proprieties diff --git a/Adamant/Stories/Account/AccountRoutes.swift b/Adamant/Stories/Account/AccountRoutes.swift deleted file mode 100644 index df9d76436..000000000 --- a/Adamant/Stories/Account/AccountRoutes.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// AccountRoutes.swift -// Adamant -// -// Created by Anokhov Pavel on 07.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene { - struct Account { - static let account = AdamantScene(identifier: "AccountViewController") { r in - let c = AccountViewController() - c.accountService = r.resolve(AccountService.self) - c.dialogService = r.resolve(DialogService.self) - c.router = r.resolve(Router.self) - c.notificationsService = r.resolve(NotificationsService.self) - c.transfersProvider = r.resolve(TransfersProvider.self) - c.localAuth = r.resolve(LocalAuthentication.self) - c.avatarService = r.resolve(AvatarService.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) - c.visibleWalletsService = r.resolve(VisibleWalletsService.self) - return c - } - - private init() {} - } -} diff --git a/Adamant/Stories/ChatsList/ChatsRoutes.swift b/Adamant/Stories/ChatsList/ChatsRoutes.swift deleted file mode 100644 index 0022ce43c..000000000 --- a/Adamant/Stories/ChatsList/ChatsRoutes.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// ChatsRoutes.swift -// Adamant -// -// Created by Anokhov Pavel on 12.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import CommonKit - -extension AdamantScene { - struct Chats { - static let chatList = AdamantScene(identifier: "ChatListViewController", factory: { r in - let c = ChatListViewController(nibName: "ChatListViewController", bundle: nil) - c.accountService = r.resolve(AccountService.self) - c.chatsProvider = r.resolve(ChatsProvider.self) - c.transfersProvider = r.resolve(TransfersProvider.self) - c.router = r.resolve(Router.self) - c.notificationsService = r.resolve(NotificationsService.self) - c.dialogService = r.resolve(DialogService.self) - c.addressBook = r.resolve(AddressBookService.self) - c.avatarService = r.resolve(AvatarService.self) - - // MARK: RichMessage handlers - // Transfer handlers from accountService' wallet services - if let accountService = r.resolve(AccountService.self) { - for case let provider as RichMessageProvider in accountService.wallets { - c.richMessageProviders[provider.dynamicRichMessageType] = provider - } - } - - return c - }) - - static let chat = AdamantScene(identifier: "ChatViewController", factory: { r in - r.resolve(ChatFactory.self)!.makeViewController() - }) - - static let newChat = AdamantScene(identifier: "NewChatViewController", factory: { r in - let c = NewChatViewController() - c.dialogService = r.resolve(DialogService.self) - c.accountService = r.resolve(AccountService.self) - c.accountsProvider = r.resolve(AccountsProvider.self) - c.router = r.resolve(Router.self) - return c - }) - - static let complexTransfer = AdamantScene(identifier: "ComplexTransferViewController", factory: { r in - let c = ComplexTransferViewController() - c.accountService = r.resolve(AccountService.self) - c.visibleWalletsService = r.resolve(VisibleWalletsService.self) - c.addressBookService = r.resolve(AddressBookService.self) - return c - }) - - static let searchResults = AdamantScene(identifier: "SearchResultsViewController", factory: { r in - let c = SearchResultsViewController( - router: r.resolve(Router.self)!, - avatarService: r.resolve(AvatarService.self)!, - addressBookService: r.resolve(AddressBookService.self)!, - accountsProvider: r.resolve(AccountsProvider.self)! - ) - return c - }) - - private init() {} - } -} diff --git a/Adamant/Stories/Delegates/DelegateRoutes.swift b/Adamant/Stories/Delegates/DelegateRoutes.swift deleted file mode 100644 index 4ac1fb98d..000000000 --- a/Adamant/Stories/Delegates/DelegateRoutes.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// DelegateRoutes.swift -// Adamant -// -// Created by Anton Boyarkin on 06/07/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation -import CommonKit - -extension AdamantScene { - struct Delegates { - static let delegates = AdamantScene(identifier: "DelegatesListViewController", factory: { r in - DelegatesListViewController( - apiService: r.resolve(ApiService.self)!, - accountService: r.resolve(AccountService.self)!, - dialogService: r.resolve(DialogService.self)!, - router: r.resolve(Router.self)! - ) - }) - - static let delegateDetails = AdamantScene(identifier: "DelegateDetailsViewController", factory: { r in - let c = DelegateDetailsViewController(nibName: "DelegateDetailsViewController", bundle: nil) - c.apiService = r.resolve(ApiService.self) - c.accountService = r.resolve(AccountService.self) - c.dialogService = r.resolve(DialogService.self) - return c - }) - - private init() {} - } -} diff --git a/Adamant/Stories/Login/LoginRoutes.swift b/Adamant/Stories/Login/LoginRoutes.swift deleted file mode 100644 index f638070dc..000000000 --- a/Adamant/Stories/Login/LoginRoutes.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// LoginRoutes.swift -// Adamant -// -// Created by Anokhov Pavel on 07.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene { - struct Login { - static let login = AdamantScene(identifier: "LoginViewController", factory: { r in - LoginViewController( - accountService: r.resolve(AccountService.self)!, - adamantCore: r.resolve(AdamantCore.self)!, - dialogService: r.resolve(DialogService.self)!, - localAuth: r.resolve(LocalAuthentication.self)!, - router: r.resolve(Router.self)!, - apiService: r.resolve(ApiService.self)! - ) - }) - - private init() {} - } -} diff --git a/Adamant/Stories/NodesEditor/EurekaNodeRow.swift b/Adamant/Stories/NodesEditor/EurekaNodeRow.swift deleted file mode 100644 index 38a1afc96..000000000 --- a/Adamant/Stories/NodesEditor/EurekaNodeRow.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// EurekaNodeRow.swift -// Adamant -// -// Created by Anokhov Pavel on 20.06.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import SnapKit -import Eureka -import CommonKit - -class NodeCell: Cell, CellType { - struct Model: Equatable { - enum NodeActivity { - case webSockets - case rest - } - - let node: Node - let nodeUpdate: () -> Void - let nodeActivity: (Node) -> Set - - static func == (lhs: Self, rhs: Self) -> Bool { - lhs.node == rhs.node - } - } - - private let checkmarkRowView = CheckmarkRowView() - private var model: NodeCell.Model? { row.value } - - required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setupView() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupView() - } - - public override func update() { - checkmarkRowView.checkmarkImage = .asset(named: "status_success") - checkmarkRowView.setIsChecked(model?.node.isEnabled ?? false, animated: true) - checkmarkRowView.onCheckmarkTap = { [weak self] in - guard let newValue = (self?.checkmarkRowView.isChecked).map({ !$0 }) else { - return - } - - self?.model?.node.isEnabled = newValue - self?.model?.nodeUpdate() - } - - checkmarkRowView.title = model?.node.asString() - checkmarkRowView.captionColor = getIndicatorColor(status: model?.node.connectionStatus) - checkmarkRowView.caption = ["●", makeActivitiesString()] - .compactMap { $0 } - .joined(separator: " ") - - let descriptionStrings = [ - model?.node.statusString, - model?.node.status?.version.map { "(\(NodeCell.Strings.version): \($0))" } - ] - - checkmarkRowView.subtitle = descriptionStrings.compactMap { $0 }.joined(separator: " ") - } - - private func makeActivitiesString() -> String? { - guard let model = model else { return nil } - let activities = model.nodeActivity(model.node) - - guard !activities.isEmpty else { return nil } - - return activities.map { activity in - switch activity { - case .webSockets: - return "ws" - case .rest: - return model.node.scheme.rawValue - } - }.sorted().joined(separator: ", ") - } - - private func setupView() { - contentView.addSubview(checkmarkRowView) - checkmarkRowView.snp.makeConstraints { - $0.directionalEdges.equalToSuperview() - } - } -} - -final class NodeRow: Row, RowType { - required public init(tag: String?) { - super.init(tag: tag) - cellProvider = .init() - } -} - -private extension Node { - var statusString: String? { - switch connectionStatus { - case .allowed: - let ping = status.map { Int($0.ping * 1000) } - return ping.map { "\(NodeCell.Strings.ping): \($0) \(NodeCell.Strings.milliseconds)" } - case .synchronizing: - return NodeCell.Strings.synchronizing - case .offline: - return NodeCell.Strings.offline - case .none: - return nil - } - } -} - -private extension NodeCell { - enum Strings { - static let ping = String.localized( - "NodesList.NodeCell.Ping", - comment: "NodesList.NodeCell: Node ping" - ) - - static let milliseconds = String.localized( - "NodesList.NodeCell.Milliseconds", - comment: "NodesList.NodeCell: Milliseconds" - ) - - static let synchronizing = String.localized( - "NodesList.NodeCell.Synchronizing", - comment: "NodesList.NodeCell: Node is synchronizing" - ) - - static let offline = String.localized( - "NodesList.NodeCell.Offline", - comment: "NodesList.NodeCell: Node is offline" - ) - - static let version = String.localized( - "NodesList.NodeCell.Version", - comment: "NodesList.NodeCell: Node version" - ) - } -} - -private func getIndicatorColor(status: Node.ConnectionStatus?) -> UIColor { - switch status { - case .allowed: - return .adamant.good - case .synchronizing: - return .adamant.alert - case .offline: - return .adamant.danger - case .none: - return .adamant.inactive - } -} diff --git a/Adamant/Stories/NodesEditor/NodesEditorRoutes.swift b/Adamant/Stories/NodesEditor/NodesEditorRoutes.swift deleted file mode 100644 index fa79b3d5a..000000000 --- a/Adamant/Stories/NodesEditor/NodesEditorRoutes.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// NodesEditorRoutes.swift -// Adamant -// -// Created by Anokhov Pavel on 20.06.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import CommonKit - -extension AdamantScene { - struct NodesEditor { - static let nodesList = AdamantScene(identifier: "NodesListViewController", factory: { r in - let c = NodesListViewController() - c.dialogService = r.resolve(DialogService.self) - c.securedStore = r.resolve(SecuredStore.self) - c.apiService = r.resolve(ApiService.self) - c.socketService = r.resolve(SocketService.self) - c.router = r.resolve(Router.self) - c.nodesSource = r.resolve(NodesSource.self) - return c - }) - - static let nodeEditor = AdamantScene(identifier: "", factory: { r in - let c = NodeEditorViewController() - c.dialogService = r.resolve(DialogService.self) - c.apiService = r.resolve(ApiService.self) - return c - }) - - private init() {} - } -} diff --git a/Adamant/Stories/Onboard/OnboardRoutes.swift b/Adamant/Stories/Onboard/OnboardRoutes.swift deleted file mode 100644 index e0a64512a..000000000 --- a/Adamant/Stories/Onboard/OnboardRoutes.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// OnboardRoutes.swift -// Adamant -// -// Created by Anokhov Pavel on 18/01/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene { - struct Onboard { - static let welcome = AdamantScene(identifier: "OnboardViewController") { _ in - let c = OnboardViewController(nibName: "OnboardViewController", bundle: nil) - return c - } - - static let eula = AdamantScene(identifier: "EulaViewController") { _ in - let c = EulaViewController(nibName: "EulaViewController", bundle: nil) - return c - } - } -} diff --git a/Adamant/Stories/Settings/Contribute/ContributeFactory.swift b/Adamant/Stories/Settings/Contribute/ContributeFactory.swift deleted file mode 100644 index bd4b3c670..000000000 --- a/Adamant/Stories/Settings/Contribute/ContributeFactory.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ContributeFactory.swift -// Adamant -// -// Created by Stanislav Jelezoglo on 14.06.2023. -// Copyright © 2023 Adamant. All rights reserved. -// - -import UIKit -import SwiftUI - -@MainActor -struct ContributeFactory { - let crashliticsService: CrashlyticsService - - func makeViewController() -> UIViewController { - UIHostingController( - rootView: ContributeView( - viewModel: .init(crashliticsService: crashliticsService) - ) - ) - } -} diff --git a/Adamant/Stories/Settings/SettingsRoutes.swift b/Adamant/Stories/Settings/SettingsRoutes.swift deleted file mode 100644 index ae0dd3e0a..000000000 --- a/Adamant/Stories/Settings/SettingsRoutes.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// SettingsRoutes.swift -// Adamant -// -// Created by Anokhov Pavel on 01.02.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import CommonKit - -extension AdamantScene { - struct Settings { - static let security = AdamantScene(identifier: "SecurityViewController") { r in - let c = SecurityViewController() - c.accountService = r.resolve(AccountService.self) - c.dialogService = r.resolve(DialogService.self) - c.notificationsService = r.resolve(NotificationsService.self) - c.localAuth = r.resolve(LocalAuthentication.self) - c.router = r.resolve(Router.self) - return c - } - - static let qRGenerator = AdamantScene(identifier: "QRGeneratorViewController") { r in - let c = QRGeneratorViewController() - c.dialogService = r.resolve(DialogService.self) - return c - } - - static let pkGenerator = AdamantScene(identifier: "PKGeneratorViewController") { r in - let c = PKGeneratorViewController() - c.dialogService = r.resolve(DialogService.self) - c.accountService = r.resolve(AccountService.self) - return c - } - - static let about = AdamantScene(identifier: "About") { r in - let c = AboutViewController() - c.accountService = r.resolve(AccountService.self) - c.accountsProvider = r.resolve(AccountsProvider.self) - c.dialogService = r.resolve(DialogService.self) - c.router = r.resolve(Router.self) - return c - } - - static let notifications = AdamantScene(identifier: "Notifications") { r in - let c = NotificationsViewController() - c.notificationsService = r.resolve(NotificationsService.self) - c.dialogService = r.resolve(DialogService.self) - return c - } - - static let visibleWallets = AdamantScene(identifier: "VisibleWallets") { r in - VisibleWalletsViewController( - visibleWalletsService: r.resolve(VisibleWalletsService.self)!, - accountService: r.resolve(AccountService.self)! - ) - } - - static let contribute = AdamantScene(identifier: "Contribute", factory: { r in - r.resolve(ContributeFactory.self)!.makeViewController() - }) - - private init() {} - } -} diff --git a/Adamant/Stories/Shared/SharedRoutes.swift b/Adamant/Stories/Shared/SharedRoutes.swift deleted file mode 100644 index 25164d9dd..000000000 --- a/Adamant/Stories/Shared/SharedRoutes.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// SharedRoutes.swift -// Adamant -// -// Created by Anokhov Pavel on 17.03.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene { - struct Shared { - static let shareQr = AdamantScene(identifier: "ShareQrViewController", factory: { r in - let controller = ShareQrViewController(nibName: "ShareQrViewController", bundle: nil) - controller.dialogService = r.resolve(DialogService.self) - return controller - }) - - private init() {} - } -} diff --git a/Adamant/Utilities/AdamantQRTools.swift b/Adamant/Utilities/AdamantQRTools.swift index ea7aa0e6c..57725b279 100644 --- a/Adamant/Utilities/AdamantQRTools.swift +++ b/Adamant/Utilities/AdamantQRTools.swift @@ -21,7 +21,7 @@ enum QRToolDecodeResult { case failure(error: Error) } -class AdamantQRTools { +final class AdamantQRTools { static func generateQrFrom(string: String, withLogo: Bool = false ) -> QRToolGenerateResult { let generator = EFQRCodeGenerator( content: string, diff --git a/Adamant/Utilities/AdamantUriTools.swift b/Adamant/Utilities/AdamantUriTools.swift index 79c21fcf0..0d7456b72 100644 --- a/Adamant/Utilities/AdamantUriTools.swift +++ b/Adamant/Utilities/AdamantUriTools.swift @@ -12,6 +12,7 @@ import CommonKit enum AdamantUri { case passphrase(passphrase: String) case address(address: String, params: [AdamantAddressParam]?) + case addressLegacy(address: String, params: [AdamantAddressParam]?) } enum AdamantAddressParam { @@ -50,7 +51,7 @@ enum AdamantAddressParam { } } -class AdamantUriTools { +final class AdamantUriTools { static let AdamantProtocol = "adm" static func encode(request: AdamantUri) -> String { @@ -65,7 +66,6 @@ class AdamantUriTools { components.queryItems = [ .init(name: "address", value: address) ] - guard let uri = components.url?.absoluteString else { return "" } params?.forEach { switch $0 { @@ -77,6 +77,29 @@ class AdamantUriTools { components.queryItems?.append(.init(name: "message", value: value)) } } + + guard let uri = components.url?.absoluteString else { return "" } + + return uri + case .addressLegacy(address: let address, params: let params): + var components = URLComponents() + components.scheme = AdmWalletService.qqPrefix + components.host = address + components.queryItems = (params?.count ?? .zero) > .zero ? [] : nil + + params?.forEach { + switch $0 { + case .address: + break + case .label(let value): + components.queryItems?.append(.init(name: "label", value: value)) + case .message(let value): + components.queryItems?.append(.init(name: "message", value: value)) + } + } + + guard let uri = components.url?.absoluteString.replacingOccurrences(of: "://", with: ":") + else { return "" } return uri } diff --git a/Adamant/Wallets/Adamant/AdmTransactionsViewController.swift b/Adamant/Wallets/Adamant/AdmTransactionsViewController.swift deleted file mode 100644 index 120d3ce43..000000000 --- a/Adamant/Wallets/Adamant/AdmTransactionsViewController.swift +++ /dev/null @@ -1,429 +0,0 @@ -// -// AdmTransactionsViewController.swift -// Adamant -// -// Created by Anton Boyarkin on 26/06/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import CoreData -import CommonKit - -class AdmTransactionsViewController: TransactionsListViewControllerBase { - // MARK: - Dependencies - - var accountService: AccountService - var transfersProvider: TransfersProvider - var chatsProvider: ChatsProvider - var dialogService: DialogService - var stack: CoreDataStack - var router: Router - var addressBookService: AddressBookService - - // MARK: - Properties - - var controller: NSFetchedResultsController? - - /* - In SplitViewController on iPhones, viewController can still present in memory, but not on screen. - In this cases not visible viewController will still mark messages isUnread = false - */ - /// ViewController currently is ontop of the screen. - private var isOnTop = false - private let transactionsPerRequest = 100 - - // MARK: - Lifecycle - - init( - nibName nibNameOrNil: String?, - bundle nibBundleOrNil: Bundle?, - accountService: AccountService, - transfersProvider: TransfersProvider, - chatsProvider: ChatsProvider, - dialogService: DialogService, - stack: CoreDataStack, - router: Router, - addressBookService: AddressBookService - ) { - self.accountService = accountService - self.transfersProvider = transfersProvider - self.chatsProvider = chatsProvider - self.dialogService = dialogService - self.stack = stack - self.router = router - self.addressBookService = addressBookService - - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - if accountService.account != nil { - reloadData() - } - - currencySymbol = AdmWalletService.currencySymbol - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - isOnTop = true - markTransfersAsRead() - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - isOnTop = false - } - - // MARK: - Overrides - - @MainActor - override func reloadData() { - Task { - controller = await transfersProvider.transfersController() - controller!.delegate = self - - do { - try controller?.performFetch() - } catch { - dialogService.showError(withMessage: "Failed to get transactions. Please, report a bug", supportEmail: true, error: error) - controller = nil - } - - isBusy = false - self.tableView.reloadData() - } - } - - @MainActor - override func handleRefresh() { - Task { - self.isBusy = true - self.emptyLabel.isHidden = true - - let result = await self.transfersProvider.update() - - guard let result = result else { - refreshControl.endRefreshing() - return - } - - switch result { - case .success: - refreshControl.endRefreshing() - tableView.reloadData() - - case .failure(let error): - refreshControl.endRefreshing() - - dialogService.showRichError(error: error) - } - - self.isBusy = false - }.stored(in: taskManager) - } - - override func loadData(silent: Bool) { - isBusy = true - emptyLabel.isHidden = true - - guard let address = accountService.account?.address else { - return - } - - Task { @MainActor in - do { - let count = try await transfersProvider.getTransactions( - forAccount: address, - type: .send, - offset: transfersProvider.offsetTransactions, - limit: transactionsPerRequest, - orderByTime: true - ) - - if count > 0 { - await transfersProvider.updateOffsetTransactions( - transfersProvider.offsetTransactions + transactionsPerRequest - ) - } - - isNeedToLoadMoore = count >= transactionsPerRequest - } catch { - isNeedToLoadMoore = false - - if !silent { - dialogService.showRichError(error: error) - } - } - - isBusy = false - emptyLabel.isHidden = !isNeedToLoadMoore - refreshControl.endRefreshing() - stopBottomIndicator() - tableView.reloadData() - }.stored(in: taskManager) - } - - private func markTransfersAsRead() { - DispatchQueue.global(qos: .utility).async { - let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - privateContext.parent = self.stack.container.viewContext - - let request = NSFetchRequest(entityName: TransferTransaction.entityName) - request.predicate = NSPredicate(format: "isUnread == true") - request.sortDescriptors = [NSSortDescriptor(key: "transactionId", ascending: false)] - - if let result = try? privateContext.fetch(request) { - result.forEach { $0.isUnread = false } - - if privateContext.hasChanges { - try? privateContext.save() - } - } - } - } - - // MARK: - UITableView - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if let f = controller?.fetchedObjects { - self.emptyLabel.isHidden = f.count > 0 && !refreshControl.isRefreshing - return f.count - } else { - self.emptyLabel.isHidden = false - return 0 - } - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let transaction = controller?.object(at: indexPath) else { - tableView.deselectRow(at: indexPath, animated: true) - return - } - - guard let controller = router.get(scene: AdamantScene.Wallets.Adamant.transactionDetails) as? AdmTransactionDetailsViewController else { - return - } - - controller.transaction = transaction - controller.comment = transaction.comment - - controller.showToChat = toShowChat(for: transaction) - - if let address = accountService.account?.address { - let partnerName = addressBookService.getName(for: transaction.partner) - - if address == transaction.senderId { - controller.senderName = String.adamant.transactionDetails.yourAddress - } else { - controller.senderName = partnerName - } - - if address == transaction.recipientId { - controller.recipientName = String.adamant.transactionDetails.yourAddress - } else { - controller.recipientName = partnerName - } - } - - navigationController?.pushViewController(controller, animated: true) - } - - func configureCell( - _ cell: TransactionTableViewCell, - for transaction: TransferTransaction - ) { - let partnerId = (transaction.isOutgoing ? transaction.recipientId : transaction.senderId) ?? "" - - let amount: Decimal = transaction.amount as Decimal? ?? 0 - - var partnerName = addressBookService.getName(for: transaction.partner) - - if let address = accountService.account?.address, partnerId == address { - partnerName = String.adamant.transactionDetails.yourAddress - } - - configureCell( - cell, - isOutgoing: transaction.isOutgoing, - partnerId: partnerId, - partnerName: partnerName, - amount: amount, - date: transaction.date as Date?) - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let transaction = controller?.object(at: indexPath) else { - return UITableViewCell(style: .default, reuseIdentifier: "cell") - } - - let identifier = transaction.chatroom?.partner?.name != nil ? cellIdentifierFull : cellIdentifierCompact - - guard let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? TransactionTableViewCell else { - return UITableViewCell(style: .default, reuseIdentifier: "cell") - } - - configureCell(cell, for: transaction) - - cell.accessoryType = .disclosureIndicator - cell.separatorInset = indexPath.row == (controller?.fetchedObjects?.count ?? 0) - 1 - ? .zero - : UITableView.defaultTransactionsSeparatorInset - - return cell - } - - func tableView(_ tableView: UITableView, editActionsForRowAt: IndexPath) -> [UITableViewRowAction]? { - guard let transaction = controller?.object(at: editActionsForRowAt), let partner = transaction.partner as? CoreDataAccount, let chatroom = partner.chatroom, let transactions = chatroom.transactions else { - return nil - } - - let messeges = transactions.first(where: { (object) -> Bool in - return !(object is TransferTransaction) - }) - - let title = (messeges != nil) ? String.adamant.transactionList.toChat : String.adamant.transactionList.startChat - - let toChat = UITableViewRowAction(style: .normal, title: title) { _, _ in - guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { - // TODO: Log this - return - } - - guard let account = self.accountService.account else { - return - } - - vc.hidesBottomBarWhenPushed = true - vc.viewModel.setup( - account: account, - chatroom: chatroom, - messageIdToShow: nil, - preservationDelegate: nil - ) - - if let nav = self.navigationController { - nav.pushViewController(vc, animated: true) - } else { - vc.modalPresentationStyle = .overFullScreen - self.present(vc, animated: true) - } - } - - toChat.backgroundColor = UIColor.adamant.primary - - return [toChat] - } - - func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - guard let transaction = controller?.object(at: indexPath) else { - return false - } - - return toShowChat(for: transaction) - } - - @available(iOS 11.0, *) - func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - guard let transaction = controller?.object(at: indexPath), let partner = transaction.partner as? CoreDataAccount, let chatroom = partner.chatroom, let transactions = chatroom.transactions else { - return nil - } - - let messeges = transactions.first(where: { (object) -> Bool in - return !(object is TransferTransaction) - }) - - let title = (messeges != nil) ? String.adamant.transactionList.toChat : String.adamant.transactionList.startChat - - let toChat = UIContextualAction(style: .normal, title: title, handler: { (_, _, _) in - guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { - // TODO: Log this - return - } - - guard let account = self.accountService.account else { - return - } - - vc.hidesBottomBarWhenPushed = true - vc.viewModel.setup( - account: account, - chatroom: chatroom, - messageIdToShow: nil, - preservationDelegate: nil - ) - - if let nav = self.navigationController { - nav.pushViewController(vc, animated: true) - } else { - vc.modalPresentationStyle = .overFullScreen - self.present(vc, animated: true) - } - }) - - toChat.image = .asset(named: "chats_tab") - toChat.backgroundColor = UIColor.adamant.primary - return UISwipeActionsConfiguration(actions: [toChat]) - } - - private func toShowChat(for transaction: TransferTransaction) -> Bool { - guard let partner = transaction.partner as? CoreDataAccount, let chatroom = partner.chatroom, !chatroom.isReadonly else { - return false - } - - return true - } -} - -// MARK: - NSFetchedResultsControllerDelegate -extension AdmTransactionsViewController: NSFetchedResultsControllerDelegate { - func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - if isBusy { return } - tableView.beginUpdates() - } - - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - if isBusy { return } - tableView.endUpdates() - } - - func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { - if isBusy { return } - switch type { - case .insert: - if let newIndexPath = newIndexPath { - tableView.insertRows(at: [newIndexPath], with: .automatic) - - if isOnTop, let transaction = anObject as? TransferTransaction { - transaction.isUnread = false - } - } - - case .delete: - if let indexPath = indexPath { - tableView.deleteRows(at: [indexPath], with: .automatic) - } - - case .update: - if let indexPath = indexPath, - let cell = self.tableView.cellForRow(at: indexPath) as? TransactionTableViewCell, - let transaction = anObject as? TransferTransaction { - configureCell(cell, for: transaction) - } - - case .move: - if let at = indexPath, let to = newIndexPath { - tableView.moveRow(at: at, to: to) - } - @unknown default: - break - } - } -} diff --git a/Adamant/Wallets/Adamant/AdmWalletRoutes.swift b/Adamant/Wallets/Adamant/AdmWalletRoutes.swift deleted file mode 100644 index a30bf523a..000000000 --- a/Adamant/Wallets/Adamant/AdmWalletRoutes.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// AdmWalletRoutes.swift -// Adamant -// -// Created by Anokhov Pavel on 28.08.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene.Wallets { - struct Adamant { - /// Wallet preview - static let wallet = AdamantScene(identifier: "AdmWalletViewController") { r in - let c = AdmWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) - c.router = r.resolve(Router.self) - c.accountService = r.resolve(AccountService.self) - return c - } - - /// Send money - static let transfer = AdamantScene(identifier: "AdmTransferViewController") { r in - AdmTransferViewController( - chatsProvider: r.resolve(ChatsProvider.self)!, - accountService: r.resolve(AccountService.self)!, - accountsProvider: r.resolve(AccountsProvider.self)!, - dialogService: r.resolve(DialogService.self)!, - router: r.resolve(Router.self)!, - currencyInfoService: r.resolve(CurrencyInfoService.self)!, - increaseFeeService: r.resolve(IncreaseFeeService.self)! - ) - } - - /// Transactions list - static let transactionsList = AdamantScene(identifier: "AdmTransactionsViewController", factory: { r in - AdmTransactionsViewController( - nibName: "TransactionsListViewControllerBase", - bundle: nil, - accountService: r.resolve(AccountService.self)!, - transfersProvider: r.resolve(TransfersProvider.self)!, - chatsProvider: r.resolve(ChatsProvider.self)!, - dialogService: r.resolve(DialogService.self)!, - stack: r.resolve(CoreDataStack.self)!, - router: r.resolve(Router.self)!, - addressBookService: r.resolve(AddressBookService.self)! - ) - }) - - /// Adamant transaction details - static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewController", factory: { r in - AdmTransactionDetailsViewController( - accountService: r.resolve(AccountService.self)!, - transfersProvider: r.resolve(TransfersProvider.self)!, - router: r.resolve(Router.self)!, - dialogService: r.resolve(DialogService.self)!, - currencyInfo: r.resolve(CurrencyInfoService.self)!, - addressBookService: r.resolve(AddressBookService.self)! - ) - }) - - /// Buy and Sell options - static let buyAndSell = AdamantScene(identifier: "BuyAndSell") { r in - let c = BuyAndSellViewController() - c.accountService = r.resolve(AccountService.self) - c.dialogService = r.resolve(DialogService.self) - return c - } - - private init() {} - } -} diff --git a/Adamant/Wallets/Bitcoin/BtcTransactionsViewController.swift b/Adamant/Wallets/Bitcoin/BtcTransactionsViewController.swift deleted file mode 100644 index 44c49091e..000000000 --- a/Adamant/Wallets/Bitcoin/BtcTransactionsViewController.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// BtcTransactionsViewController.swift -// Adamant -// -// Created by Anton Boyarkin on 30/01/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import UIKit -import BitcoinKit -import CommonKit - -class BtcTransactionsViewController: TransactionsListViewControllerBase { - - // MARK: - Dependencies - var btcWalletService: BtcWalletService! - var dialogService: DialogService! - var router: Router! - var addressBook: AddressBookService! - - // MARK: - Properties - var transactions: [BtcTransaction] = [] - - override func viewDidLoad() { - super.viewDidLoad() - updateLoadingView(isHidden: false) - currencySymbol = BtcWalletService.currencySymbol - handleRefresh() - } - - override func handleRefresh() { - transactions.removeAll() - tableView.reloadData() - loadData(silent: false) - } - - override func loadData(silent: Bool) { - isBusy = true - - Task { @MainActor in - do { - let trs = try await btcWalletService.getTransactions(fromTx: transactions.last?.txId) - transactions.append(contentsOf: trs) - isNeedToLoadMoore = trs.count > 0 - } catch { - isNeedToLoadMoore = false - if !silent { - dialogService.showRichError(error: error) - } - } - - isBusy = false - emptyLabel.isHidden = transactions.count > 0 - stopBottomIndicator() - refreshControl.endRefreshing() - tableView.reloadData() - updateLoadingView(isHidden: true) - }.stored(in: taskManager) - } - - // MARK: - UITableView - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return transactions.count - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - let transaction = transactions[indexPath.row] - - guard let controller = router.get(scene: AdamantScene.Wallets.Bitcoin.transactionDetails) as? BtcTransactionDetailsViewController else { - return - } - - controller.transaction = transaction - controller.service = btcWalletService - - if let address = btcWalletService.wallet?.address { - if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { - controller.senderName = String.adamant.transactionDetails.yourAddress - } - if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { - controller.recipientName = String.adamant.transactionDetails.yourAddress - } - } - - navigationController?.pushViewController(controller, animated: true) - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierCompact, for: indexPath) as? TransactionTableViewCell else { - // TODO: Display & Log error - return UITableViewCell(style: .default, reuseIdentifier: "cell") - } - - let transaction = transactions[indexPath.row] - - cell.accessoryType = .disclosureIndicator - cell.separatorInset = indexPath.row == transactions.count - 1 - ? .zero - : UITableView.defaultTransactionsSeparatorInset - - configureCell(cell, for: transaction) - return cell - } - - func configureCell(_ cell: TransactionTableViewCell, for transaction: BtcTransaction) { - let outgoing = transaction.isOutgoing - let partnerId = outgoing ? transaction.recipientAddress : transaction.senderAddress - - var partnerName: String? - if let address = btcWalletService.wallet?.address { - if partnerId == address { - partnerName = String.adamant.transactionDetails.yourAddress - } else { - partnerName = addressBook.getName(for: address) - } - } - - configureCell(cell, - isOutgoing: outgoing, - partnerId: partnerId, - partnerName: partnerName, - amount: transaction.amountValue ?? 0, - date: transaction.dateValue) - } -} diff --git a/Adamant/Wallets/Bitcoin/BtcWalletRoutes.swift b/Adamant/Wallets/Bitcoin/BtcWalletRoutes.swift deleted file mode 100644 index b04c0809e..000000000 --- a/Adamant/Wallets/Bitcoin/BtcWalletRoutes.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// BtcWalletRoutes.swift -// Adamant -// -// Created by Anton Boyarkin on 14/01/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene.Wallets { - struct Bitcoin { - /// Wallet preview - static let wallet = AdamantScene(identifier: "BtcWalletViewController") { r in - let c = BtcWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) - c.accountService = r.resolve(AccountService.self) - return c - } - - /// Send BTC tokens - static let transfer = AdamantScene(identifier: "BtcTransferViewController") { r in - BtcTransferViewController( - chatsProvider: r.resolve(ChatsProvider.self)!, - accountService: r.resolve(AccountService.self)!, - accountsProvider: r.resolve(AccountsProvider.self)!, - dialogService: r.resolve(DialogService.self)!, - router: r.resolve(Router.self)!, - currencyInfoService: r.resolve(CurrencyInfoService.self)!, - increaseFeeService: r.resolve(IncreaseFeeService.self)! - ) - } - - /// List of BTC transactions - static let transactionsList = AdamantScene(identifier: "BtcTransactionsViewController") { r in - let c = BtcTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.router = r.resolve(Router.self) - c.addressBook = r.resolve(AddressBookService.self) - return c - } - - /// BTC transaction details - static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewControllerBase") { r in - BtcTransactionDetailsViewController( - dialogService: r.resolve(DialogService.self)!, - currencyInfo: r.resolve(CurrencyInfoService.self)!, - addressBookService: r.resolve(AddressBookService.self)!, - accountService: r.resolve(AccountService.self)! - ) - } - } -} diff --git a/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift b/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift deleted file mode 100644 index 62c34138f..000000000 --- a/Adamant/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// BtcWalletService+RichMessageProvider.swift -// Adamant -// -// Created by Anton Boyarkin on 20/02/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import Foundation -import MessageKit -import UIKit -import CommonKit - -extension BtcWalletService: RichMessageProvider { - var newPendingInterval: TimeInterval { - .init(milliseconds: type(of: self).newPendingInterval) - } - - var oldPendingInterval: TimeInterval { - .init(milliseconds: type(of: self).oldPendingInterval) - } - - var registeredInterval: TimeInterval { - .init(milliseconds: type(of: self).registeredInterval) - } - - var newPendingAttempts: Int { - type(of: self).newPendingAttempts - } - - var oldPendingAttempts: Int { - type(of: self).oldPendingAttempts - } - - var dynamicRichMessageType: String { - return type(of: self).richMessageType - } - - // MARK: Events - - @MainActor - func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { - // MARK: 0. Prepare - guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) - else { - return - } - - let comment: String? - if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { - comment = raw - } else { - comment = nil - } - - // MARK: Go to transaction - - presentDetailTransactionVC( - hash: hash, - senderId: transaction.senderId, - recipientId: transaction.recipientId, - senderAddress: "", - recipientAddress: "", - comment: comment, - transaction: nil, - richTransaction: transaction, - in: chat - ) - } - - private func presentDetailTransactionVC( - hash: String, - senderId: String?, - recipientId: String?, - senderAddress: String, - recipientAddress: String, - comment: String?, - transaction: BtcTransaction?, - richTransaction: RichMessageTransaction, - in chat: ChatViewController - ) { - guard let vc = router.get(scene: AdamantScene.Wallets.Bitcoin.transactionDetails) as? BtcTransactionDetailsViewController else { - return - } - - let amount: Decimal - if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { - amount = decimal - } else { - amount = 0 - } - - let failedTransaction = SimpleTransactionDetails( - txId: hash, - senderAddress: senderAddress, - recipientAddress: recipientAddress, - dateValue: nil, - amountValue: amount, - feeValue: nil, - confirmationsValue: nil, - blockValue: nil, - isOutgoing: richTransaction.isOutgoing, - transactionStatus: nil - ) - - vc.service = self - vc.senderId = senderId - vc.recipientId = recipientId - vc.comment = comment - vc.transaction = transaction ?? failedTransaction - vc.richTransaction = richTransaction - - chat.navigationController?.pushViewController(vc, animated: true) - } - - // MARK: Short description - - func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { - let amount: String - - guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) - else { - return NSAttributedString(string: "⬅️ \(BtcWalletService.currencySymbol)") - } - - if let decimal = Decimal(string: raw) { - amount = AdamantBalanceFormat.full.format(decimal) - } else { - amount = raw - } - - let string: String - if transaction.isOutgoing { - string = "⬅️ \(amount) \(BtcWalletService.currencySymbol)" - } else { - string = "➡️ \(amount) \(BtcWalletService.currencySymbol)" - } - - return NSAttributedString(string: string) - } -} diff --git a/Adamant/Wallets/Bitcoin/BtcWalletService+Send.swift b/Adamant/Wallets/Bitcoin/BtcWalletService+Send.swift deleted file mode 100644 index 49dbca503..000000000 --- a/Adamant/Wallets/Bitcoin/BtcWalletService+Send.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// BtcWalletService+Send.swift -// Adamant -// -// Created by Anton Boyarkin on 08/02/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import UIKit -import Alamofire -import BitcoinKit - -extension BtcWalletService: WalletServiceTwoStepSend { - typealias T = BitcoinKit.Transaction - - func transferViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Bitcoin.transfer) as? BtcTransferViewController else { - fatalError("Can't get BtcTransferViewController") - } - - vc.service = self - return vc - } - - // MARK: Create & Send - func createTransaction(recipient: String, amount: Decimal) async throws -> BitcoinKit.Transaction { - // MARK: 1. Prepare - guard let wallet = self.btcWallet else { - throw WalletServiceError.notLogged - } - - let key = wallet.privateKey - - guard let toAddress = try? addressConverter.convert(address: recipient) else { - throw WalletServiceError.accountNotFound - } - - let rawAmount = NSDecimalNumber(decimal: amount * BtcWalletService.multiplier).uint64Value - let fee = NSDecimalNumber(decimal: self.transactionFee * BtcWalletService.multiplier).uint64Value - - // MARK: 2. Search for unspent transactions - - let utxos = try await getUnspentTransactions() - - // MARK: 3. Check if we have enought money - - let totalAmount: UInt64 = UInt64(utxos.reduce(0) { $0 + $1.output.value }) - guard totalAmount >= rawAmount + fee else { // This shit can crash BitcoinKit - throw WalletServiceError.notEnoughMoney - } - - // MARK: 4. Create local transaction - - let transaction = BitcoinKit.Transaction.createNewTransaction( - toAddress: toAddress, - amount: rawAmount, - fee: fee, - changeAddress: wallet.addressEntity, - utxos: utxos, - keys: [key] - ) - - return transaction - } - - func sendTransaction(_ transaction: BitcoinKit.Transaction) async throws { - guard let url = BtcWalletService.nodes.randomElement()?.asURL() else { - throw WalletServiceError.internalError( - message: "Failed to get BTC endpoint URL", - error: nil - ) - } - - // Request url - let endpoint = url.appendingPathComponent(BtcApiCommands.sendTransaction()) - - // MARK: Prepare params - - let txHex = transaction.serialized().hex - - // MARK: Sending request - - let responseData = try await apiService.sendRequest( - url: endpoint, - method: .post, - parameters: nil, - encoding: BodyStringEncoding(body: txHex) - ) - - let response = String(decoding: responseData, as: UTF8.self) - guard response != transaction.txId else { return } - throw WalletServiceError.remoteServiceError(message: response) - } - - func getUnspentTransactions() async throws -> [UnspentTransaction] { - guard let url = BtcWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get BTC endpoint URL") - } - - guard let wallet = self.btcWallet else { - throw WalletServiceError.notLogged - } - - let address = wallet.address - - // Headers - let headers: HTTPHeaders = [ - "Content-Type": "application/json" - ] - - // Request url - let endpoint = url.appendingPathComponent(BtcApiCommands.getUnspentTransactions(for: address)) - - let parameters: Parameters = [ - "noCache": "1" - ] - - // MARK: Sending request - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<[UnspentTransaction], Error>) in - AF.request(endpoint, method: .get, parameters: parameters, headers: headers).responseData(queue: defaultDispatchQueue) { response in - switch response.result { - case .success(let data): - guard - let items = try? Self.jsonDecoder.decode([BtcUnspentTransactionResponse].self, - from: data) - else { - continuation.resume(throwing: WalletServiceError.internalError(message: "BTC Wallet: not valid response", error: nil)) - break - } - - var utxos = [UnspentTransaction]() - for item in items { - guard item.status.confirmed else { - continue - } - - let value = NSDecimalNumber(decimal: item.value).uint64Value - - let lockScript = wallet.addressEntity.lockingScript - let txHash = Data(hex: item.txId).map { Data($0.reversed()) } ?? Data() - let txIndex = item.vout - - let unspentOutput = TransactionOutput(value: value, lockingScript: lockScript) - let unspentOutpoint = TransactionOutPoint(hash: txHash, index: txIndex) - let utxo = UnspentTransaction(output: unspentOutput, outpoint: unspentOutpoint) - - utxos.append(utxo) - } - continuation.resume(returning: utxos) - return - case .failure: - continuation.resume(throwing: WalletServiceError.internalError(message: "BTC Wallet: server not response", error: nil)) - return - } - } - } - } - -} - -struct BodyStringEncoding: ParameterEncoding { - - private let body: String - - init(body: String) { self.body = body } - - func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { - guard var urlRequest = urlRequest.urlRequest else { throw Errors.emptyURLRequest } - guard let data = body.data(using: .utf8) else { throw Errors.encodingProblem } - urlRequest.httpBody = data - return urlRequest - } -} - -extension BodyStringEncoding { - enum Errors: Error { - case emptyURLRequest - case encodingProblem - } -} - -extension BodyStringEncoding.Errors: LocalizedError { - var errorDescription: String? { - switch self { - case .emptyURLRequest: return "Empty url request" - case .encodingProblem: return "Encoding problem" - } - } -} diff --git a/Adamant/Wallets/Dash/DashTransactionsViewController.swift b/Adamant/Wallets/Dash/DashTransactionsViewController.swift deleted file mode 100644 index 239b5ab93..000000000 --- a/Adamant/Wallets/Dash/DashTransactionsViewController.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// DashTransactionsViewController.swift -// Adamant -// -// Created by Anton Boyarkin on 19/05/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import UIKit -import ProcedureKit - -class DashTransactionsViewController: TransactionsListViewControllerBase { - - // MARK: - Dependencies - var walletService: DashWalletService! - var dialogService: DialogService! - var router: Router! - - // MARK: - Properties - var transactions: [DashTransaction] = [] - private var allTransactionsIds: [String] = [] - private var offset = 0 - private var maxPerRequest = 25 - - override func viewDidLoad() { - super.viewDidLoad() - updateLoadingView(isHidden: false) - currencySymbol = DashWalletService.currencySymbol - handleRefresh() - } - - override func handleRefresh() { - transactions.removeAll() - tableView.reloadData() - allTransactionsIds.removeAll() - offset = 0 - - loadData(silent: true) - } - - override func loadData(silent: Bool) { - guard let address = walletService.wallet?.address else { - transactions = [] - return - } - - isBusy = true - emptyLabel.isHidden = true - - Task { @MainActor in - do { - if allTransactionsIds.isEmpty { - allTransactionsIds = try await walletService.requestTransactionsIds(for: address).reversed() - } - - let availableToLoad = allTransactionsIds.count - offset - - let maxPerRequest = availableToLoad > maxPerRequest - ? maxPerRequest - : availableToLoad - - let ids = Array(allTransactionsIds[offset..<(offset + maxPerRequest)]) - - let trs = try await walletService.getTransactions(by: ids) - - transactions.append(contentsOf: trs) - offset += trs.count - isNeedToLoadMoore = allTransactionsIds.count - offset > 0 - } catch { - isNeedToLoadMoore = false - - if !silent { - dialogService.showRichError(error: error) - } - } - - isBusy = false - emptyLabel.isHidden = transactions.count > 0 - stopBottomIndicator() - refreshControl.endRefreshing() - tableView.reloadData() - updateLoadingView(isHidden: true) - }.stored(in: taskManager) - } - - // MARK: - UITableView - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return transactions.count - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let controller = router.get(scene: AdamantScene.Wallets.Dash.transactionDetails) as? DashTransactionDetailsViewController else { - fatalError("Failed to getDashTransactionDetailsViewController") - } - - // Hold reference - guard let address = walletService.wallet?.address else { - return - } - - controller.service = self.walletService - - let transaction = transactions[indexPath.row] - - let isOutgoing: Bool = transaction.recipientAddress != address - - let emptyTransaction = SimpleTransactionDetails( - txId: transaction.txId, - senderAddress: transaction.senderAddress, - recipientAddress: transaction.recipientAddress, - dateValue: nil, - amountValue: transaction.amountValue, - feeValue: nil, - confirmationsValue: nil, - blockValue: nil, - isOutgoing: isOutgoing, - transactionStatus: nil - ) - - controller.transaction = emptyTransaction - - if emptyTransaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { - controller.senderName = String.adamant.transactionDetails.yourAddress - } - - if emptyTransaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { - controller.recipientName = String.adamant.transactionDetails.yourAddress - } - - navigationController?.pushViewController(controller, animated: true) - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierCompact, for: indexPath) as? TransactionTableViewCell else { - // TODO: Display & Log error - return UITableViewCell(style: .default, reuseIdentifier: "cell") - } - - let transaction = transactions[indexPath.row] - - cell.accessoryType = .disclosureIndicator - cell.separatorInset = indexPath.row == transactions.count - 1 - ? .zero - : UITableView.defaultTransactionsSeparatorInset - - configureCell(cell, for: transaction) - return cell - } - - func configureCell(_ cell: TransactionTableViewCell, for transaction: BaseBtcTransaction) { - let outgoing = transaction.isOutgoing - let partnerId = outgoing ? transaction.recipientAddress : transaction.senderAddress - - let partnerName: String? - if let address = walletService.wallet?.address, partnerId == address { - partnerName = String.adamant.transactionDetails.yourAddress - } else { - partnerName = nil - } - - configureCell(cell, - isOutgoing: outgoing, - partnerId: partnerId, - partnerName: partnerName, - amount: transaction.amountValue ?? 0, - date: transaction.dateValue) - } -} - -private class LoadMoreDashTransactionsProcedure: Procedure { - let service: DashWalletService - - private(set) var result: DashTransactionsPointer? - - init(service: DashWalletService) { - self.service = service - - super.init() - - log.severity = .warning - } - - override func execute() { - service.getNextTransaction { result in - switch result { - case .success(let result): - self.result = result - self.finish() - - case .failure(let error): - self.result = nil - self.finish(with: error) - } - } - } -} diff --git a/Adamant/Wallets/Dash/DashWalletRouter.swift b/Adamant/Wallets/Dash/DashWalletRouter.swift deleted file mode 100644 index a4e11270a..000000000 --- a/Adamant/Wallets/Dash/DashWalletRouter.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// DashWalletRouter.swift -// Adamant -// -// Created by Anton Boyarkin on 25/04/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene.Wallets { - struct Dash { - /// Wallet preview - static let wallet = AdamantScene(identifier: "DashWalletViewController") { r in - let c = DashWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) - c.accountService = r.resolve(AccountService.self) - return c - } - - /// Send tokens - static let transfer = AdamantScene(identifier: "DashTransferViewController") { r in - DashTransferViewController( - chatsProvider: r.resolve(ChatsProvider.self)!, - accountService: r.resolve(AccountService.self)!, - accountsProvider: r.resolve(AccountsProvider.self)!, - dialogService: r.resolve(DialogService.self)!, - router: r.resolve(Router.self)!, - currencyInfoService: r.resolve(CurrencyInfoService.self)!, - increaseFeeService: r.resolve(IncreaseFeeService.self)! - ) - } - - /// List of transactions - static let transactionsList = AdamantScene(identifier: "DashTransactionsViewController") { r in - let c = DashTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.router = r.resolve(Router.self) - return c - } - - /// Transaction details - static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewControllerBase") { r in - DashTransactionDetailsViewController( - dialogService: r.resolve(DialogService.self)!, - currencyInfo: r.resolve(CurrencyInfoService.self)!, - addressBookService: r.resolve(AddressBookService.self)!, - accountService: r.resolve(AccountService.self)! - ) - } - } -} diff --git a/Adamant/Wallets/Dash/DashWalletService+RichMessageProvider.swift b/Adamant/Wallets/Dash/DashWalletService+RichMessageProvider.swift deleted file mode 100644 index a9a38a16f..000000000 --- a/Adamant/Wallets/Dash/DashWalletService+RichMessageProvider.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// DashWalletService+RichMessageProvider.swift -// Adamant -// -// Created by Anton Boyarkin on 26/05/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import Foundation -import MessageKit -import UIKit -import CommonKit - -extension DashWalletService: RichMessageProvider { - var newPendingInterval: TimeInterval { - .init(milliseconds: type(of: self).newPendingInterval) - } - - var oldPendingInterval: TimeInterval { - .init(milliseconds: type(of: self).oldPendingInterval) - } - - var registeredInterval: TimeInterval { - .init(milliseconds: type(of: self).registeredInterval) - } - - var newPendingAttempts: Int { - type(of: self).newPendingAttempts - } - - var oldPendingAttempts: Int { - type(of: self).oldPendingAttempts - } - - var dynamicRichMessageType: String { - return type(of: self).richMessageType - } - - // MARK: Events - - @MainActor - func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { - // MARK: 0. Prepare - guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash), - let address = accountService.account?.address - else { - return - } - - let comment: String? - if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { - comment = raw - } else { - comment = nil - } - - // MARK: Go to transaction - - presentDetailTransactionVC( - hash: hash, - senderId: transaction.senderId, - recipientId: transaction.recipientId, - senderAddress: "", - recipientAddress: "", - comment: comment, - address: address, - blockId: nil, - transaction: nil, - richTransaction: transaction, - in: chat - ) - } - - private func presentDetailTransactionVC( - hash: String, - senderId: String?, - recipientId: String?, - senderAddress: String, - recipientAddress: String, - comment: String?, - address: String, - blockId: String?, - transaction: BTCRawTransaction?, - richTransaction: RichMessageTransaction, - in chat: ChatViewController - ) { - guard let vc = router.get(scene: AdamantScene.Wallets.Dash.transactionDetails) as? DashTransactionDetailsViewController else { - return - } - - let amount: Decimal - if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { - amount = decimal - } else { - amount = 0 - } - - var dashTransaction = transaction?.asBtcTransaction(DashTransaction.self, for: address) - if let blockId = blockId { - dashTransaction = transaction?.asBtcTransaction(DashTransaction.self, for: address, blockId: blockId) - } - let failedTransaction = SimpleTransactionDetails( - txId: hash, - senderAddress: senderAddress, - recipientAddress: recipientAddress, - dateValue: nil, - amountValue: amount, - feeValue: nil, - confirmationsValue: nil, - blockValue: nil, - isOutgoing: richTransaction.isOutgoing, - transactionStatus: nil - ) - - vc.service = self - vc.senderId = senderId - vc.recipientId = recipientId - vc.comment = comment - vc.transaction = dashTransaction ?? failedTransaction - vc.richTransaction = richTransaction - - chat.navigationController?.pushViewController(vc, animated: true) - } - - // MARK: Short description - - func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { - let amount: String - - guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) - else { - return NSAttributedString(string: "⬅️ \(DashWalletService.currencySymbol)") - } - - if let decimal = Decimal(string: raw) { - amount = AdamantBalanceFormat.full.format(decimal) - } else { - amount = raw - } - - let string: String - if transaction.isOutgoing { - string = "⬅️ \(amount) \(DashWalletService.currencySymbol)" - } else { - string = "➡️ \(amount) \(DashWalletService.currencySymbol)" - } - - return NSAttributedString(string: string) - } -} diff --git a/Adamant/Wallets/Dash/DashWalletService+Transactions.swift b/Adamant/Wallets/Dash/DashWalletService+Transactions.swift deleted file mode 100644 index 302ceac25..000000000 --- a/Adamant/Wallets/Dash/DashWalletService+Transactions.swift +++ /dev/null @@ -1,257 +0,0 @@ -// -// DashWalletService+Transactions.swift -// Adamant -// -// Created by Anton Boyarkin on 11.04.2021. -// Copyright © 2021 Adamant. All rights reserved. -// - -import Foundation -import Alamofire -import BitcoinKit - -struct DashTransactionsPointer { - let total: Int - let transactions: [DashTransaction] - let hasMore: Bool -} - -extension DashWalletService { - - func getNextTransaction(completion: @escaping (ApiServiceResult) -> Void) { - guard let id = transatrionsIds.last else { - completion(.success(.init(total: transatrionsIds.count, transactions: [], hasMore: false))) - return - } - Task { - do { - let transaction = try await getTransaction(by: id) - handleTransactionResponse(id: id, .success(transaction), completion) - } catch { - let error = error as? WalletServiceError - let errorApi = ApiServiceError.internalError(message: error?.message ?? "", error: error) - handleTransactionResponse(id: id, .failure(errorApi), completion) - } - } - } - - func getTransaction(by hash: String) async throws -> BTCRawTransaction { - guard let endpoint = DashWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get DASH endpoint URL") - } - - let parameters: Parameters = [ - "method": "getrawtransaction", - "params": [ - hash, true - ] - ] - - // MARK: Sending request - - let result: BTCRPCServerResponce = try await apiService.sendRequest( - url: endpoint, - method: .post, - parameters: parameters, - encoding: JSONEncoding.default - ) - - if let transaction = result.result { - return transaction - } else { - throw ApiServiceError.internalError(message: "Unaviable transaction", error: nil) - } - } - - func getTransactions(by hashes: [String]) async throws -> [DashTransaction] { - guard let address = wallet?.address else { - throw ApiServiceError.notLogged - } - - guard let endpoint = DashWalletService.nodes.randomElement()?.asURL() else { - throw ApiServiceError.internalError(message: "Failed to get DASH endpoint URL", error: nil) - } - - var parameters: [Parameters] = [] - - hashes.forEach { hash in - let params: Parameters = [ - "method": "getrawtransaction", - "params": [ - hash, - true - ] - ] - - parameters.append(params) - } - - // MARK: Sending request - - guard let dataParameters = try? JSONSerialization.data(withJSONObject: parameters) else { - throw ApiServiceError.internalError(message: "Failed to create request", error: nil) - } - - var request = URLRequest(url: endpoint) - request.httpMethod = "POST" - request.httpBody = dataParameters - - let data = try await apiService.sendRequest(request: AF.request(request)) - - do { - let model = try JSONDecoder().decode( - [BTCRPCServerResponce].self, - from: data - ) - - return model.compactMap { $0.result?.asBtcTransaction(DashTransaction.self, for: address) } - } catch { - throw ApiServiceError.internalError(message: error.localizedDescription, error: error) - } - } - - func getBlockId(by hash: String?) async throws -> String { - guard let hash = hash else { - throw ApiServiceError.internalError(message: "Hash is empty", error: nil) - } - - guard let endpoint = DashWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get DASH endpoint URL") - } - - let parameters: Parameters = [ - "method": "getblock", - "params": [ - hash - ] - ] - - // MARK: Sending request - - let result: BTCRPCServerResponce = try await apiService.sendRequest( - url: endpoint, - method: .post, - parameters: parameters, - encoding: JSONEncoding.default - ) - - if let block = result.result { - return String(block.height) - } else { - throw ApiServiceError.internalError(message: "DASH: Parsing block error", error: nil) - } - } - - func getUnspentTransactions() async throws -> [UnspentTransaction] { - guard let endpoint = DashWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get DASH endpoint URL") - } - - guard let wallet = self.dashWallet else { - throw WalletServiceError.internalError(message: "DASH Wallet not found", error: nil) - } - - let parameters: Parameters = [ - "method": "getaddressutxos", - "params": [ - wallet.address - ] - ] - - // MARK: Sending request - - let response: BTCRPCServerResponce<[DashUnspentTransaction]> = - try await apiService.sendRequest( - url: endpoint, - method: .post, - parameters: parameters, - encoding: JSONEncoding.default - ) - - if let result = response.result { - return result.map { - $0.asUnspentTransaction(lockScript: wallet.addressEntity.lockingScript) - } - } else if let error = response.error?.message { - throw WalletServiceError.internalError(message: error, error: nil) - } - - throw WalletServiceError.internalError( - message: "DASH Wallet: not a valid response", - error: nil - ) - } - -} - -// MARK: - Handlers - -private extension DashWalletService { - - func handleTransactionsResponse(_ response: ApiServiceResult<[String]>, _ completion: @escaping (ApiServiceResult) -> Void) { - switch response { - case .success(let ids): - transatrionsIds = ids - getNextTransaction(completion: completion) - case .failure(let error): - completion(.failure(error)) - } - } - - func handleTransactionResponse(id: String, _ response: ApiServiceResult, _ completion: @escaping (ApiServiceResult) -> Void) { - guard let address = wallet?.address else { - completion(.failure(.notLogged)) - return - } - - switch response { - case .success(let rawTransaction): - if let idx = self.transatrionsIds.firstIndex(of: id) { - self.transatrionsIds.remove(at: idx) - } - let transaction = rawTransaction.asBtcTransaction(DashTransaction.self, for: address) - completion(.success(.init(total: transatrionsIds.count, transactions: [transaction], hasMore: !transatrionsIds.isEmpty))) - case .failure(let error): - completion(.failure(error)) - } - } - -} - -// MARK: - Network Requests - -extension DashWalletService { - - func requestTransactionsIds(for address: String) async throws -> [String] { - guard let endpoint = DashWalletService.nodes.randomElement()?.asURL() else { - fatalError("Failed to get DASH endpoint URL") - } - - guard let address = self.dashWallet?.address else { - throw ApiServiceError.internalError(message: "DASH Wallet not found", error: nil) - } - - let parameters: Parameters = [ - "method": "getaddresstxids", - "params": [ - address - ] - ] - - // MARK: Sending request - - let response: BTCRPCServerResponce<[String]> = try await apiService.sendRequest( - url: endpoint, - method: .post, - parameters: parameters, - encoding: JSONEncoding.default - ) - - if let result = response.result { - return result - } - - throw ApiServiceError.internalError(message: "DASH Wallet: not a valid response", error: nil) - } - -} diff --git a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift deleted file mode 100644 index 37beae152..000000000 --- a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// DogeTransactionsViewController.swift -// Adamant -// -// Created by Anton Boyarkin on 11/03/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import UIKit -import ProcedureKit -import CommonKit - -class DogeTransactionsViewController: TransactionsListViewControllerBase { - - // MARK: - Dependencies - var walletService: DogeWalletService! - var dialogService: DialogService! - var router: Router! - - // MARK: - Properties - var transactions: [DogeTransaction] = [] - private var offset = 0 - - override func viewDidLoad() { - super.viewDidLoad() - updateLoadingView(isHidden: false) - currencySymbol = DogeWalletService.currencySymbol - handleRefresh() - } - - override func handleRefresh() { - offset = 0 - transactions.removeAll() - tableView.reloadData() - loadData(silent: false) - } - - override func loadData(silent: Bool) { - isBusy = true - - Task { - do { - let tuple = try await walletService.getTransactions(from: offset) - transactions.append(contentsOf: tuple.transactions) - offset += tuple.transactions.count - isNeedToLoadMoore = tuple.hasMore - } catch { - isNeedToLoadMoore = false - - if !silent { - dialogService.showRichError(error: error) - } - } - - isBusy = false - emptyLabel.isHidden = transactions.count > 0 - stopBottomIndicator() - refreshControl.endRefreshing() - tableView.reloadData() - updateLoadingView(isHidden: true) - }.stored(in: taskManager) - } - - // MARK: - UITableView - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return transactions.count - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let controller = router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController else { - fatalError("Failed to get DogeTransactionDetailsViewController") - } - - // Hold reference - guard let address = walletService.wallet?.address else { - return - } - - controller.service = self.walletService - - let transaction = transactions[indexPath.row] - - let isOutgoing: Bool = transaction.recipientAddress != address - - let emptyTransaction = SimpleTransactionDetails( - txId: transaction.txId, - senderAddress: transaction.senderAddress, - recipientAddress: transaction.recipientAddress, - dateValue: nil, - amountValue: transaction.amountValue, - feeValue: nil, - confirmationsValue: nil, - blockValue: nil, - isOutgoing: isOutgoing, - transactionStatus: nil - ) - - controller.transaction = emptyTransaction - - if emptyTransaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { - controller.senderName = String.adamant.transactionDetails.yourAddress - } - - if emptyTransaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { - controller.recipientName = String.adamant.transactionDetails.yourAddress - } - - navigationController?.pushViewController(controller, animated: true) - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierCompact, for: indexPath) as? TransactionTableViewCell else { - // TODO: Display & Log error - return UITableViewCell(style: .default, reuseIdentifier: "cell") - } - - let transaction = transactions[indexPath.row] - - cell.accessoryType = .disclosureIndicator - cell.separatorInset = indexPath.row == transactions.count - 1 - ? .zero - : UITableView.defaultTransactionsSeparatorInset - - configureCell(cell, for: transaction) - return cell - } - - func configureCell(_ cell: TransactionTableViewCell, for transaction: DogeTransaction) { - let outgoing = transaction.isOutgoing - let partnerId = outgoing ? transaction.recipientAddress : transaction.senderAddress - - let partnerName: String? - if let address = walletService.wallet?.address, partnerId == address { - partnerName = String.adamant.transactionDetails.yourAddress - } else { - partnerName = nil - } - - configureCell(cell, - isOutgoing: outgoing, - partnerId: partnerId, - partnerName: partnerName, - amount: transaction.amountValue ?? 0, - date: transaction.dateValue) - } -} - -private class LoadMoreDogeTransactionsProcedure: Procedure { - let from: Int - let service: DogeWalletService - - private(set) var result: (transactions: [DogeTransaction], hasMore: Bool)? - - init(service: DogeWalletService, from: Int) { - self.from = from - self.service = service - - super.init() - log.severity = .warning - } - - override func execute() { - Task { - do { - let result = try await service.getTransactions(from: from) - self.result = result - self.finish() - } catch { - self.result = nil - self.finish(with: error) - } - } - } -} diff --git a/Adamant/Wallets/Doge/DogeWalletRoutes.swift b/Adamant/Wallets/Doge/DogeWalletRoutes.swift deleted file mode 100644 index b9d2c77bb..000000000 --- a/Adamant/Wallets/Doge/DogeWalletRoutes.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// DogeWalletRoutes.swift -// Adamant -// -// Created by Anton Boyarkin on 05/03/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene.Wallets { - struct Doge { - /// Wallet preview - static let wallet = AdamantScene(identifier: "DogeWalletViewController") { r in - let c = DogeWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) - c.accountService = r.resolve(AccountService.self) - return c - } - - /// Send tokens - static let transfer = AdamantScene(identifier: "DogeTransferViewController") { r in - DogeTransferViewController( - chatsProvider: r.resolve(ChatsProvider.self)!, - accountService: r.resolve(AccountService.self)!, - accountsProvider: r.resolve(AccountsProvider.self)!, - dialogService: r.resolve(DialogService.self)!, - router: r.resolve(Router.self)!, - currencyInfoService: r.resolve(CurrencyInfoService.self)!, - increaseFeeService: r.resolve(IncreaseFeeService.self)! - ) - } - - /// List of transactions - static let transactionsList = AdamantScene(identifier: "DogeTransactionsViewController") { r in - let c = DogeTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.router = r.resolve(Router.self) - return c - } - - /// Transaction details - static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewControllerBase") { r in - DogeTransactionDetailsViewController( - dialogService: r.resolve(DialogService.self)!, - currencyInfo: r.resolve(CurrencyInfoService.self)!, - addressBookService: r.resolve(AddressBookService.self)!, - accountService: r.resolve(AccountService.self)! - ) - } - } -} diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift deleted file mode 100644 index 7cce76917..000000000 --- a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// DogeWalletService+RichMessageProvider.swift -// Adamant -// -// Created by Anton Boyarkin on 13/03/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import Foundation -import MessageKit -import UIKit -import CommonKit - -extension DogeWalletService: RichMessageProvider { - var newPendingInterval: TimeInterval { - .init(milliseconds: type(of: self).newPendingInterval) - } - - var oldPendingInterval: TimeInterval { - .init(milliseconds: type(of: self).oldPendingInterval) - } - - var registeredInterval: TimeInterval { - .init(milliseconds: type(of: self).registeredInterval) - } - - var newPendingAttempts: Int { - type(of: self).newPendingAttempts - } - - var oldPendingAttempts: Int { - type(of: self).oldPendingAttempts - } - - var dynamicRichMessageType: String { - return type(of: self).richMessageType - } - - // MARK: Events - - @MainActor - func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { - // MARK: 0. Prepare - guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) - else { - return - } - - let comment: String? - if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { - comment = raw - } else { - comment = nil - } - - // MARK: 2. Go to transaction - - presentDetailTransactionVC( - hash: hash, - senderId: transaction.senderId, - recipientId: transaction.recipientId, - comment: comment, - senderAddress: "", - recipientAddress: "", - transaction: nil, - richTransaction: transaction, - in: chat - ) - } - - @MainActor - private func presentDetailTransactionVC( - hash: String, - senderId: String?, - recipientId: String?, - comment: String?, - senderAddress: String, - recipientAddress: String, - transaction: DogeTransaction?, - richTransaction: RichMessageTransaction, - in chat: ChatViewController - ) { - guard let vc = router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController else { - dialogService.dismissProgress() - return - } - - vc.service = self - vc.senderId = senderId - vc.recipientId = recipientId - vc.comment = comment - - let amount: Decimal - if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { - amount = decimal - } else { - amount = 0 - } - - let failedTransaction = SimpleTransactionDetails( - txId: hash, - senderAddress: senderAddress, - recipientAddress: recipientAddress, - dateValue: nil, - amountValue: amount, - feeValue: nil, - confirmationsValue: nil, - blockValue: nil, - isOutgoing: richTransaction.isOutgoing, - transactionStatus: nil) - - vc.transaction = transaction ?? failedTransaction - vc.richTransaction = richTransaction - - chat.navigationController?.pushViewController(vc, animated: true) - } - - // MARK: Short description - - func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { - let amount: String - - guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) - else { - return NSAttributedString(string: "⬅️ \(DogeWalletService.currencySymbol)") - } - - if let decimal = Decimal(string: raw) { - amount = AdamantBalanceFormat.full.format(decimal) - } else { - amount = raw - } - - let string: String - if transaction.isOutgoing { - string = "⬅️ \(amount) \(DogeWalletService.currencySymbol)" - } else { - string = "➡️ \(amount) \(DogeWalletService.currencySymbol)" - } - - return NSAttributedString(string: string) - } -} diff --git a/Adamant/Wallets/ERC20/ERC20TransactionsViewController.swift b/Adamant/Wallets/ERC20/ERC20TransactionsViewController.swift deleted file mode 100644 index b3e4b8667..000000000 --- a/Adamant/Wallets/ERC20/ERC20TransactionsViewController.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// ERC20TransactionsViewController.swift -// Adamant -// -// Created by Anton Boyarkin on 06/07/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import UIKit -import web3swift -import CommonKit - -class ERC20TransactionsViewController: TransactionsListViewControllerBase { - - // MARK: - Dependencies - var walletService: ERC20WalletService! { - didSet { - ethAddress = walletService.wallet?.address ?? "" - } - } - var dialogService: DialogService! - var router: Router! - - // MARK: - Properties - var transactions: [EthTransactionShort] = [] - private var ethAddress: String = "" - private lazy var exponent: Int = { - var exponent = EthWalletService.currencyExponent - if let naturalUnits = walletService.token?.naturalUnits { - exponent = -1 * naturalUnits - } - return exponent - }() - private var offset = 0 - - override func viewDidLoad() { - super.viewDidLoad() - updateLoadingView(isHidden: false) - currencySymbol = walletService.tokenSymbol - handleRefresh() - } - - // MARK: - Overrides - - override func handleRefresh() { - offset = 0 - transactions.removeAll() - tableView.reloadData() - loadData(silent: false) - } - - override func loadData(silent: Bool) { - isBusy = true - emptyLabel.isHidden = true - - guard let address = walletService.wallet?.address else { - transactions = [] - return - } - - Task { @MainActor in - do { - let trs = try await walletService.getTransactionsHistory( - address: address, - offset: offset - ) - - transactions.append(contentsOf: trs) - offset += trs.count - isNeedToLoadMoore = trs.count > 0 - } catch { - isNeedToLoadMoore = false - - if !silent { - dialogService.showRichError(error: error) - } - } - - isBusy = false - emptyLabel.isHidden = transactions.count > 0 - stopBottomIndicator() - refreshControl.endRefreshing() - tableView.reloadData() - updateLoadingView(isHidden: true) - }.stored(in: taskManager) - } - - override func reloadData() { - handleRefresh() - } - - // MARK: - UITableView - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return transactions.count - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let address = walletService.wallet?.address else { return } - - tableView.deselectRow(at: indexPath, animated: true) - - let transaction = transactions[indexPath.row] - - guard let vc = router.get(scene: AdamantScene.Wallets.ERC20.transactionDetails) as? ERC20TransactionDetailsViewController else { - fatalError("Failed to get ERC20TransactionDetailsViewController") - } - - vc.service = walletService - - let isOutgoing: Bool = transaction.to != address - - let emptyTransaction = SimpleTransactionDetails( - txId: transaction.hash, - senderAddress: transaction.from, - recipientAddress: transaction.to, - dateValue: nil, - amountValue: transaction.value, - feeValue: nil, - confirmationsValue: nil, - blockValue: nil, - isOutgoing: isOutgoing, - transactionStatus: nil - ) - - vc.transaction = emptyTransaction - - if emptyTransaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { - vc.senderName = String.adamant.transactionDetails.yourAddress - } - - if emptyTransaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { - vc.recipientName = String.adamant.transactionDetails.yourAddress - } - - navigationController?.pushViewController(vc, animated: true) - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierCompact, for: indexPath) as? TransactionTableViewCell else { - return UITableViewCell(style: .default, reuseIdentifier: "cell") - } - - let transaction = transactions[indexPath.row] - cell.accessoryType = .disclosureIndicator - cell.separatorInset = indexPath.row == transactions.count - 1 - ? .zero - : UITableView.defaultTransactionsSeparatorInset - - configureCell(cell, for: transaction) - return cell - } - - func configureCell(_ cell: TransactionTableViewCell, for transaction: EthTransactionShort) { - let outgoing = isOutgoing(transaction) - let partnerId = outgoing ? transaction.to : transaction.from - - configureCell(cell, - isOutgoing: outgoing, - partnerId: partnerId, - partnerName: nil, - amount: transaction.contract_value.asDecimal(exponent: exponent), - date: transaction.date) - } -} - -// MARK: - Tools -extension ERC20TransactionsViewController { - private func isOutgoing(_ transaction: EthTransactionShort) -> Bool { - return transaction.from.lowercased() == ethAddress.lowercased() - } -} diff --git a/Adamant/Wallets/ERC20/ERC20WalletRouter.swift b/Adamant/Wallets/ERC20/ERC20WalletRouter.swift deleted file mode 100644 index dc223279c..000000000 --- a/Adamant/Wallets/ERC20/ERC20WalletRouter.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// ERC20WalletRouter.swift -// Adamant -// -// Created by Anton Boyarkin on 26/06/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene.Wallets { - struct ERC20 { - /// Wallet preview - static let wallet = AdamantScene(identifier: "ERC20WalletViewController") { r in - let c = ERC20WalletViewController(nibName: "WalletViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) - c.accountService = r.resolve(AccountService.self) - return c - } - - /// Send money - static let transfer = AdamantScene(identifier: "ERC20TransferViewController") { r in - ERC20TransferViewController( - chatsProvider: r.resolve(ChatsProvider.self)!, - accountService: r.resolve(AccountService.self)!, - accountsProvider: r.resolve(AccountsProvider.self)!, - dialogService: r.resolve(DialogService.self)!, - router: r.resolve(Router.self)!, - currencyInfoService: r.resolve(CurrencyInfoService.self)!, - increaseFeeService: r.resolve(IncreaseFeeService.self)! - ) - } - - /// List of Ethereum transactions - static let transactionsList = AdamantScene(identifier: "ERC20TransactionsViewController") { r in - let c = ERC20TransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.router = r.resolve(Router.self) - return c - } - - /// Ethereum transaction details - static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewControllerBase") { r in - ERC20TransactionDetailsViewController( - dialogService: r.resolve(DialogService.self)!, - currencyInfo: r.resolve(CurrencyInfoService.self)!, - addressBookService: r.resolve(AddressBookService.self)!, - accountService: r.resolve(AccountService.self)! - ) - } - } -} diff --git a/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift b/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift deleted file mode 100644 index 543da44ab..000000000 --- a/Adamant/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// ERC20WalletService+RichMessageProvider.swift -// Adamant -// -// Created by Anton Boyarkin on 06/07/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import Foundation -import MessageKit -import UIKit -import CommonKit - -extension ERC20WalletService: RichMessageProvider { - var newPendingInterval: TimeInterval { - .init(milliseconds: EthWalletService.newPendingInterval) - } - - var oldPendingInterval: TimeInterval { - .init(milliseconds: EthWalletService.oldPendingInterval) - } - - var registeredInterval: TimeInterval { - .init(milliseconds: EthWalletService.registeredInterval) - } - - var newPendingAttempts: Int { - EthWalletService.newPendingAttempts - } - - var oldPendingAttempts: Int { - EthWalletService.oldPendingAttempts - } - - // MARK: Events - - @MainActor - func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { - // MARK: 0. Prepare - guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) - else { - return - } - - let comment: String? - if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { - comment = raw - } else { - comment = nil - } - - // MARK: Go to transaction - - presentDetailTransactionVC( - hash: hash, - senderId: transaction.senderId, - recipientId: transaction.recipientId, - senderAddress: "", - recipientAddress: "", - comment: comment, - transaction: nil, - richTransaction: transaction, - in: chat - ) - } - - private func presentDetailTransactionVC( - hash: String, - senderId: String?, - recipientId: String?, - senderAddress: String, - recipientAddress: String, - comment: String?, - transaction: EthTransaction?, - richTransaction: RichMessageTransaction, - in chat: ChatViewController - ) { - guard let vc = router.get(scene: AdamantScene.Wallets.ERC20.transactionDetails) as? ERC20TransactionDetailsViewController else { - return - } - - let amount: Decimal - if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { - amount = decimal - } else { - amount = 0 - } - - let failedTransaction = SimpleTransactionDetails( - txId: hash, - senderAddress: senderAddress, - recipientAddress: recipientAddress, - dateValue: nil, - amountValue: amount, - feeValue: nil, - confirmationsValue: nil, - blockValue: nil, - isOutgoing: richTransaction.isOutgoing, - transactionStatus: nil - ) - - vc.service = self - vc.senderId = senderId - vc.recipientId = recipientId - vc.comment = comment - vc.transaction = transaction ?? failedTransaction - vc.richTransaction = richTransaction - - chat.navigationController?.pushViewController(vc, animated: true) - } - - // MARK: Short description - - func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { - let amount: String - - guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) - else { - return NSAttributedString(string: "⬅️ \(self.tokenSymbol)") - } - - if let decimal = Decimal(string: raw) { - amount = AdamantBalanceFormat.full.format(decimal) - } else { - amount = raw - } - - let string: String - if transaction.isOutgoing { - string = "⬅️ \(amount) \(self.tokenSymbol)" - } else { - string = "➡️ \(amount) \(self.tokenSymbol)" - } - - return NSAttributedString(string: string) - } -} diff --git a/Adamant/Wallets/ERC20/ERC20WalletService+Send.swift b/Adamant/Wallets/ERC20/ERC20WalletService+Send.swift deleted file mode 100644 index 37b0b93ac..000000000 --- a/Adamant/Wallets/ERC20/ERC20WalletService+Send.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// ERC20WalletService+Send.swift -// Adamant -// -// Created by Anton Boyarkin on 06/07/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import UIKit -import web3swift -import struct BigInt.BigUInt -import Web3Core -import CommonKit - -extension ERC20WalletService: WalletServiceTwoStepSend { - typealias T = CodableTransaction - - func transferViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.ERC20.transfer) as? ERC20TransferViewController else { - fatalError("Can't get ERC20TransferViewController") - } - - vc.service = self - return vc - } - - // MARK: Create & Send - func createTransaction(recipient: String, amount: Decimal) async throws -> CodableTransaction { - guard let ethWallet = ethWallet, - let erc20 = erc20 - else { - throw WalletServiceError.notLogged - } - - guard let ethRecipient = EthereumAddress(recipient) else { - throw WalletServiceError.accountNotFound - } - - guard let web3 = await web3 else { - throw WalletServiceError.internalError(message: "Failed to get web3", error: nil) - } - - guard let keystoreManager = web3.provider.attachedKeystoreManager else { - throw WalletServiceError.internalError(message: "Failed to get web3.provider.KeystoreManager", error: nil) - } - - let provider = web3.provider - let resolver = PolicyResolver(provider: provider) - - // MARK: Create transaction - - do { - var tx = try await erc20.transfer( - from: ethWallet.ethAddress, - to: ethRecipient, - amount: "\(amount)" - ).transaction - - await calculateFee(for: ethRecipient) - - let policies: Policies = Policies( - gasLimitPolicy: .manual(gasLimit), - gasPricePolicy: .manual(gasPrice) - ) - - try await resolver.resolveAll(for: &tx, with: policies) - - try Web3Signer.signTX( - transaction: &tx, - keystore: keystoreManager, - account: ethWallet.ethAddress, - password: ERC20WalletService.walletPassword - ) - - return tx - } catch { - throw WalletServiceError.internalError(message: "Transaction sign error", error: error) - } - } - - func sendTransaction(_ transaction: CodableTransaction) async throws { - guard let txEncoded = transaction.encode() else { - throw WalletServiceError.internalError(message: String.adamant.sharedErrors.unknownError, error: nil) - } - - do { - _ = try await web3?.eth.send(raw: txEncoded) - } catch { - throw WalletServiceError.internalError(message: "Error: \(error.localizedDescription)", error: nil) - } - } -} diff --git a/Adamant/Wallets/Ethereum/EthTransactionsViewController.swift b/Adamant/Wallets/Ethereum/EthTransactionsViewController.swift deleted file mode 100644 index ab4f47762..000000000 --- a/Adamant/Wallets/Ethereum/EthTransactionsViewController.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// EthTransactionsViewController.swift -// Adamant -// -// Created by Anton Boyarkin on 25/06/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import web3swift -import CommonKit - -class EthTransactionsViewController: TransactionsListViewControllerBase { - - // MARK: - Dependencies - var ethWalletService: EthWalletService! { - didSet { - ethAddress = ethWalletService.wallet?.address ?? "" - } - } - var dialogService: DialogService! - var router: Router! - - // MARK: - Properties - var transactions: [EthTransactionShort] = [] - private var ethAddress: String = "" - private var offset = 0 - - override func viewDidLoad() { - super.viewDidLoad() - updateLoadingView(isHidden: false) - currencySymbol = EthWalletService.currencySymbol - handleRefresh() - } - - // MARK: - Overrides - - override func handleRefresh() { - offset = 0 - transactions.removeAll() - tableView.reloadData() - loadData(silent: false) - } - - override func loadData(silent: Bool) { - isBusy = true - emptyLabel.isHidden = true - - guard let address = ethWalletService.wallet?.address else { - transactions = [] - return - } - - Task { @MainActor in - do { - let trs = try await ethWalletService.getTransactionsHistory( - address: address, - offset: offset - ) - - transactions.append(contentsOf: trs) - offset += trs.count - isNeedToLoadMoore = trs.count > 0 - } catch { - isNeedToLoadMoore = false - - if !silent { - dialogService.showRichError(error: error) - } - } - - isBusy = false - emptyLabel.isHidden = transactions.count > 0 - refreshControl.endRefreshing() - stopBottomIndicator() - tableView.reloadData() - updateLoadingView(isHidden: true) - }.stored(in: taskManager) - } - - override func reloadData() { - handleRefresh() - } - - // MARK: - UITableView - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return transactions.count - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let address = ethWalletService.wallet?.address else { return } - - tableView.deselectRow(at: indexPath, animated: true) - let transaction = transactions[indexPath.row] - - guard let vc = router.get(scene: AdamantScene.Wallets.Ethereum.transactionDetails) as? EthTransactionDetailsViewController else { - fatalError("Failed to get EthTransactionDetailsViewController") - } - - vc.service = ethWalletService - - let isOutgoing: Bool = transaction.to != address - - let emptyTransaction = SimpleTransactionDetails( - txId: transaction.hash, - senderAddress: transaction.from, - recipientAddress: transaction.to, - dateValue: nil, - amountValue: transaction.value, - feeValue: nil, - confirmationsValue: nil, - blockValue: nil, - isOutgoing: isOutgoing, - transactionStatus: nil - ) - - vc.transaction = emptyTransaction - - if emptyTransaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { - vc.senderName = String.adamant.transactionDetails.yourAddress - } - - if emptyTransaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { - vc.recipientName = String.adamant.transactionDetails.yourAddress - } - - navigationController?.pushViewController(vc, animated: true) - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierCompact, for: indexPath) as? TransactionTableViewCell else { - return UITableViewCell(style: .default, reuseIdentifier: "cell") - } - - let transaction = transactions[indexPath.row] - cell.accessoryType = .disclosureIndicator - cell.separatorInset = indexPath.row == transactions.count - 1 - ? .zero - : UITableView.defaultTransactionsSeparatorInset - - configureCell(cell, for: transaction) - return cell - } - - func configureCell(_ cell: TransactionTableViewCell, for transaction: EthTransactionShort) { - let outgoing = isOutgoing(transaction) - let partnerId = outgoing ? transaction.to : transaction.from - - configureCell(cell, - isOutgoing: outgoing, - partnerId: partnerId, - partnerName: nil, - amount: transaction.value, - date: transaction.date) - } -} - -// MARK: - Tools -extension EthTransactionsViewController { - private func isOutgoing(_ transaction: EthTransactionShort) -> Bool { - return transaction.from.lowercased() == ethAddress.lowercased() - } -} diff --git a/Adamant/Wallets/Ethereum/EthWalletRoutes.swift b/Adamant/Wallets/Ethereum/EthWalletRoutes.swift deleted file mode 100644 index 573322002..000000000 --- a/Adamant/Wallets/Ethereum/EthWalletRoutes.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// EthWalletRoutes.swift -// Adamant -// -// Created by Anokhov Pavel on 28.08.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene.Wallets { - struct Ethereum { - /// Wallet preview - static let wallet = AdamantScene(identifier: "EthWalletViewController") { r in - let c = EthWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) - c.accountService = r.resolve(AccountService.self) - return c - } - - /// Send money - static let transfer = AdamantScene(identifier: "EthTransferViewController") { r in - EthTransferViewController( - chatsProvider: r.resolve(ChatsProvider.self)!, - accountService: r.resolve(AccountService.self)!, - accountsProvider: r.resolve(AccountsProvider.self)!, - dialogService: r.resolve(DialogService.self)!, - router: r.resolve(Router.self)!, - currencyInfoService: r.resolve(CurrencyInfoService.self)!, - increaseFeeService: r.resolve(IncreaseFeeService.self)! - ) - } - - /// List of Ethereum transactions - static let transactionsList = AdamantScene(identifier: "EthTransactionsViewController") { r in - let c = EthTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.router = r.resolve(Router.self) - return c - } - - /// Ethereum transaction details - static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewControllerBase") { r in - EthTransactionDetailsViewController( - dialogService: r.resolve(DialogService.self)!, - currencyInfo: r.resolve(CurrencyInfoService.self)!, - addressBookService: r.resolve(AddressBookService.self)!, - accountService: r.resolve(AccountService.self)! - ) - } - } -} diff --git a/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift b/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift deleted file mode 100644 index c4106688f..000000000 --- a/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// EthWalletService+RichMessageProvider.swift -// Adamant -// -// Created by Anokhov Pavel on 08.09.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation -import MessageKit -import UIKit -import CommonKit - -extension EthWalletService: RichMessageProvider { - var newPendingInterval: TimeInterval { - .init(milliseconds: type(of: self).newPendingInterval) - } - - var oldPendingInterval: TimeInterval { - .init(milliseconds: type(of: self).oldPendingInterval) - } - - var registeredInterval: TimeInterval { - .init(milliseconds: type(of: self).registeredInterval) - } - - var newPendingAttempts: Int { - type(of: self).newPendingAttempts - } - - var oldPendingAttempts: Int { - type(of: self).oldPendingAttempts - } - - var dynamicRichMessageType: String { - return type(of: self).richMessageType - } - - // MARK: Events - - @MainActor - func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { - // MARK: 0. Prepare - guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) - else { - return - } - - let comment: String? - if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { - comment = raw - } else { - comment = nil - } - - // MARK: Go to transaction - - presentDetailTransactionVC( - hash: hash, - senderId: transaction.senderId, - recipientId: transaction.recipientId, - senderAddress: "", - recipientAddress: "", - comment: comment, - transaction: nil, - richTransaction: transaction, - in: chat - ) - } - - private func presentDetailTransactionVC( - hash: String, - senderId: String?, - recipientId: String?, - senderAddress: String, - recipientAddress: String, - comment: String?, - transaction: EthTransaction?, - richTransaction: RichMessageTransaction, - in chat: ChatViewController - ) { - guard let vc = router.get(scene: AdamantScene.Wallets.Ethereum.transactionDetails) as? EthTransactionDetailsViewController else { - return - } - - let amount: Decimal - if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { - amount = decimal - } else { - amount = 0 - } - - let failedTransaction = SimpleTransactionDetails( - txId: hash, - senderAddress: senderAddress, - recipientAddress: recipientAddress, - dateValue: nil, - amountValue: amount, - feeValue: nil, - confirmationsValue: nil, - blockValue: nil, - isOutgoing: richTransaction.isOutgoing, - transactionStatus: nil - ) - - vc.service = self - vc.senderId = senderId - vc.recipientId = recipientId - vc.comment = comment - vc.transaction = transaction ?? failedTransaction - vc.richTransaction = richTransaction - - chat.navigationController?.pushViewController(vc, animated: true) - } - - // MARK: Short description - - func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { - let amount: String - - guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) - else { - return NSAttributedString(string: "⬅️ \(EthWalletService.currencySymbol)") - } - - if let decimal = Decimal(string: raw) { - amount = AdamantBalanceFormat.full.format(decimal) - } else { - amount = raw - } - - let string: String - if transaction.isOutgoing { - string = "⬅️ \(amount) \(EthWalletService.currencySymbol)" - } else { - string = "➡️ \(amount) \(EthWalletService.currencySymbol)" - } - - return NSAttributedString(string: string) - } -} diff --git a/Adamant/Wallets/Ethereum/EthWalletService+Transfers.swift b/Adamant/Wallets/Ethereum/EthWalletService+Transfers.swift deleted file mode 100644 index 43fefb6e7..000000000 --- a/Adamant/Wallets/Ethereum/EthWalletService+Transfers.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// EthWalletService+Transfers.swift -// Adamant -// -// Created by Anokhov Pavel on 21.08.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit - -extension EthWalletService: WalletServiceWithTransfers { - func transferListViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Ethereum.transactionsList) as? EthTransactionsViewController else { - fatalError("Can't get EthTransactionsViewController") - } - - vc.ethWalletService = self - return vc - } -} diff --git a/Adamant/Wallets/Lisk/LskTransactionsViewController.swift b/Adamant/Wallets/Lisk/LskTransactionsViewController.swift deleted file mode 100644 index e72258853..000000000 --- a/Adamant/Wallets/Lisk/LskTransactionsViewController.swift +++ /dev/null @@ -1,323 +0,0 @@ -// -// LskTransactionsViewController -// Adamant -// -// Created by Anton Boyarkin on 17/07/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import LiskKit -import web3swift -import BigInt -import CommonKit - -class LskTransactionsViewController: TransactionsListViewControllerBase { - - // MARK: - Dependencies - var lskWalletService: LskWalletService! - var dialogService: DialogService! - var router: Router! - - // MARK: - Properties - var transactions: [Transactions.TransactionModel] = [] - - private var offset: UInt = 0 - - override func viewDidLoad() { - super.viewDidLoad() - updateLoadingView(isHidden: false) - currencySymbol = LskWalletService.currencySymbol - handleRefresh() - } - - override func handleRefresh() { - emptyLabel.isHidden = true - transactions.removeAll() - tableView.reloadData() - offset = 0 - loadData(silent: false) - } - - override func loadData(silent: Bool) { - isBusy = true - Task { @MainActor in - do { - let trs = try await lskWalletService.getTransactions(offset: offset) - transactions.append(contentsOf: trs) - offset += UInt(trs.count) - isNeedToLoadMoore = trs.count > 0 - } catch { - isNeedToLoadMoore = false - - if !silent { - dialogService.showRichError(error: error) - } - } - - isBusy = false - emptyLabel.isHidden = self.transactions.count > 0 - stopBottomIndicator() - refreshControl.endRefreshing() - tableView.reloadData() - updateLoadingView(isHidden: true) - }.stored(in: taskManager) - } - - // MARK: - UITableView - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return transactions.count - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - let transaction = transactions[indexPath.row] - - guard let controller = router.get(scene: AdamantScene.Wallets.Lisk.transactionDetails) as? LskTransactionDetailsViewController else { - return - } - - let emptyTransaction = SimpleTransactionDetails( - txId: transaction.txId, - senderAddress: transaction.senderAddress, - recipientAddress: transaction.recipientAddress, - dateValue: transaction.dateValue, - amountValue: transaction.amountValue, - feeValue: transaction.feeValue, - confirmationsValue: transaction.confirmationsValue, - blockValue: transaction.blockValue, - isOutgoing: transaction.isOutgoing, - transactionStatus: nil - ) - - controller.transaction = emptyTransaction - controller.service = lskWalletService - - if let address = lskWalletService.wallet?.address { - if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { - controller.senderName = String.adamant.transactionDetails.yourAddress - } else if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { - controller.recipientName = String.adamant.transactionDetails.yourAddress - } - } - - navigationController?.pushViewController(controller, animated: true) - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierCompact, for: indexPath) as? TransactionTableViewCell else { - // TODO: Display & Log error - return UITableViewCell(style: .default, reuseIdentifier: "cell") - } - - let transaction = transactions[indexPath.row] - - cell.accessoryType = .disclosureIndicator - cell.separatorInset = indexPath.row == transactions.count - 1 - ? .zero - : UITableView.defaultTransactionsSeparatorInset - - configureCell(cell, for: transaction) - return cell - } - - func configureCell(_ cell: TransactionTableViewCell, for transaction: Transactions.TransactionModel) { - let outgoing = isOutgoing(transaction) - let partnerId = outgoing ? transaction.recipientId : transaction.senderId - - configureCell(cell, - isOutgoing: outgoing, - partnerId: partnerId ?? "", - partnerName: nil, - amount: transaction.amountValue ?? 0, - date: transaction.dateValue) - } - - private func isOutgoing(_ transaction: Transactions.TransactionModel) -> Bool { - return transaction.senderId.lowercased() == lskWalletService.wallet?.address.lowercased() - } -} - -extension Transactions.TransactionModel: TransactionDetails { - - var defaultCurrencySymbol: String? { LskWalletService.currencySymbol } - - var txId: String { - return id - } - - var dateValue: Date? { - return timestamp.map { Date(timeIntervalSince1970: TimeInterval($0)) } - } - - var amountValue: Decimal? { - let value = BigUInt(self.amount) ?? BigUInt(0) - - return value.asDecimal(exponent: LskWalletService.currencyExponent) - } - - var feeValue: Decimal? { - let value = BigUInt(self.fee) ?? BigUInt(0) - - return value.asDecimal(exponent: LskWalletService.currencyExponent) - } - - var confirmationsValue: String? { - guard let confirmations = confirmations, let height = height else { return "0" } - if confirmations < height { return "0" } - if confirmations > 0 { - return "\(confirmations - height + 1)" - } - - return "\(confirmations)" - } - - var blockHeight: UInt64? { - return self.height - } - - var blockValue: String? { - return self.blockId - } - - var isOutgoing: Bool { - return false - } - - var transactionStatus: TransactionStatus? { - guard let confirmations = confirmations, let height = height else { return .registered } - if confirmations < height { return .registered } - - if confirmations > 0 && height > 0 { - let conf = (confirmations - height) + 1 - if conf > 1 { - return .success - } else { - return .registered - } - } - return .registered - } - - var senderAddress: String { - return self.senderId - } - - var recipientAddress: String { - return self.recipientId ?? "" - } - - var sentDate: Date? { - timestamp.map { Date(timeIntervalSince1970: TimeInterval($0)) } - } -} - -extension LocalTransaction: TransactionDetails { - - var defaultCurrencySymbol: String? { LskWalletService.currencySymbol } - - var txId: String { - return id ?? "" - } - - var senderAddress: String { - return "" - } - - var recipientAddress: String { - return self.recipientId ?? "" - } - - var dateValue: Date? { - return Date(timeIntervalSince1970: TimeInterval(self.timestamp)) - } - - var amountValue: Decimal? { - let value = BigUInt(self.amount) - - return value.asDecimal(exponent: LskWalletService.currencyExponent) - } - - var feeValue: Decimal? { - let value = BigUInt(self.fee) - - return value.asDecimal(exponent: LskWalletService.currencyExponent) - } - - var confirmationsValue: String? { - return nil - } - - var blockHeight: UInt64? { - return nil - } - - var blockValue: String? { - return nil - } - - var isOutgoing: Bool { - return true - } - - var transactionStatus: TransactionStatus? { - return .pending - } - -} - -extension TransactionEntity: TransactionDetails { - - var defaultCurrencySymbol: String? { LskWalletService.currencySymbol } - - var txId: String { - return id - } - - var senderAddress: String { - return LiskKit.Crypto.getBase32Address(from: senderPublicKey) - } - - var recipientAddress: String { - return self.asset.recipientAddress - } - - var dateValue: Date? { - return nil - } - - var amountValue: Decimal? { - let value = BigUInt(self.asset.amount) - - return value.asDecimal(exponent: LskWalletService.currencyExponent) - } - - var feeValue: Decimal? { - let value = BigUInt(self.fee) - - return value.asDecimal(exponent: LskWalletService.currencyExponent) - } - - var confirmationsValue: String? { - return nil - } - - var blockHeight: UInt64? { - return nil - } - - var blockValue: String? { - return nil - } - - var isOutgoing: Bool { - return true - } - - var transactionStatus: TransactionStatus? { - return .pending - } - -} diff --git a/Adamant/Wallets/Lisk/LskWalletRoutes.swift b/Adamant/Wallets/Lisk/LskWalletRoutes.swift deleted file mode 100644 index 200ed1259..000000000 --- a/Adamant/Wallets/Lisk/LskWalletRoutes.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// LskWalletRoutes.swift -// Adamant -// -// Created by Anton Boyarkin on 27/11/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene.Wallets { - struct Lisk { - /// Wallet preview - static let wallet = AdamantScene(identifier: "LskWalletViewController") { r in - let c = LskWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.currencyInfoService = r.resolve(CurrencyInfoService.self) - c.accountService = r.resolve(AccountService.self) - return c - } - - /// Send LSK tokens - static let transfer = AdamantScene(identifier: "LskTransferViewController") { r in - LskTransferViewController( - chatsProvider: r.resolve(ChatsProvider.self)!, - accountService: r.resolve(AccountService.self)!, - accountsProvider: r.resolve(AccountsProvider.self)!, - dialogService: r.resolve(DialogService.self)!, - router: r.resolve(Router.self)!, - currencyInfoService: r.resolve(CurrencyInfoService.self)!, - increaseFeeService: r.resolve(IncreaseFeeService.self)! - ) - } - - /// List of Lisk transactions - static let transactionsList = AdamantScene(identifier: "LskTransactionsViewController") { r in - let c = LskTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) - c.dialogService = r.resolve(DialogService.self) - c.router = r.resolve(Router.self) - return c - } - - /// Lisk transaction details - static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewControllerBase") { r in - LskTransactionDetailsViewController( - dialogService: r.resolve(DialogService.self)!, - currencyInfo: r.resolve(CurrencyInfoService.self)!, - addressBookService: r.resolve(AddressBookService.self)!, - accountService: r.resolve(AccountService.self)! - ) - } - } -} diff --git a/Adamant/Wallets/Lisk/LskWalletService+RichMessageProvider.swift b/Adamant/Wallets/Lisk/LskWalletService+RichMessageProvider.swift deleted file mode 100644 index 7b8c0190b..000000000 --- a/Adamant/Wallets/Lisk/LskWalletService+RichMessageProvider.swift +++ /dev/null @@ -1,145 +0,0 @@ -// -// LskWalletService+RichMessageProvider.swift -// Adamant -// -// Created by Anton Boyarkin on 06/12/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation -import MessageKit -import UIKit -import LiskKit -import CommonKit - -extension LskWalletService: RichMessageProvider { - var newPendingInterval: TimeInterval { - .init(milliseconds: type(of: self).newPendingInterval) - } - - var oldPendingInterval: TimeInterval { - .init(milliseconds: type(of: self).oldPendingInterval) - } - - var registeredInterval: TimeInterval { - .init(milliseconds: type(of: self).registeredInterval) - } - - var newPendingAttempts: Int { - type(of: self).newPendingAttempts - } - - var oldPendingAttempts: Int { - type(of: self).oldPendingAttempts - } - - var dynamicRichMessageType: String { - return type(of: self).richMessageType - } - - // MARK: Events - - @MainActor - func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { - // MARK: 0. Prepare - guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) - else { - return - } - - let comment: String? - if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { - comment = raw - } else { - comment = nil - } - - // MARK: Go to transaction - - presentDetailTransactionVC( - hash: hash, - senderId: transaction.senderId, - recipientId: transaction.recipientId, - comment: comment, - senderAddress: "", - recipientAddress: "", - transaction: nil, - richTransaction: transaction, - in: chat - ) - } - - @MainActor - private func presentDetailTransactionVC( - hash: String, - senderId: String?, - recipientId: String?, - comment: String?, - senderAddress: String, - recipientAddress: String, - transaction: Transactions.TransactionModel?, - richTransaction: RichMessageTransaction, - in chat: ChatViewController - ) { - guard let vc = router.get(scene: AdamantScene.Wallets.Lisk.transactionDetails) as? LskTransactionDetailsViewController else { - dialogService.dismissProgress() - return - } - - vc.service = self - vc.senderId = senderId - vc.recipientId = recipientId - vc.comment = comment - - let amount: Decimal - if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { - amount = decimal - } else { - amount = 0 - } - - let failedTransaction = SimpleTransactionDetails( - txId: hash, - senderAddress: senderAddress, - recipientAddress: recipientAddress, - dateValue: nil, - amountValue: amount, - feeValue: nil, - confirmationsValue: nil, - blockValue: nil, - isOutgoing: richTransaction.isOutgoing, - transactionStatus: nil) - - vc.transaction = transaction ?? failedTransaction - vc.richTransaction = richTransaction - - chat.navigationController?.pushViewController(vc, animated: true) - } - - // MARK: Short description - - func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { - let amount: String - - guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) - else { - return NSAttributedString(string: "⬅️ \(LskWalletService.currencySymbol)") - } - - if let decimal = Decimal(string: raw) { - amount = AdamantBalanceFormat.full.format(decimal) - } else { - amount = raw - } - - let string: String - if transaction.isOutgoing { - string = "⬅️ \(amount) \(LskWalletService.currencySymbol)" - } else { - string = "➡️ \(amount) \(LskWalletService.currencySymbol)" - } - - return NSAttributedString(string: string) - } -} diff --git a/Adamant/Wallets/Lisk/LskWalletService+Transfers.swift b/Adamant/Wallets/Lisk/LskWalletService+Transfers.swift deleted file mode 100644 index bac4e86b8..000000000 --- a/Adamant/Wallets/Lisk/LskWalletService+Transfers.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// LskWalletService+Transfers.swift -// Adamant -// -// Created by Anton Boyarkin on 28/11/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit - -extension LskWalletService: WalletServiceWithTransfers { - func transferListViewController() -> UIViewController { - guard let vc = router.get(scene: AdamantScene.Wallets.Lisk.transactionsList) as? LskTransactionsViewController else { - fatalError("Can't get LskTransactionsViewController") - } - - vc.lskWalletService = self - return vc - } -} diff --git a/Adamant/Wallets/TransactionTableViewCell.swift b/Adamant/Wallets/TransactionTableViewCell.swift deleted file mode 100644 index 5ac3f7a4c..000000000 --- a/Adamant/Wallets/TransactionTableViewCell.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// TransactionTableViewCell.swift -// Adamant -// -// Created by Anokhov Pavel on 08.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import CommonKit - -class TransactionTableViewCell: UITableViewCell { - enum TransactionType { - case income, outcome - - var imageTop: UIImage { - switch self { - case .income: return .asset(named: "transfer-in_top") ?? .init() - case .outcome: return .asset(named: "transfer-out_top") ?? .init() - } - } - - var imageBottom: UIImage { - switch self { - case .income: return .asset(named: "transfer-in_bot") ?? .init() - case .outcome: return .asset(named: "transfer-out_bot") ?? .init() - } - } - - var bottomTintColor: UIColor { - switch self { - case .income: return UIColor.adamant.transferIncomeIconBackground - case .outcome: return UIColor.adamant.transferOutcomeIconBackground - } - } - } - - // MARK: - Constants - - static let cellHeightCompact: CGFloat = 90.0 - static let cellFooterLoadingCompact: CGFloat = 30.0 - static let cellHeightFull: CGFloat = 100.0 - - // MARK: - IBOutlets - - @IBOutlet weak var topImageView: UIImageView! - @IBOutlet weak var bottomImageView: UIImageView! - @IBOutlet weak var accountLabel: UILabel! - @IBOutlet weak var addressLabel: UILabel! - @IBOutlet weak var ammountLabel: UILabel! - @IBOutlet weak var dateLabel: UILabel! - - // MARK: - Properties - - var transactionType: TransactionType = .income { - didSet { - topImageView.image = transactionType.imageTop - bottomImageView.image = transactionType.imageBottom - bottomImageView.tintColor = transactionType.bottomTintColor - } - } - - // MARK: - Initializers - - override func awakeFromNib() { - transactionType = .income - } -} diff --git a/Adamant/Wallets/WalletsRoutes.swift b/Adamant/Wallets/WalletsRoutes.swift deleted file mode 100644 index d8a98a97d..000000000 --- a/Adamant/Wallets/WalletsRoutes.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// WalletsRoutes.swift -// Adamant -// -// Created by Anokhov Pavel on 14.08.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene { - struct Wallets { - - private init() { } - } -} diff --git a/AdamantTests/Core/adamant-core.js b/AdamantTests/Core/adamant-core.js index 9307a0077..fd8b69d08 100644 --- a/AdamantTests/Core/adamant-core.js +++ b/AdamantTests/Core/adamant-core.js @@ -66584,4 +66584,4 @@ exports.decode = decode; //# sourceMappingURL=utf8.js.map /***/ }) -/******/ ]); \ No newline at end of file +/******/ ]); diff --git a/CommonKit/Scripts/CoinsScript.rb b/CommonKit/Scripts/CoinsScript.rb index 2b05035c8..c8ca7d70c 100755 --- a/CommonKit/Scripts/CoinsScript.rb +++ b/CommonKit/Scripts/CoinsScript.rb @@ -21,21 +21,34 @@ def writeToSwiftFile(name, json) # Read data from json + fullName = json["name"] + symbol = json["symbol"] + decimals = json["decimals"] + explorerTx = json["explorerTx"] + nodes = "" nodesArray = json["nodes"] if nodesArray != nil nodesArray.each do |node| url = node["url"] - nodes += "Node(url: URL(string: \"#{url}\")!),\n" + altUrl = node["alt_ip"] + if altUrl == nil + nodes += "Node(url: URL(string: \"#{url}\")!),\n" + else + nodes += "Node(url: URL(string: \"#{url}\")!, altUrl: URL(string: \"#{altUrl}\")),\n" + end end end serviceNodes = "" - serviceNodesArray = json["serviceNodes"] - if serviceNodesArray != nil - serviceNodesArray.each do |node| - url = node["url"] - serviceNodes += "Node(url: URL(string: \"#{url}\")!),\n" + services = json["services"] + if services != nil + serviceNodesArray = services["#{symbol.downcase}Service"] + if serviceNodesArray != nil + serviceNodesArray.each do |node| + url = node["url"] + serviceNodes += "Node(url: URL(string: \"#{url}\")!),\n" + end end end @@ -47,10 +60,6 @@ def writeToSwiftFile(name, json) fixedFee = 0.0 end - fullName = json["name"] - symbol = json["symbol"] - decimals = json["decimals"] - explorerTx = json["explorerTx"] consistencyMaxTime = json["txConsistencyMaxTime"] if consistencyMaxTime == nil consistencyMaxTime = 0 @@ -74,6 +83,13 @@ def writeToSwiftFile(name, json) defaultOrdinalLevel = json["defaultOrdinalLevel"] + minNodeVersion = json["minNodeVersion"] + if minNodeVersion == nil + minNodeVersion = "nil" + else + minNodeVersion = "\"#{minNodeVersion}\"" + end + # txFetchInfo txFetchInfo = json["txFetchInfo"] @@ -187,6 +203,10 @@ def writeToSwiftFile(name, json) #{defaultOrdinalLevel} } + var minNodeVersion: String? { + #{minNodeVersion} + } + static let explorerAddress = \"#{explorerTx.sub! '${ID}', ''}\" static var nodes: [Node] { @@ -218,9 +238,9 @@ def writeToSwiftFile(name, json) } }" File.open(Dir.pwd + "/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift", 'w') { |file| file.write(textResources) } - File.open(Dir.pwd + "/Adamant/Wallets/#{name}/#{symbol}WalletService+DynamicConstants.swift", 'w') { |file| file.write(text) } + File.open(Dir.pwd + "/Adamant/Modules/Wallets/#{name}/#{symbol}WalletService+DynamicConstants.swift", 'w') { |file| file.write(text) } else - File.open(Dir.pwd + "/Adamant/Wallets/#{name}/#{symbol}WalletService+DynamicConstants.swift", 'w') { |file| file.write(text) } + File.open(Dir.pwd + "/Adamant/Modules/Wallets/#{name}/#{symbol}WalletService+DynamicConstants.swift", 'w') { |file| file.write(text) } end end @@ -246,4 +266,4 @@ def startUnpack(branch) end -Coins.new.startUnpack("master") #master #dev +Coins.new.startUnpack("dev") #master #dev diff --git a/CommonKit/Scripts/UpdateWalletsScript.sh b/CommonKit/Scripts/UpdateWalletsScript.sh index 76e702e8c..a98aab927 100755 --- a/CommonKit/Scripts/UpdateWalletsScript.sh +++ b/CommonKit/Scripts/UpdateWalletsScript.sh @@ -1,5 +1,5 @@ ROOT="$PWD" -BRANCH_NAME="master" #master #dev +BRANCH_NAME="dev" #master #dev SCRIPTS_DIR="$ROOT/scripts" WALLETS_DIR="$ROOT/scripts/wallets" WALLETS_NAME_DIR="$ROOT/scripts/wallets/adamant-wallets-$BRANCH_NAME/assets/general" diff --git a/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift b/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift index c538d3afc..a77ac3571 100644 --- a/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift +++ b/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift @@ -6,10 +6,10 @@ public extension AdamantResources { [ Node(url: URL(string: "https://clown.adamant.im")!), Node(url: URL(string: "https://lake.adamant.im")!), -Node(url: URL(string: "https://endless.adamant.im")!), +Node(url: URL(string: "https://endless.adamant.im")!, altUrl: URL(string: "http://149.102.157.15:36666")), Node(url: URL(string: "https://bid.adamant.im")!), Node(url: URL(string: "https://unusual.adamant.im")!), -Node(url: URL(string: "https://debate.adamant.im")!), +Node(url: URL(string: "https://debate.adamant.im")!, altUrl: URL(string: "http://95.216.161.113:36666")), Node(url: URL(string: "http://78.47.205.206:36666")!), Node(url: URL(string: "http://5.161.53.74:36666")!), Node(url: URL(string: "http://184.94.215.92:45555")!), @@ -18,4 +18,4 @@ Node(url: URL(string: "https://node2.blockchain2fa.io")!), ] } -} \ No newline at end of file +} diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings index 8e63f3473..7f7b85c0b 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings @@ -275,7 +275,7 @@ "ApiService.InternalError.ParsingFailed" = "Parsing fehlgeschlagen. Bericht senden"; /* Serious internal error: No nodes available */ -"ApiService.InternalError.NoNodesAvailable" = "No nodes available. Report a bug"; +"ApiService.InternalError.NoNodesAvailable" = "No active %@ nodes. Review the node list"; /* Eureka forms Cancel button */ "Cancel" = "Abbrechen"; @@ -427,6 +427,9 @@ /* Chat: Error scrolling to message, this message has been deleted and is no longer accessible */ "ChatScene.Error.messageWasDeleted" = "Entschuldigung, aber diese Nachricht wurde gelöscht und ist nicht mehr zugänglich"; +/* Chat: Error message is too big */ +"ChatScene.Error.messageIsTooBig" = "Die Botschaft ist zu groß. Senden Sie es in Teilen."; + /* Delegates page: scene title */ "Delegates.Title" = "Delegierte"; @@ -614,10 +617,10 @@ "NewChatScene.NotInitialized.HelpButton" = "Was bedeutet das?"; /* NodesList: Button label */ -"NodesList.NodesList" = "Node-Liste"; +"NodesList.NodesList" = "ADM Node-Liste"; /* NodesList: scene title */ -"NodesList.Title" = "Liste der benutzten Nodes"; +"NodesList.Title" = "Liste der benutzten ADM Nodes"; /* NodesList: 'Saved' message */ "NodesList.Saved" = "Gespeichert"; @@ -637,6 +640,9 @@ /* NodesList: 'Prefer the fastest node' switch */ "NodesList.PreferTheFastestNode" = "Prefer the fastest node"; +/* NodesList: 'Prefer the fastest node' switch */ +"NodesList.PreferTheFastestNode.Footer" = "Verarbeiten Sie Anfragen schneller, aber es kann zu weniger Datenschutz führen"; + /* NodesList.NodeCell: Node ping */ "NodesList.NodeCell.Ping" = "Ping"; @@ -649,12 +655,21 @@ /* NodesList.NodeCell: Node is offline */ "NodesList.NodeCell.Offline" = "Offline"; +/* NodesList.NodeCell: Node is disabled */ +"NodesList.NodeCell.Disabled" = "Deaktiviert"; + /* NodesList.NodeCell: Node version */ "NodesList.NodeCell.Version" = "version"; /* NodeList: Inform that default nodes was loaded, if user deleted all nodes */ "NodeList.DefaultNodesLoaded" = "Default nodes list was loaded"; +/* CoinsNodesList: Title */ +"CoinsNodesList.Title" = "Liste der Münz- und Serviceknoten"; + +/* CoinsNodesList: ServiceNode */ +"CoinsNodesList.ServiceNode" = "Service"; + /* NodesEditor: New node scene title */ "NodesEditor.NewNodeTitle" = "New node"; @@ -1140,3 +1155,9 @@ /* Pending message reply error */ "Reply.pendingMessageError" = "Sie können nicht auf eine ausstehende Nachricht antworten. Auf Bestätigungen warten (schätzungsweise 1–2 Sekunden)"; + +/* Include partner name */ +"PartnerQR.includePartnerName" = "Neem contact op met"; + +/* Include partner url */ +"PartnerQR.includePartnerURL" = "Koppeling naar webapp opnemen"; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings index ea1976c2b..e01391cfb 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings @@ -272,7 +272,7 @@ "ApiService.InternalError.ParsingFailed" = "Parsing failed. Report a bug"; /* Serious internal error: No nodes available */ -"ApiService.InternalError.NoNodesAvailable" = "No nodes available. Report a bug"; +"ApiService.InternalError.NoNodesAvailable" = "No active %@ nodes. Review the node list"; /* Eureka forms Cancel button */ "Cancel" = "Cancel"; @@ -424,6 +424,9 @@ /* Chat: Error scrolling to message, this message has been deleted and is no longer accessible */ "ChatScene.Error.messageWasDeleted" = "Sorry, but this message has been deleted and is no longer accessible"; +/* Chat: Error message is too big */ +"ChatScene.Error.messageIsTooBig" = "The message is too big. Send it in parts."; + /* Delegates page: scene title */ "Delegates.Title" = "Delegates"; @@ -611,10 +614,10 @@ "NewChatScene.NotInitialized.HelpButton" = "What does it mean?"; /* NodesList: Button label */ -"NodesList.NodesList" = "Node list"; +"NodesList.NodesList" = "ADM node list"; /* NodesList: scene title */ -"NodesList.Title" = "List of nodes"; +"NodesList.Title" = "List of ADM nodes"; /* NodesList: 'Add new node' button lable */ "NodesList.AddNewNode" = "Add new node"; @@ -628,6 +631,9 @@ /* NodesList: 'Prefer the fastest node' switch */ "NodesList.PreferTheFastestNode" = "Prefer the fastest node"; +/* NodesList: 'Prefer the fastest node' switch */ +"NodesList.PreferTheFastestNode.Footer" = "Process requests faster, but it may cause less privacy"; + /* NodesList.NodeCell: Node ping */ "NodesList.NodeCell.Ping" = "Ping"; @@ -640,12 +646,21 @@ /* NodesList.NodeCell: Node is offline */ "NodesList.NodeCell.Offline" = "Offline"; +/* NodesList.NodeCell: Node is disabled */ +"NodesList.NodeCell.Disabled" = "Disabled"; + /* NodesList.NodeCell: Node version */ "NodesList.NodeCell.Version" = "version"; /* NodeList: Inform that default nodes was loaded, if user deleted all nodes */ "NodeList.DefaultNodesLoaded" = "Default nodes list was loaded"; +/* CoinsNodesList: Title */ +"CoinsNodesList.Title" = "Coin and service node list"; + +/* CoinsNodesList: ServiceNode */ +"CoinsNodesList.ServiceNode" = "Service"; + /* NodesEditor: New node scene title */ "NodesEditor.NewNodeTitle" = "New node"; @@ -1119,3 +1134,9 @@ /* Pending message reply error */ "Reply.pendingMessageError" = "You can't reply to a pending message. Wait for confirmations (estimate 1-2 seconds)"; + +/* Include partner name */ +"PartnerQR.includePartnerName" = "Include contact name"; + +/* Include partner url */ +"PartnerQR.includePartnerURL" = "Include Web app link"; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings index e92083edf..262f07c4d 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings @@ -71,7 +71,7 @@ "Contribute.Section.RunNodes" = "Запустите узел сети АДАМАНТа"; /* Contribute scene: 'Run nodes' section description. */ -"Contribute.Section.RunNodesDescription" = "Поддержите децентрализацию и повысьте уровень конфиденциальности"; +"Contribute.Section.RunNodesDescription" = "Поддержите децентрализацию и повысьте уровень приватности"; /* Contribute scene: 'Network delegate' section title. */ "Contribute.Section.NetworkDelegate" = "Станьте делегатом сети"; @@ -272,7 +272,7 @@ "ApiService.InternalError.ParsingFailed" = "Не удалось разобрать ответ узла блокчена. Сообщите разработчикам"; /* Serious internal error: No nodes available */ -"ApiService.InternalError.NoNodesAvailable" = "Нет доступных нод. Сообщите разработчикам"; +"ApiService.InternalError.NoNodesAvailable" = "Нет доступных %@ нод. Просмотрите список узлов"; /* Eureka forms Cancel button */ "Cancel" = "Отмена"; @@ -424,6 +424,9 @@ /* Chat: Error scrolling to message, this message has been deleted and is no longer accessible */ "ChatScene.Error.messageWasDeleted" = "Извините, но это сообщение было удалено и больше недоступно"; +/* Chat: Error message is too big */ +"ChatScene.Error.messageIsTooBig" = "Сообщение слишком большое. Отправьте его частями."; + /* Delegates page: scene title */ "Delegates.Title" = "Делегаты"; @@ -611,10 +614,10 @@ "NewChatScene.NotInitialized.HelpButton" = "Что это значит?"; /* NodesList: Button label */ -"NodesList.NodesList" = "Список нод"; +"NodesList.NodesList" = "Список нод ADM"; /* NodesList: scene title */ -"NodesList.Title" = "Список нод"; +"NodesList.Title" = "Список нод ADM"; /* NodesList: 'Add new node' button lable */ "NodesList.AddNewNode" = "Добавить новую ноду"; @@ -628,6 +631,9 @@ /* NodesList: 'Prefer the fastest node' switch */ "NodesList.PreferTheFastestNode" = "Выбирать самую быструю ноду"; +/* NodesList: 'Prefer the fastest node' switch */ +"NodesList.PreferTheFastestNode.Footer" = "Обрабатывайте запросы быстрее, но это может привести к снижению приватности."; + /* NodesList.NodeCell: Node ping */ "NodesList.NodeCell.Ping" = "Пинг"; @@ -643,6 +649,15 @@ /* NodesList.NodeCell: Node version */ "NodesList.NodeCell.Version" = "версия"; +/* CoinsNodesList: Title */ +"CoinsNodesList.Title" = "Ноды сервисов и монет"; + +/* NodesList.NodeCell: Node is disabled */ +"NodesList.NodeCell.Disabled" = "Отключена"; + +/* CoinsNodesList: ServiceNode */ +"CoinsNodesList.ServiceNode" = "Сервис"; + /* NodeList: Inform that default nodes was loaded, if user deleted all nodes */ "NodeList.DefaultNodesLoaded" = "Был загружен список нод по умолчанию"; @@ -1116,3 +1131,9 @@ /* Pending message reply error */ "Reply.pendingMessageError" = "Вы не можете ответить на ожидающее сообщение. Дождитесь подтверждения (оценка 1-2 секунды)"; + +/* Include partner name */ +"PartnerQR.includePartnerName" = "Добавить имя контакта"; + +/* Include partner url */ +"PartnerQR.includePartnerURL" = "Добавить ссылку на веб-приложение"; diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/Contents.json new file mode 100644 index 000000000..60cc4eab0 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "filename" : "invisible.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "invisible_dark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "invisible@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "invisible_dark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "invisible@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "invisible_dark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible.png new file mode 100644 index 000000000..efda9a00b Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible@2x.png new file mode 100644 index 000000000..8fed53a14 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible@2x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible@3x.png new file mode 100644 index 000000000..d19c8a4dc Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible_dark.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible_dark.png new file mode 100644 index 000000000..90e125645 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible_dark.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible_dark@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible_dark@2x.png new file mode 100644 index 000000000..67424cd90 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible_dark@2x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible_dark@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible_dark@3x.png new file mode 100644 index 000000000..811f676ee Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_close.imageset/invisible_dark@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/Contents.json new file mode 100644 index 000000000..68db4069e --- /dev/null +++ b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "filename" : "visible.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "visible_dark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "visible@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "visible_dark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "visible@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "visible_dark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible.png new file mode 100644 index 000000000..03284f7c4 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible@2x.png new file mode 100644 index 000000000..f1294845e Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible@2x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible@3x.png new file mode 100644 index 000000000..fdcb518fa Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible_dark.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible_dark.png new file mode 100644 index 000000000..341f4b539 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible_dark.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible_dark@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible_dark@2x.png new file mode 100644 index 000000000..fcfcc5522 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible_dark@2x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible_dark@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible_dark@3x.png new file mode 100644 index 000000000..c9b41e0b5 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/eye_open.imageset/visible_dark@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/Contents.json new file mode 100644 index 000000000..585ef3f55 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "transfer-in_bot_transparent.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "transfer-in_bot_transparent@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "transfer-in_bot_transparent@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/transfer-in_bot_transparent.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/transfer-in_bot_transparent.png new file mode 100644 index 000000000..38c526d51 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/transfer-in_bot_transparent.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/transfer-in_bot_transparent@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/transfer-in_bot_transparent@2x.png new file mode 100644 index 000000000..3334366a1 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/transfer-in_bot_transparent@2x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/transfer-in_bot_transparent@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/transfer-in_bot_transparent@3x.png new file mode 100644 index 000000000..71e010a50 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Transfers/transfer-self_bot.imageset/transfer-in_bot_transparent@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Core/SecuredStore.swift b/CommonKit/Sources/CommonKit/Core/SecuredStore.swift index a67325f94..0c8e8d466 100644 --- a/CommonKit/Sources/CommonKit/Core/SecuredStore.swift +++ b/CommonKit/Sources/CommonKit/Core/SecuredStore.swift @@ -46,6 +46,11 @@ public extension StoreKey { enum emojis { public static let emojis = "emojis" } + + enum partnerQR { + public static let includeNameEnabled = "includeNameEnabled" + public static let includeURLEnabled = "includeURLEnabled" + } } public protocol SecuredStore: AnyObject { diff --git a/CommonKit/Sources/CommonKit/Helpers/Encodable+Dictionary.swift b/CommonKit/Sources/CommonKit/Helpers/Encodable+Dictionary.swift index e4f26ba0f..201048be4 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Encodable+Dictionary.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Encodable+Dictionary.swift @@ -9,7 +9,7 @@ import Foundation public extension Encodable { - var asDictionary: [String: Any]? { + func asDictionary() -> [String: Any]? { guard let data = try? JSONEncoder().encode(self), let object = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) diff --git a/CommonKit/Sources/CommonKit/Helpers/HashableAction.swift b/CommonKit/Sources/CommonKit/Helpers/HashableAction.swift deleted file mode 100644 index 0d9fd783a..000000000 --- a/CommonKit/Sources/CommonKit/Helpers/HashableAction.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// HashableAction.swift -// -// -// Created by Andrey Golubenko on 06.12.2022. -// - -import Foundation - -public struct HashableAction { - public let id: Int - public let action: () -> Void - - public init(id: Int, action: @escaping () -> Void) { - self.id = id - self.action = action - } -} - -extension HashableAction: Equatable { - public static func == (lhs: HashableAction, rhs: HashableAction) -> Bool { - lhs.id == rhs.id - } -} - -extension HashableAction: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} diff --git a/CommonKit/Sources/CommonKit/Helpers/HashableIDWrapper.swift b/CommonKit/Sources/CommonKit/Helpers/HashableIDWrapper.swift new file mode 100644 index 000000000..f3c2ac52e --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/HashableIDWrapper.swift @@ -0,0 +1,36 @@ +// +// File.swift +// +// +// Created by Stanislav Jelezoglo on 19.10.2023. +// + +import Foundation + +public struct HashableIDWrapper: Hashable { + public let identifier: ComplexIdentifier + public let value: Value + + public init(identifier: ComplexIdentifier, value: Value) { + self.identifier = identifier + self.value = value + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(identifier) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.identifier == rhs.identifier + } +} + +public struct ComplexIdentifier: Hashable { + public let identifier: String + public let index: Int + + public init(identifier: String, index: Int) { + self.identifier = identifier + self.index = index + } +} diff --git a/CommonKit/Sources/CommonKit/Helpers/IDWrapper.swift b/CommonKit/Sources/CommonKit/Helpers/IDWrapper.swift new file mode 100644 index 000000000..d30a5208f --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/IDWrapper.swift @@ -0,0 +1,31 @@ +// +// IDWrapper.swift +// Adamant +// +// Created by Andrey Golubenko on 17.07.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +public struct IDWrapper: Identifiable { + public let id: String + public let value: T + + public init(id: String, value: T) { + self.id = id + self.value = value + } +} + +extension IDWrapper: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } +} + +extension IDWrapper: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/CommonKit/Sources/CommonKit/Helpers/IdentifiableContainer.swift b/CommonKit/Sources/CommonKit/Helpers/IdentifiableContainer.swift deleted file mode 100644 index 1ef54151a..000000000 --- a/CommonKit/Sources/CommonKit/Helpers/IdentifiableContainer.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// IdentifiableContainer.swift -// Adamant -// -// Created by Andrey Golubenko on 17.07.2023. -// Copyright © 2023 Adamant. All rights reserved. -// - -import Foundation - -public struct IdentifiableContainer>: Identifiable { - public let value: T - - public var id: String { - value.rawValue - } - - public init(value: T) { - self.value = value - } -} diff --git a/CommonKit/Sources/CommonKit/Helpers/Nodes+Allowance.swift b/CommonKit/Sources/CommonKit/Helpers/Nodes+Allowance.swift index 57c7c2a5b..21f7b0721 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Nodes+Allowance.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Nodes+Allowance.swift @@ -6,11 +6,12 @@ // Copyright © 2022 Adamant. All rights reserved. // -public extension Collection where Element: Node { +public extension Collection where Element == Node { func getAllowedNodes(sortedBySpeedDescending: Bool, needWS: Bool) -> [Node] { var allowedNodes = filter { $0.connectionStatus == .allowed - && (!needWS || $0.status?.wsEnabled ?? false) + && $0.isEnabled + && (!needWS || $0.wsEnabled) } if allowedNodes.isEmpty && !needWS { @@ -19,7 +20,7 @@ public extension Collection where Element: Node { return sortedBySpeedDescending ? allowedNodes.sorted { - $0.status?.ping ?? .greatestFiniteMagnitude < $1.status?.ping ?? .greatestFiniteMagnitude + $0.ping ?? .greatestFiniteMagnitude < $1.ping ?? .greatestFiniteMagnitude } : allowedNodes.shuffled() } diff --git a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/DragGesture+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/DragGesture+Extension.swift deleted file mode 100644 index e92ff8006..000000000 --- a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/DragGesture+Extension.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// File.swift -// -// -// Created by Andrey Golubenko on 24.07.2023. -// - -import SwiftUI - -public extension DragGesture.Value { - var velocity: CGSize { - .init( - width: predictedEndLocation.x - location.x, - height: predictedEndLocation.y - location.y - ) - } -} diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift index 10d883295..5e4077409 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift @@ -105,7 +105,7 @@ extension UIColor { /// Reactions background color public static var reactionsBackground: UIColor { - let colorWhiteTheme = UIColor(red: 0.29, green: 0.29, blue: 0.29, alpha: 0.1) + let colorWhiteTheme = UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1) let colorDarkTheme = UIColor(red: 0.264, green: 0.264, blue: 0.264, alpha: 1) return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } diff --git a/CommonKit/Sources/CommonKit/Helpers/URL+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/URL+Extension.swift deleted file mode 100644 index 020eb47dd..000000000 --- a/CommonKit/Sources/CommonKit/Helpers/URL+Extension.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// URL+Extension.swift -// Adamant -// -// Created by Andrey Golubenko on 17.07.2023. -// Copyright © 2023 Adamant. All rights reserved. -// - -import Foundation - -extension URL: RawRepresentable { - public typealias RawValue = String - - public init?(rawValue: String) { - self.init(string: rawValue) - } - - public var rawValue: String { - absoluteString - } -} diff --git a/CommonKit/Sources/CommonKit/Models/Node.swift b/CommonKit/Sources/CommonKit/Models/Node.swift index 23347edfa..cdcbd703f 100644 --- a/CommonKit/Sources/CommonKit/Models/Node.swift +++ b/CommonKit/Sources/CommonKit/Models/Node.swift @@ -8,167 +8,107 @@ import Foundation -public enum URLScheme: String, Codable { - case http, https - - public static let `default`: URLScheme = .https - - public var defaultPort: Int { - switch self { - case .http: return 36666 - case .https: return 443 - } - } -} - -final public class Node: Equatable { - public struct Status: Equatable, Codable { - public let ping: TimeInterval - public let wsEnabled: Bool - public let height: Int? - public let version: String? - - public init(ping: TimeInterval, wsEnabled: Bool, height: Int?, version: String?) { - self.ping = ping - self.wsEnabled = wsEnabled - self.height = height - self.version = version - } - } - - public enum ConnectionStatus: Equatable, Codable { - case offline - case synchronizing - case allowed - } - - public static func == (lhs: Node, rhs: Node) -> Bool { - lhs.scheme == rhs.scheme - && lhs.host == rhs.host - && lhs.port == rhs.port - && lhs.status == rhs.status - && lhs.isEnabled == rhs.isEnabled - && lhs._connectionStatus == rhs._connectionStatus - } +public struct Node: Equatable, Codable, Identifiable { + public let id: UUID + public var scheme: URLScheme + public var host: String + public var isEnabled: Bool + public var wsEnabled: Bool + public var port: Int? + public var wsPort: Int? + public var version: String? + public var height: Int? + public var ping: TimeInterval? + public var connectionStatus: ConnectionStatus? public init( + id: UUID = .init(), scheme: URLScheme, host: String, + isEnabled: Bool, + wsEnabled: Bool, port: Int? = nil, wsPort: Int? = nil, - status: Status? = nil, - isEnabled: Bool = true, + version: String? = nil, + height: Int? = nil, + ping: TimeInterval? = nil, connectionStatus: ConnectionStatus? = nil ) { + self.id = id self.scheme = scheme self.host = host + self.isEnabled = isEnabled + self.wsEnabled = wsEnabled self.port = port self.wsPort = wsPort - self.status = status - self.isEnabled = isEnabled - self._connectionStatus = connectionStatus + self.version = version + self.height = height + self.ping = ping + self.connectionStatus = connectionStatus } - - public init(url: URL) { - let schemeRaw = url.scheme ?? "https" - self.scheme = URLScheme(rawValue: schemeRaw) ?? .https - self.host = url.host ?? "" - self.port = url.port - self.isEnabled = true +} + +public extension Node { + enum ConnectionStatus: Equatable, Codable { + case offline + case synchronizing + case allowed } - @Atomic public var scheme: URLScheme - @Atomic public var host: String - @Atomic public var port: Int? - @Atomic public var wsPort: Int? - @Atomic public var status: Status? - @Atomic public var isEnabled: Bool - - @Atomic private var _connectionStatus: ConnectionStatus? + enum URLScheme: String, Codable { + case http, https + + public static let `default`: URLScheme = .https + + public var defaultPort: Int { + switch self { + case .http: return 36666 + case .https: return 443 + } + } + } - public var connectionStatus: ConnectionStatus? { - get { isEnabled ? _connectionStatus : nil } - set { _connectionStatus = newValue } + init(url: URL, altUrl _: URL? = nil) { + self.init( + scheme: URLScheme(rawValue: url.scheme ?? .empty) ?? .https, + host: url.host ?? .empty, + isEnabled: true, + wsEnabled: false, + port: url.port + ) } - public func asString() -> String { - if let url = asURL(forcePort: scheme != URLScheme.default) { + func asString() -> String { + if let url = asURL(forcePort: scheme != .https) { return url.absoluteString } else { return host } } - /// Builds URL, using specified port, or default scheme's port, if nil - /// - /// - Returns: URL, if no errors were thrown - - public func asSocketURL() -> URL? { - return asURL(forcePort: false, useWsPort: true) + func asSocketURL() -> URL? { + asURL(forcePort: false, useWsPort: true) } - - public func asURL() -> URL? { - return asURL(forcePort: true) + + func asURL() -> URL? { + asURL(forcePort: true) } - - private func asURL(forcePort: Bool, useWsPort: Bool = false) -> URL? { +} + +private extension Node { + func asURL(forcePort: Bool, useWsPort: Bool = false) -> URL? { var components = URLComponents() components.scheme = scheme.rawValue components.host = host - + let usePort = useWsPort ? wsPort : port - + if let port = usePort, scheme == .http { components.port = port } else if forcePort { components.port = usePort ?? scheme.defaultPort } - - return components.url - } -} -extension Node: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(scheme, forKey: .scheme) - try container.encode(host, forKey: .host) - try container.encode(port, forKey: .port) - try container.encode(wsPort, forKey: .wsPort) - try container.encode(status, forKey: .status) - try container.encode(isEnabled, forKey: .isEnabled) - try container.encode(_connectionStatus, forKey: ._connectionStatus) - } -} - -extension Node: Decodable { - public convenience init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.init( - scheme: try container.decode(URLScheme.self, forKey: .scheme), - host: try container.decode(String.self, forKey: .host), - port: try? container.decode(Optional.self, forKey: .port), - wsPort: try? container.decode(Optional.self, forKey: .wsPort), - status: try? container.decode(Optional.self, forKey: .status), - isEnabled: try container.decode(Bool.self, forKey: .isEnabled), - connectionStatus: try? container.decode( - Optional.self, - forKey: ._connectionStatus - ) - ) - } -} - -private extension Node { - enum CodingKeys: String, CodingKey { - case scheme - case host - case port - case wsPort - case status - case isEnabled - case _connectionStatus + return components.url } } diff --git a/CommonKit/Sources/CommonKit/Models/NodeGroup+Constants.swift b/CommonKit/Sources/CommonKit/Models/NodeGroup+Constants.swift new file mode 100644 index 000000000..1fedf72cd --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/NodeGroup+Constants.swift @@ -0,0 +1,58 @@ +// +// NodeGroup+Constants.swift +// +// +// Created by Andrew G on 18.11.2023. +// + +import Foundation + +public extension NodeGroup { + var crucialUpdateInterval: TimeInterval { 30 } + var onScreenUpdateInterval: TimeInterval { 10 } + + var nodeHeightEpsilon: Int { + switch self { + case .adm: + return 10 + case .btc: + return 2 + case .eth: + return 5 + case .lskService, .lskNode: + return 5 + case .doge: + return 3 + case .dash: + return 3 + } + } + + var normalUpdateInterval: TimeInterval { + switch self { + case .adm: + return 300000 + case .btc: + return 360000 + case .eth: + return 300000 + case .lskNode: + return 270000 + case .lskService: + return 330000 + case .doge: + return 390000 + case .dash: + return 210000 + } + } + + var defaultFastestNodeMode: Bool { + switch self { + case .adm: + return false + case .eth, .lskNode, .lskService, .doge, .dash, .btc: + return true + } + } +} diff --git a/CommonKit/Sources/CommonKit/Models/NodeGroup.swift b/CommonKit/Sources/CommonKit/Models/NodeGroup.swift new file mode 100644 index 000000000..d87fb9d80 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/NodeGroup.swift @@ -0,0 +1,16 @@ +// +// NodeGroup.swift +// +// +// Created by Andrew G on 30.10.2023. +// + +public enum NodeGroup: Codable, CaseIterable, Hashable { + case btc + case eth + case lskNode + case lskService + case doge + case dash + case adm +} diff --git a/CommonKit/Sources/CommonKit/Services/KeychainStore.swift b/CommonKit/Sources/CommonKit/Services/KeychainStore.swift index 60714d917..bd290b26d 100644 --- a/CommonKit/Sources/CommonKit/Services/KeychainStore.swift +++ b/CommonKit/Sources/CommonKit/Services/KeychainStore.swift @@ -26,12 +26,7 @@ public final class KeychainStore: SecuredStore { let data = raw.data(using: .utf8) else { return nil } - do { - return try JSONDecoder().decode(T.self, from: data) - } catch { - assertionFailure("Failed to decode data. Error: \(error.localizedDescription)") - return nil - } + return try? JSONDecoder().decode(T.self, from: data) } public func set(_ value: T, for key: String) { @@ -40,12 +35,8 @@ public final class KeychainStore: SecuredStore { return } - do { - let data = try JSONEncoder().encode(value) - String(data: data, encoding: .utf8).map { setString($0, for: key) } - } catch { - assertionFailure("Failed to encode data. Error: \(error.localizedDescription)") - } + guard let data = try? JSONEncoder().encode(value) else { return } + String(data: data, encoding: .utf8).map { setString($0, for: key) } } public func remove(_ key: String) { diff --git a/LiskKit/Sources/API/Node/Models/NodeStatusModel.swift b/LiskKit/Sources/API/Node/Models/NodeStatusModel.swift index 69b794450..e9bfdc8cd 100644 --- a/LiskKit/Sources/API/Node/Models/NodeStatusModel.swift +++ b/LiskKit/Sources/API/Node/Models/NodeStatusModel.swift @@ -44,6 +44,8 @@ extension Node { public let version: String public let networkVersion: String + + public let height: Int? } } diff --git a/LiskKit/Sources/API/Service/Service.swift b/LiskKit/Sources/API/Service/Service.swift index c325f00e6..bee73e0ed 100644 --- a/LiskKit/Sources/API/Service/Service.swift +++ b/LiskKit/Sources/API/Service/Service.swift @@ -38,7 +38,18 @@ extension Service { } /// List transaction objects - public func transactions(id: String? = nil, block: String? = nil, sender: String? = nil, recipient: String? = nil, senderIdOrRecipientId: String? = nil, limit: UInt? = nil, offset: UInt? = nil, sort: APIRequest.Sort? = nil, completionHandler: @escaping (Result<[Transactions.TransactionModel]>) -> Void) { + public func transactions( + ownerAddress: String?, + id: String? = nil, + block: String? = nil, + sender: String? = nil, + recipient: String? = nil, + senderIdOrRecipientId: String? = nil, + limit: UInt? = nil, + offset: UInt? = nil, + sort: APIRequest.Sort? = nil, + completionHandler: @escaping (Result<[Transactions.TransactionModel]>) -> Void + ) { if version == .v1 { transactionsV1(id: id, block: block, sender: sender, recipient: recipient, senderIdOrRecipientId: senderIdOrRecipientId, limit: limit, offset: offset, sort: sort) { result in switch result { @@ -53,19 +64,22 @@ extension Service { switch result { case .success(response: let value): let transaction = value.data.map { - Transactions.TransactionModel(id: $0.id, - height: $0.height, - blockId: $0.blockId, - type: $0.type, - timestamp: $0.timestamp, - senderPublicKey: $0.senderPublicKey, - senderId: $0.senderId, - recipientId: $0.recipientId, - recipientPublicKey: $0.recipientPublicKey, - amount: $0.amount, - fee: $0.fee, - signature: $0.signature, - confirmations: $0.confirmations) + Transactions.TransactionModel( + id: $0.id, + height: $0.height, + blockId: $0.blockId, + type: $0.type, + timestamp: $0.timestamp, + senderPublicKey: $0.senderPublicKey, + senderId: $0.senderId, + recipientId: $0.recipientId, + recipientPublicKey: $0.recipientPublicKey, + amount: $0.amount, + fee: $0.fee, + signature: $0.signature, + confirmations: $0.confirmations, + isOutgoing: $0.senderId.lowercased() == ownerAddress?.lowercased() + ) } completionHandler(.success(response: transaction)) case .error(response: let error): diff --git a/LiskKit/Sources/API/Transactions/LocalTransaction.swift b/LiskKit/Sources/API/Transactions/LocalTransaction.swift index f6c269a1b..09fd4dad1 100644 --- a/LiskKit/Sources/API/Transactions/LocalTransaction.swift +++ b/LiskKit/Sources/API/Transactions/LocalTransaction.swift @@ -64,7 +64,8 @@ public struct TransactionEntity { public struct Asset { public var amount: UInt64 - public var recipientAddress: String + public var recipientAddressBase32: String + public var recipientAddressBinary: String public var data: String = "" public func bytes() -> [UInt8] { @@ -72,8 +73,8 @@ public struct TransactionEntity { value += generateKey(for: 1, with: 0) value += writeUInt64(amount) value += generateKey(for: 2, with: 2) - value += writeUInt32(UInt32(recipientAddress.allHexBytes().count)) - value += recipientAddress.allHexBytes() + value += writeUInt32(UInt32(recipientAddressBinary.allHexBytes().count)) + value += recipientAddressBinary.allHexBytes() value += generateKey(for: 3, with: 2) value += writeUInt32(UInt32(data.bytes.count)) if data.count > 0 { @@ -85,7 +86,7 @@ public struct TransactionEntity { public var requestOptions: RequestOptions { let options: RequestOptions = [ "amount": "\(amount)", - "recipientAddress": recipientAddress, + "recipientAddress": recipientAddressBinary, "data": data ] @@ -111,17 +112,43 @@ public struct TransactionEntity { self.signatures = signatures } - public init(amount: Decimal, fee: Decimal, nonce: String, senderPublicKey: String, recipientAddress: String) { + public init( + amount: Decimal, + fee: Decimal, + nonce: String, + senderPublicKey: String, + recipientAddressBase32: String, + recipientAddressBinary: String + ) { let amount = Crypto.fixedPoint(amount: amount) let fee = Crypto.fixedPoint(amount: fee) - self.init(amount: amount, fee: fee, nonce: nonce, senderPublicKey: senderPublicKey, recipientAddress: recipientAddress) - } - - public init(amount: UInt64, fee: UInt64, nonce: String, senderPublicKey: String, recipientAddress: String, signatures: [String] = []) { + self.init( + amount: amount, + fee: fee, + nonce: nonce, + senderPublicKey: senderPublicKey, + recipientAddressBase32: recipientAddressBase32, + recipientAddressBinary: recipientAddressBinary + ) + } + + public init( + amount: UInt64, + fee: UInt64, + nonce: String, + senderPublicKey: String, + recipientAddressBase32: String, + recipientAddressBinary: String, + signatures: [String] = [] + ) { self.fee = fee self.nonce = UInt64(nonce) ?? 0 self.senderPublicKey = senderPublicKey - self.asset = .init(amount: amount, recipientAddress: recipientAddress) + self.asset = .init( + amount: amount, + recipientAddressBase32: recipientAddressBase32, + recipientAddressBinary: recipientAddressBinary + ) self.signatures = signatures } diff --git a/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift b/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift index fbfdc0f9b..51a9b3abc 100644 --- a/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift +++ b/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift @@ -50,6 +50,8 @@ extension Transactions { public let signature: String public var confirmations: UInt64? + + public var isOutgoing: Bool = false // MARK: - Hashable diff --git a/LiskKit/Sources/Core/APIClient.swift b/LiskKit/Sources/Core/APIClient.swift index 436b1bc60..a54dfe9da 100644 --- a/LiskKit/Sources/Core/APIClient.swift +++ b/LiskKit/Sources/Core/APIClient.swift @@ -8,10 +8,7 @@ import Foundation /// Represents an HTTP response -public enum Response { - case success(response: R) - case error(response: APIError) -} +public typealias Response = Result public enum Result { case success(response: R) diff --git a/PopupKit/Sources/PopupKit/Implementation/Models/AdvancedAlertModel.swift b/PopupKit/Sources/PopupKit/Implementation/Models/AdvancedAlertModel.swift index 0e53db715..920133c58 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Models/AdvancedAlertModel.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Models/AdvancedAlertModel.swift @@ -33,9 +33,9 @@ public struct AdvancedAlertModel: Equatable, Hashable { public extension AdvancedAlertModel { struct Button: Equatable, Hashable { public let title: String - public let action: HashableAction + public let action: IDWrapper - public init(title: String, action: HashableAction) { + public init(title: String, action: IDWrapper) { self.title = title self.action = action } diff --git a/PopupKit/Sources/PopupKit/Implementation/Models/NotificationModel.swift b/PopupKit/Sources/PopupKit/Implementation/Models/NotificationModel.swift index 070670264..ebc5403be 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Models/NotificationModel.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Models/NotificationModel.swift @@ -12,5 +12,5 @@ struct NotificationModel: Equatable, Hashable { let icon: UIImage? let title: String? let description: String? - let tapHandler: HashableAction? + let tapHandler: IDWrapper? } diff --git a/PopupKit/Sources/PopupKit/Implementation/Models/PopupCoordinatorModel.swift b/PopupKit/Sources/PopupKit/Implementation/Models/PopupCoordinatorModel.swift index 9f6c7882e..2c7420fd7 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Models/PopupCoordinatorModel.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Models/PopupCoordinatorModel.swift @@ -10,6 +10,6 @@ import UIKit final class PopupCoordinatorModel: ObservableObject { @Published var notification: NotificationModel? @Published var alert: AlertModel? - @Published var toastMessage: String? @Published var advancedAlert: AdvancedAlertModel? + @Published var toastMessage: String? } diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/AdvancedAlertView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/AdvancedAlertView.swift index 33fa281e2..1e476e82a 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/AdvancedAlertView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/AdvancedAlertView.swift @@ -70,7 +70,7 @@ private extension AdvancedAlertView { } var primaryButton: some View { - Button(action: model.primaryButton.action.action) { + Button(action: model.primaryButton.action.value) { Text(model.primaryButton.title) .padding(.vertical, bigSpacing) .expanded(axes: .horizontal) @@ -87,7 +87,7 @@ private extension AdvancedAlertView { } func makeSecondaryButton(model: AdvancedAlertModel.Button) -> some View { - Button(model.title, action: model.action.action) + Button(model.title, action: model.action.value) } } diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift index 889ff59c5..c6fa15832 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift @@ -17,14 +17,15 @@ struct NotificationView: View { let dismissAction: () -> Void var body: some View { - HStack(alignment: .bottom, spacing: 7) { + HStack(alignment: .top, spacing: 8) { if let icon = model.icon { makeIcon(image: icon) } textStack Spacer(minLength: .zero) } - .padding(10) + .padding([.leading, .trailing], 15) + .padding([.top, .bottom], 10) .background(GeometryReader(content: processGeometry)) .padding(.top, safeAreaInsets.top) .expanded(axes: .horizontal) @@ -39,10 +40,11 @@ private extension NotificationView { func makeIcon(image: UIImage) -> some View { Image(uiImage: image) .resizable() - .renderingMode(.template) + .renderingMode(.original) .foregroundColor(.secondary) .scaledToFit() .frame(squareSize: 30) + .padding(.top, 2) } var textStack: some View { @@ -81,7 +83,7 @@ private extension NotificationView { } func onTap() { - model.tapHandler?.action() + model.tapHandler?.value() dismissAction() } } diff --git a/PopupKit/Sources/PopupKit/PopupManager.swift b/PopupKit/Sources/PopupKit/PopupManager.swift index 918eaa557..57a506cd1 100644 --- a/PopupKit/Sources/PopupKit/PopupManager.swift +++ b/PopupKit/Sources/PopupKit/PopupManager.swift @@ -82,7 +82,7 @@ public extension PopupManager { icon: icon, title: title, description: description, - tapHandler: tapHandler.map { .init(id: .zero, action: $0) } + tapHandler: tapHandler.map { .init(id: .empty, value: $0) } ) if autoDismiss { diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayView.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayView.swift index 2e13c1bf4..9b8624de0 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayView.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayView.swift @@ -62,6 +62,7 @@ struct ContextMenuOverlayView: View { viewModel.additionalMenuVisible.toggle() } viewModel.delegate?.didAppear() + viewModel.scrollToEnd = true } } } @@ -69,6 +70,10 @@ struct ContextMenuOverlayView: View { private extension ContextMenuOverlayView { func makeOverlayView() -> some View { + makeOverlayScrollToBottom(makeOverlayScrollView()) + } + + func makeOverlayScrollView() -> some View { ScrollView(axes, showsIndicators: false) { VStack(spacing: .zero) { makeContentView() @@ -80,11 +85,30 @@ private extension ContextMenuOverlayView { + minContentsSpace ) } + .id(1) } .fullScreen() .transition(.opacity) } + func makeOverlayScrollToBottom(_ content: some View) -> some View { + if #available(iOS 17.0, *) { + return content + .defaultScrollAnchor(.bottom) + } + + return ScrollViewReader { value in + content + .onChange(of: viewModel.scrollToEnd) { scrollToBottom in + guard scrollToBottom else { return } + + withAnimation { + value.scrollTo(1, anchor: .bottom) + } + } + } + } + func makeContentView() -> some View { HStack { UIViewWrapper(view: viewModel.contentView) diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayViewModel.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayViewModel.swift index 7c3813da2..7631807ea 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayViewModel.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayViewModel.swift @@ -35,6 +35,7 @@ final class ContextMenuOverlayViewModel: ObservableObject { @Published var additionalMenuVisible = false @Published var shouldScroll: Bool = false + @Published var scrollToEnd = false init( contentView: UIView, diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuViewController.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuViewController.swift index 33104233b..95b8b354c 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuViewController.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuViewController.swift @@ -234,10 +234,10 @@ extension AMenuViewController: UITableViewDelegate, UITableViewDataSource { let rowPosition: AMenuRowCell.RowPosition - if indexPath.row == 0 { - rowPosition = .top - } else if indexPath.row == menuContent.menuItems.count - 1 { + if indexPath.row == menuContent.menuItems.count - 1 { rowPosition = .bottom + } else if indexPath.row == .zero { + rowPosition = .top } else { rowPosition = .other }