diff --git a/.gitignore b/.gitignore index 7a7f97098..bdfc17fd2 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,7 @@ xcuserdata/ ### Here we store Release password. It's super-secret, not for git. ### AdamantSecret.swift +GitData.plist runkit ex.rtf to do.rtf tz.rtf diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index ddb3f1e79..7e58e03ef 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -7,7 +7,44 @@ objects = { /* Begin PBXBuildFile section */ + 2621AB372C60E74A00046D7A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2621AB362C60E74A00046D7A /* NotificationsView.swift */; }; + 2621AB392C60E7AE00046D7A /* NotificationsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2621AB382C60E7AE00046D7A /* NotificationsViewModel.swift */; }; + 2621AB3B2C613C8100046D7A /* NotificationsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2621AB3A2C613C8100046D7A /* NotificationsFactory.swift */; }; + 2657A0CA2C707D780021E7E6 /* notification.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = E9A174B820587B83003667CD /* notification.mp3 */; }; + 2657A0CB2C707D7B0021E7E6 /* so-proud-notification.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57A28C8B7DA009337F2 /* so-proud-notification.mp3 */; }; + 2657A0CC2C707D7E0021E7E6 /* relax-message-tone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57C28C8B7F9009337F2 /* relax-message-tone.mp3 */; }; + 2657A0CD2C707D800021E7E6 /* short-success.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57E28C8B834009337F2 /* short-success.mp3 */; }; + 2657A0CE2C707D830021E7E6 /* default.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D58028C8B8D1009337F2 /* default.mp3 */; }; 265AA1622B74E6B900CF98B0 /* ChatPreservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 265AA1612B74E6B900CF98B0 /* ChatPreservation.swift */; }; + 269B83102C74A2FF002AA1D7 /* note.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B830F2C74A2FF002AA1D7 /* note.mp3 */; }; + 269B83112C74A34F002AA1D7 /* note.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B830F2C74A2FF002AA1D7 /* note.mp3 */; }; + 269B831E2C74B4EC002AA1D7 /* handoff.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83122C74B4EA002AA1D7 /* handoff.mp3 */; }; + 269B831F2C74B4EC002AA1D7 /* handoff.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83122C74B4EA002AA1D7 /* handoff.mp3 */; }; + 269B83202C74B4EC002AA1D7 /* portal.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83132C74B4EA002AA1D7 /* portal.mp3 */; }; + 269B83212C74B4EC002AA1D7 /* portal.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83132C74B4EA002AA1D7 /* portal.mp3 */; }; + 269B83222C74B4EC002AA1D7 /* antic.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83142C74B4EB002AA1D7 /* antic.mp3 */; }; + 269B83232C74B4EC002AA1D7 /* antic.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83142C74B4EB002AA1D7 /* antic.mp3 */; }; + 269B83242C74B4EC002AA1D7 /* droplet.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83152C74B4EB002AA1D7 /* droplet.mp3 */; }; + 269B83252C74B4EC002AA1D7 /* droplet.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83152C74B4EB002AA1D7 /* droplet.mp3 */; }; + 269B83262C74B4EC002AA1D7 /* passage.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83162C74B4EB002AA1D7 /* passage.mp3 */; }; + 269B83272C74B4EC002AA1D7 /* passage.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83162C74B4EB002AA1D7 /* passage.mp3 */; }; + 269B83282C74B4EC002AA1D7 /* chord.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83172C74B4EB002AA1D7 /* chord.mp3 */; }; + 269B83292C74B4EC002AA1D7 /* chord.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83172C74B4EB002AA1D7 /* chord.mp3 */; }; + 269B832A2C74B4EC002AA1D7 /* rattle.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83182C74B4EB002AA1D7 /* rattle.mp3 */; }; + 269B832B2C74B4EC002AA1D7 /* rattle.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83182C74B4EB002AA1D7 /* rattle.mp3 */; }; + 269B832C2C74B4EC002AA1D7 /* rebound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83192C74B4EB002AA1D7 /* rebound.mp3 */; }; + 269B832D2C74B4EC002AA1D7 /* rebound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83192C74B4EB002AA1D7 /* rebound.mp3 */; }; + 269B832E2C74B4EC002AA1D7 /* milestone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831A2C74B4EC002AA1D7 /* milestone.mp3 */; }; + 269B832F2C74B4EC002AA1D7 /* milestone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831A2C74B4EC002AA1D7 /* milestone.mp3 */; }; + 269B83302C74B4EC002AA1D7 /* cheers.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831B2C74B4EC002AA1D7 /* cheers.mp3 */; }; + 269B83312C74B4EC002AA1D7 /* cheers.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831B2C74B4EC002AA1D7 /* cheers.mp3 */; }; + 269B83322C74B4EC002AA1D7 /* slide.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831C2C74B4EC002AA1D7 /* slide.mp3 */; }; + 269B83332C74B4EC002AA1D7 /* slide.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831C2C74B4EC002AA1D7 /* slide.mp3 */; }; + 269B83342C74B4EC002AA1D7 /* welcome.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831D2C74B4EC002AA1D7 /* welcome.mp3 */; }; + 269B83352C74B4EC002AA1D7 /* welcome.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831D2C74B4EC002AA1D7 /* welcome.mp3 */; }; + 269B83372C74D1F9002AA1D7 /* NotificationSoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269B83362C74D1F9002AA1D7 /* NotificationSoundsView.swift */; }; + 269B833A2C74D4AA002AA1D7 /* NotificationSoundsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269B83392C74D4AA002AA1D7 /* NotificationSoundsViewModel.swift */; }; + 269B833D2C74E661002AA1D7 /* NotificationSoundsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269B833C2C74E661002AA1D7 /* NotificationSoundsFactory.swift */; }; 269E13522B594B2D008D1CA7 /* AccountFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269E13512B594B2D008D1CA7 /* AccountFooterView.swift */; }; 26A975FF2B7E843E0095C367 /* SelectTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A975FE2B7E843E0095C367 /* SelectTextView.swift */; }; 26A976012B7E852E0095C367 /* ChatSelectTextViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A976002B7E852E0095C367 /* ChatSelectTextViewFactory.swift */; }; @@ -67,19 +104,15 @@ 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 */; }; - 3AA388032B67F47600125684 /* RPCResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA388022B67F47600125684 /* RPCResponseModel.swift */; }; 3AA388052B67F4DD00125684 /* BtcBlockchainInfoDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA388042B67F4DD00125684 /* BtcBlockchainInfoDTO.swift */; }; 3AA388072B67F53F00125684 /* BtcNetworkInfoDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA388062B67F53F00125684 /* BtcNetworkInfoDTO.swift */; }; 3AA3880A2B69173500125684 /* DashNetworkInfoDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA388092B69173500125684 /* DashNetworkInfoDTO.swift */; }; - 3AA3880C2B69201B00125684 /* ADM+JsonDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA3880B2B69201B00125684 /* ADM+JsonDecode.swift */; }; - 3AA3880E2B6A356900125684 /* RpcRequestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA3880D2B6A356900125684 /* RpcRequestModel.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 */; }; 3AA6DF402BA9941E00EA2E16 /* MediaContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA6DF3F2BA9941E00EA2E16 /* MediaContainerView.swift */; }; 3AA6DF442BA997C000EA2E16 /* FileListContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA6DF432BA997C000EA2E16 /* FileListContainerView.swift */; }; 3AA6DF462BA9BEB700EA2E16 /* MediaContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA6DF452BA9BEB700EA2E16 /* MediaContentView.swift */; }; - 3AB87CD62BF6237100AE8743 /* MultipartFormDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB87CD52BF6237100AE8743 /* MultipartFormDataModel.swift */; }; 3AC76E3D2AB09118008042C4 /* ElegantEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3AC76E3C2AB09118008042C4 /* ElegantEmojiPicker */; }; 3ACD307E2BBD86B700ABF671 /* FilesStorageProprietiesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACD307D2BBD86B700ABF671 /* FilesStorageProprietiesService.swift */; }; 3ACD30802BBD86C800ABF671 /* FilesStorageProprietiesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACD307F2BBD86C800ABF671 /* FilesStorageProprietiesProtocol.swift */; }; @@ -124,7 +157,6 @@ 4164A9D928F17DA700EEF16D /* AdamantChatTransactionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4164A9D828F17DA700EEF16D /* AdamantChatTransactionService.swift */; }; 416F5EA4290162EB00EF0400 /* SocketIO in Frameworks */ = {isa = PBXBuildFile; productRef = 416F5EA3290162EB00EF0400 /* SocketIO */; }; 4177E5E12A52DA7100C089FE /* AdvancedContextMenuKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4177E5E02A52DA7100C089FE /* AdvancedContextMenuKit */; }; - 417BA7F428BF894F00DF94C5 /* NotificationSoundsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 417BA7F328BF894F00DF94C5 /* NotificationSoundsViewController.swift */; }; 4184F16E2A33023A00D7B8B9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4184F16D2A33023A00D7B8B9 /* GoogleService-Info.plist */; }; 4184F1712A33044E00D7B8B9 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 4184F1702A33044E00D7B8B9 /* FirebaseCrashlytics */; }; 4184F1732A33102800D7B8B9 /* AdamantCrashlysticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4184F1722A33102800D7B8B9 /* AdamantCrashlysticsService.swift */; }; @@ -160,7 +192,6 @@ 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 */; }; - 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 */; }; 55D1D84F287B78F200F94A4E /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 55D1D84E287B78F200F94A4E /* SnapKit */; }; @@ -187,8 +218,6 @@ 6449BA6F235CA0930033B936 /* ERC20WalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA65235CA0930033B936 /* ERC20WalletService+Send.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 /* 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 */; }; @@ -232,8 +261,7 @@ 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 */; }; - 64EAB37422463E020018D9B2 /* CurrencyInfoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EAB37322463E020018D9B2 /* CurrencyInfoService.swift */; }; - 64EAB37622463F680018D9B2 /* AdamantCurrencyInfoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EAB37522463F680018D9B2 /* AdamantCurrencyInfoService.swift */; }; + 64EAB37422463E020018D9B2 /* InfoServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EAB37322463E020018D9B2 /* InfoServiceProtocol.swift */; }; 64F085D920E2D7600006DE68 /* AdmTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F085D820E2D7600006DE68 /* AdmTransactionsViewController.swift */; }; 64FA53CD20E1300B006783C9 /* EthTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53CC20E1300A006783C9 /* EthTransactionsViewController.swift */; }; 64FA53D120E24942006783C9 /* TransactionDetailsViewControllerBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53D020E24941006783C9 /* TransactionDetailsViewControllerBase.swift */; }; @@ -241,8 +269,12 @@ 9304F8BE292F88F900173F18 /* ANSPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9304F8BD292F88F900173F18 /* ANSPayload.swift */; }; 9304F8C2292F895C00173F18 /* PushNotificationsTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9304F8C1292F895C00173F18 /* PushNotificationsTokenService.swift */; }; 9304F8C4292F8A3100173F18 /* AdamantPushNotificationsTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9304F8C3292F8A3100173F18 /* AdamantPushNotificationsTokenService.swift */; }; - 9304F8C6292F971600173F18 /* ApiServiceResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9304F8C5292F971600173F18 /* ApiServiceResult.swift */; }; - 9304F8C8292F972600173F18 /* ApiServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9304F8C7292F972600173F18 /* ApiServiceError.swift */; }; + 931224A92C7AA0E4009E0ED0 /* InfoServiceApiService+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224A82C7AA0E4009E0ED0 /* InfoServiceApiService+Extension.swift */; }; + 931224AB2C7AA212009E0ED0 /* InfoServiceRatesRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224AA2C7AA212009E0ED0 /* InfoServiceRatesRequestDTO.swift */; }; + 931224AD2C7AA67B009E0ED0 /* InfoServiceHistoryRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224AC2C7AA67B009E0ED0 /* InfoServiceHistoryRequestDTO.swift */; }; + 931224AF2C7AA88E009E0ED0 /* InfoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224AE2C7AA88E009E0ED0 /* InfoService.swift */; }; + 931224B12C7ACFE6009E0ED0 /* InfoServiceAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224B02C7ACFE6009E0ED0 /* InfoServiceAssembly.swift */; }; + 931224B32C7AD5DD009E0ED0 /* InfoServiceTicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224B22C7AD5DD009E0ED0 /* InfoServiceTicker.swift */; }; 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 */; }; @@ -261,13 +293,9 @@ 93294B9A2AAD624100911109 /* WalletFactoryCompose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B992AAD624100911109 /* WalletFactoryCompose.swift */; }; 932B34E92974AA4A002A75BA /* ChatPreservationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932B34E82974AA4A002A75BA /* ChatPreservationProtocol.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 */; }; + 9332C39D2C76BE7500164B80 /* FileApiServiceResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332C39C2C76BE7500164B80 /* FileApiServiceResult.swift */; }; + 9332C3A32C76C45A00164B80 /* ApiServiceComposeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332C3A22C76C45A00164B80 /* ApiServiceComposeProtocol.swift */; }; + 9332C3A52C76C4EC00164B80 /* ApiServiceCompose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332C3A42C76C4EC00164B80 /* ApiServiceCompose.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 */; }; @@ -284,22 +312,37 @@ 93496BB52A6CAED100DD062F /* Roboto_300_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BAA2A6CAED100DD062F /* Roboto_300_normal.ttf */; }; 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 */; }; + 934FD9A42C783D2E00336841 /* InfoServiceStatusDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9A32C783D2E00336841 /* InfoServiceStatusDTO.swift */; }; + 934FD9A62C783DB700336841 /* InfoServiceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9A52C783DB700336841 /* InfoServiceStatus.swift */; }; + 934FD9A82C783E0C00336841 /* InfoServiceMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9A72C783E0C00336841 /* InfoServiceMapper.swift */; }; + 934FD9AA2C7842C800336841 /* InfoServiceResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9A92C7842C800336841 /* InfoServiceResponseDTO.swift */; }; + 934FD9AC2C78443600336841 /* InfoServiceHistoryItemDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9AB2C78443600336841 /* InfoServiceHistoryItemDTO.swift */; }; + 934FD9AE2C7846BA00336841 /* InfoServiceHistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9AD2C7846BA00336841 /* InfoServiceHistoryItem.swift */; }; + 934FD9B02C78481500336841 /* InfoServiceApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9AF2C78481500336841 /* InfoServiceApiError.swift */; }; + 934FD9B22C7849C800336841 /* InfoServiceApiResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9B12C7849C800336841 /* InfoServiceApiResult.swift */; }; + 934FD9B42C78514E00336841 /* InfoServiceApiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9B32C78514E00336841 /* InfoServiceApiCore.swift */; }; + 934FD9B62C78519600336841 /* InfoServiceApiCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9B52C78519600336841 /* InfoServiceApiCommands.swift */; }; + 934FD9B82C7854AF00336841 /* InfoServiceMapperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9B72C7854AF00336841 /* InfoServiceMapperProtocol.swift */; }; + 934FD9BA2C78565400336841 /* InfoServiceApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9B92C78565400336841 /* InfoServiceApiService.swift */; }; + 934FD9BC2C78567300336841 /* InfoServiceApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9BB2C78567300336841 /* InfoServiceApiServiceProtocol.swift */; }; 93547BCA29E2262D00B0914B /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93547BC929E2262D00B0914B /* WelcomeViewController.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 */; }; + 937173F52C8049E0009D5191 /* InfoService+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 937173F42C8049E0009D5191 /* InfoService+Constants.swift */; }; 9371E561295CD53100438F2C /* ChatLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9371E560295CD53100438F2C /* ChatLocalization.swift */; }; + 93760BD72C656CF8002507C3 /* DefaultNodesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93760BD62C656CF8002507C3 /* DefaultNodesProvider.swift */; }; + 93760BDF2C65A284002507C3 /* WordList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93760BDE2C65A284002507C3 /* WordList.swift */; }; + 93760BE12C65A2F3002507C3 /* Mnemonic+extended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93760BE02C65A2F3002507C3 /* Mnemonic+extended.swift */; }; + 93760BE22C65A424002507C3 /* english.txt in Resources */ = {isa = PBXBuildFile; fileRef = 93760BDD2C65A1FA002507C3 /* english.txt */; }; 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 */; }; @@ -309,9 +352,8 @@ 93775E462A674FA9009061AC /* Markdown+Adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93775E452A674FA9009061AC /* Markdown+Adamant.swift */; }; 9377FBDF296C2A2F00C9211B /* ChatTransactionContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9377FBDE296C2A2F00C9211B /* ChatTransactionContentView.swift */; }; 9377FBE2296C2ACA00C9211B /* ChatTransactionContentView+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9377FBE1296C2ACA00C9211B /* ChatTransactionContentView+Model.swift */; }; + 937EDFC02C9CF6B300F219BB /* VersionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 937EDFBF2C9CF6B300F219BB /* VersionFooterView.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 */; }; @@ -332,13 +374,9 @@ 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 */; }; @@ -370,13 +408,6 @@ 93E1234C2A6DFF62004DF33B /* NotificationStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1234A2A6DFEF7004DF33B /* NotificationStrings.swift */; }; 93E1234D2A6DFF62004DF33B /* NotificationStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1234A2A6DFEF7004DF33B /* NotificationStrings.swift */; }; 93E1234E2A6DFF62004DF33B /* NotificationStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1234A2A6DFEF7004DF33B /* NotificationStrings.swift */; }; - 93E5D4DB293000BE00439298 /* UnregisteredTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E5D4DA293000BE00439298 /* UnregisteredTransaction.swift */; }; - 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 */; }; - 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 */; }; @@ -392,7 +423,6 @@ A50A41142822FC35006BDFE1 /* BtcTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A41102822FC35006BDFE1 /* BtcTransferViewController.swift */; }; A50AEB04262C815200B37C22 /* EFQRCode in Frameworks */ = {isa = PBXBuildFile; productRef = A50AEB03262C815200B37C22 /* EFQRCode */; }; A50AEB0C262C81E300B37C22 /* QRCodeReader in Frameworks */ = {isa = PBXBuildFile; productRef = A50AEB0B262C81E300B37C22 /* QRCodeReader */; }; - A50AEB14262C837900B37C22 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = A50AEB13262C837900B37C22 /* Alamofire */; }; A5241B70262DEDE1009FA43E /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5241B6F262DEDE1009FA43E /* Clibsodium */; }; A5241B77262DEDEF009FA43E /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5241B76262DEDEF009FA43E /* Clibsodium */; }; A5241B7E262DEDFE009FA43E /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5241B7D262DEDFE009FA43E /* Clibsodium */; }; @@ -406,7 +436,6 @@ A578BDE52623051C00090141 /* DashWalletService+Transactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578BDE42623051C00090141 /* DashWalletService+Transactions.swift */; }; A5AC8DFF262E0B030053A7E2 /* SipHash in Frameworks */ = {isa = PBXBuildFile; productRef = A5AC8DFE262E0B030053A7E2 /* SipHash */; }; A5AC8E00262E0B030053A7E2 /* SipHash in Embed Frameworks */ = {isa = PBXBuildFile; productRef = A5AC8DFE262E0B030053A7E2 /* SipHash */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - A5BBD811262C657300B5C40C /* ByteBackpacker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BBD810262C657300B5C40C /* ByteBackpacker.swift */; }; A5C99E0E262C9E3A00F7B1B7 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = A5C99E0D262C9E3A00F7B1B7 /* Reachability */; }; A5D87BA3262CA01D00DC28F0 /* ProcedureKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5D87BA2262CA01D00DC28F0 /* ProcedureKit */; }; A5DBBABD262C7221004AC028 /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5DBBABC262C7221004AC028 /* Clibsodium */; }; @@ -454,7 +483,6 @@ E90847372196FEA80095825D /* Chatroom+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847292196FEA80095825D /* Chatroom+CoreDataProperties.swift */; }; E90847392196FEF50095825D /* BaseTransaction+TransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847382196FEF50095825D /* BaseTransaction+TransactionDetails.swift */; }; E908473B219707200095825D /* AccountViewController+StayIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = E908473A219707200095825D /* AccountViewController+StayIn.swift */; }; - E908473D219713300095825D /* NotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E908473C219713300095825D /* NotificationsViewController.swift */; }; E90A4943204C5ED6009F6A65 /* EurekaPassphraseRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90A4942204C5ED6009F6A65 /* EurekaPassphraseRow.swift */; }; E90A4945204C6204009F6A65 /* PassphraseCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E90A4944204C5F60009F6A65 /* PassphraseCell.xib */; }; E90A494B204D9EB8009F6A65 /* AdamantAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90A494A204D9EB8009F6A65 /* AdamantAuthentication.swift */; }; @@ -462,15 +490,10 @@ E90EA5C321BA8BF400A2CE25 /* DelegateDetailsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E90EA5C221BA8BF400A2CE25 /* DelegateDetailsViewController.xib */; }; E913C8F21FFFA51D001A83F7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E913C8F11FFFA51D001A83F7 /* AppDelegate.swift */; }; E913C8F91FFFA51D001A83F7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E913C8F81FFFA51D001A83F7 /* Assets.xcassets */; }; - E913C9081FFFA943001A83F7 /* AdamantCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E913C9071FFFA943001A83F7 /* AdamantCore.swift */; }; E9147B5F20500E9300145913 /* MyLittlePinpad+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9147B5E20500E9300145913 /* MyLittlePinpad+adamant.swift */; }; E9147B612050599000145913 /* LoginViewController+QR.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9147B602050599000145913 /* LoginViewController+QR.swift */; }; E9147B6320505C7500145913 /* QRCodeReader+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9147B6220505C7500145913 /* QRCodeReader+adamant.swift */; }; E9147B6F205088DE00145913 /* LoginViewController+Pinpad.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9147B6E205088DE00145913 /* LoginViewController+Pinpad.swift */; }; - E91947AC20001A9A001362F8 /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91947AB20001A9A001362F8 /* ApiService.swift */; }; - 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 /* 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 */; }; @@ -511,7 +534,6 @@ E9484B7F2285C016008E10F0 /* PKGeneratorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9484B7E2285C016008E10F0 /* PKGeneratorViewController.swift */; }; E94883E7203F07CD00F6E1B0 /* PassphraseValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94883E6203F07CD00F6E1B0 /* PassphraseValidation.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 /* ShareQRFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94E7B07205D4CB80042B639 /* ShareQRFactory.swift */; }; E94E7B0C205D5E4A0042B639 /* TransactionsListViewControllerBase.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B0B205D5E4A0042B639 /* TransactionsListViewControllerBase.xib */; }; @@ -527,7 +549,6 @@ E957E132229B10F80019732A /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E957E131229B10F80019732A /* NotificationViewController.swift */; }; E957E135229B10F80019732A /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E957E133229B10F80019732A /* MainInterface.storyboard */; }; E957E139229B10F80019732A /* TransferNotificationContentExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E957E12D229B10F80019732A /* TransferNotificationContentExtension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 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 /* ChatListFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F857E2008C8D60070534A /* ChatListFactory.swift */; }; @@ -537,19 +558,14 @@ E95F85C0200A51BB0070534A /* Account.json in Resources */ = {isa = PBXBuildFile; fileRef = E95F85BF200A51BB0070534A /* Account.json */; }; E95F85C7200A9B070070534A /* ChatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85C5200A9B070070534A /* ChatTableViewCell.swift */; }; E95F85C8200A9B070070534A /* ChatTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E95F85C6200A9B070070534A /* ChatTableViewCell.xib */; }; - E965A53020B594120041A3EA /* AdamantApi+States.swift in Sources */ = {isa = PBXBuildFile; fileRef = E965A52F20B594120041A3EA /* AdamantApi+States.swift */; }; E96BBE3121F70F5E009AA738 /* ReadonlyTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96BBE3021F70F5E009AA738 /* ReadonlyTextView.swift */; }; E96BBE3321F71290009AA738 /* BuyAndSellViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96BBE3221F71290009AA738 /* BuyAndSellViewController.swift */; }; - E96D64B62295BED700CA5587 /* NormalizedTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85682006AB9D0070534A /* NormalizedTransaction.swift */; }; E96D64BE2295C06400CA5587 /* JSAdamantCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9220E0221983155009C9642 /* JSAdamantCore.swift */; }; E96D64BF2295C06400CA5587 /* adamant-core.js in Resources */ = {isa = PBXBuildFile; fileRef = E9220E0121983155009C9642 /* adamant-core.js */; }; E96D64C02295C06400CA5587 /* JSModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F856A200789450070534A /* JSModels.swift */; }; E96D64C12295C06400CA5587 /* JSAdamantCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85762007E8EC0070534A /* JSAdamantCoreTests.swift */; }; E96D64C22295C06400CA5587 /* NativeCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9220E07219879B9009C9642 /* NativeCoreTests.swift */; }; - E96D64C62295C3ED00CA5587 /* Mnemonic+extended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649E9A142111B3C200686B01 /* Mnemonic+extended.swift */; }; E96D64C82295C44400CA5587 /* Data+utilites.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96D64C72295C44400CA5587 /* Data+utilites.swift */; }; - E96D64CA2295C4A800CA5587 /* WordList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96D64C92295C4A800CA5587 /* WordList.swift */; }; - E96D64CE2295C7F500CA5587 /* english.txt in Resources */ = {isa = PBXBuildFile; fileRef = E96D648C229570ED00CA5587 /* english.txt */; }; E96D64CF2295C82B00CA5587 /* Chat.json in Resources */ = {isa = PBXBuildFile; fileRef = E95F85C3200A540B0070534A /* Chat.json */; }; E96D64D02295C82B00CA5587 /* NormalizedTransaction.json in Resources */ = {isa = PBXBuildFile; fileRef = E95F85C1200A53E90070534A /* NormalizedTransaction.json */; }; E96D64D12295C82B00CA5587 /* TransactionChat.json in Resources */ = {isa = PBXBuildFile; fileRef = E95F85BD200A503A0070534A /* TransactionChat.json */; }; @@ -562,7 +578,6 @@ E9722066201F42BB004F2AAD /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9722065201F42BB004F2AAD /* CoreDataStack.swift */; }; E9722068201F42CC004F2AAD /* InMemoryCoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9722067201F42CC004F2AAD /* InMemoryCoreDataStack.swift */; }; E972206B201F44CA004F2AAD /* TransfersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972206A201F44CA004F2AAD /* TransfersProvider.swift */; }; - E9771D9E22997A6F0099AAC7 /* NativeCore+AdamantCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9771D9D22997A6F0099AAC7 /* NativeCore+AdamantCore.swift */; }; E9771DA722997F310099AAC7 /* ServerResponseWithTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9771DA622997F310099AAC7 /* ServerResponseWithTimestamp.swift */; }; E983AE2120E655C500497E1A /* AccountHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983AE2020E655C500497E1A /* AccountHeaderView.swift */; }; E983AE2A20E65F3200497E1A /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983AE2820E65F3200497E1A /* AccountViewController.swift */; }; @@ -602,13 +617,7 @@ E9B4E1A8210F079E007E77FC /* DoubleDetailsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B4E1A7210F079E007E77FC /* DoubleDetailsTableViewCell.swift */; }; E9B4E1AA210F1803007E77FC /* DoubleDetailsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9B4E1A9210F08BE007E77FC /* DoubleDetailsTableViewCell.xib */; }; E9C51ECF200E2D1100385EB7 /* FeeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C51ECE200E2D1100385EB7 /* FeeTests.swift */; }; - E9C51EEF20139DC600385EB7 /* TransactionIdResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C51EEE20139DC600385EB7 /* TransactionIdResponse.swift */; }; E9C51EF12013F18000385EB7 /* NewChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C51EF02013F18000385EB7 /* NewChatViewController.swift */; }; - E9CAE8D22018AA7700345E76 /* AdamantApi+Accounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9CAE8D12018AA7700345E76 /* AdamantApi+Accounts.swift */; }; - E9CAE8D42018AC1800345E76 /* AdamantApi+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9CAE8D32018AC1800345E76 /* AdamantApi+Keys.swift */; }; - E9CAE8D62018AC5300345E76 /* AdamantApi+Transactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9CAE8D52018AC5300345E76 /* AdamantApi+Transactions.swift */; }; - E9CAE8D82018ACA700345E76 /* AdamantApi+Transfers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9CAE8D72018ACA700345E76 /* AdamantApi+Transfers.swift */; }; - E9CAE8DA2018ACD300345E76 /* AdamantApi+Chats.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9CAE8D92018ACD300345E76 /* AdamantApi+Chats.swift */; }; E9D1BE1C211DABE100E86B72 /* WalletPagingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D1BE1B211DABE100E86B72 /* WalletPagingItem.swift */; }; E9DFB71C21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9DFB71B21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift */; }; E9E7CD8B20026B0600DFC4DB /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD8A20026B0600DFC4DB /* AccountService.swift */; }; @@ -691,7 +700,26 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2621AB362C60E74A00046D7A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; + 2621AB382C60E7AE00046D7A /* NotificationsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewModel.swift; sourceTree = ""; }; + 2621AB3A2C613C8100046D7A /* NotificationsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsFactory.swift; sourceTree = ""; }; 265AA1612B74E6B900CF98B0 /* ChatPreservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreservation.swift; sourceTree = ""; }; + 269B830F2C74A2FF002AA1D7 /* note.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = note.mp3; sourceTree = ""; }; + 269B83122C74B4EA002AA1D7 /* handoff.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = handoff.mp3; sourceTree = ""; }; + 269B83132C74B4EA002AA1D7 /* portal.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = portal.mp3; sourceTree = ""; }; + 269B83142C74B4EB002AA1D7 /* antic.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = antic.mp3; sourceTree = ""; }; + 269B83152C74B4EB002AA1D7 /* droplet.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = droplet.mp3; sourceTree = ""; }; + 269B83162C74B4EB002AA1D7 /* passage.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = passage.mp3; sourceTree = ""; }; + 269B83172C74B4EB002AA1D7 /* chord.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = chord.mp3; sourceTree = ""; }; + 269B83182C74B4EB002AA1D7 /* rattle.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = rattle.mp3; sourceTree = ""; }; + 269B83192C74B4EB002AA1D7 /* rebound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = rebound.mp3; sourceTree = ""; }; + 269B831A2C74B4EC002AA1D7 /* milestone.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = milestone.mp3; sourceTree = ""; }; + 269B831B2C74B4EC002AA1D7 /* cheers.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = cheers.mp3; sourceTree = ""; }; + 269B831C2C74B4EC002AA1D7 /* slide.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = slide.mp3; sourceTree = ""; }; + 269B831D2C74B4EC002AA1D7 /* welcome.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = welcome.mp3; sourceTree = ""; }; + 269B83362C74D1F9002AA1D7 /* NotificationSoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundsView.swift; sourceTree = ""; }; + 269B83392C74D4AA002AA1D7 /* NotificationSoundsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundsViewModel.swift; sourceTree = ""; }; + 269B833C2C74E661002AA1D7 /* NotificationSoundsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundsFactory.swift; sourceTree = ""; }; 269E13512B594B2D008D1CA7 /* AccountFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFooterView.swift; sourceTree = ""; }; 26A975FE2B7E843E0095C367 /* SelectTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTextView.swift; sourceTree = ""; }; 26A976002B7E852E0095C367 /* ChatSelectTextViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSelectTextViewFactory.swift; sourceTree = ""; }; @@ -750,19 +778,15 @@ 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 = ""; }; - 3AA388022B67F47600125684 /* RPCResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RPCResponseModel.swift; sourceTree = ""; }; 3AA388042B67F4DD00125684 /* BtcBlockchainInfoDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcBlockchainInfoDTO.swift; sourceTree = ""; }; 3AA388062B67F53F00125684 /* BtcNetworkInfoDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcNetworkInfoDTO.swift; sourceTree = ""; }; 3AA388092B69173500125684 /* DashNetworkInfoDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashNetworkInfoDTO.swift; sourceTree = ""; }; - 3AA3880B2B69201B00125684 /* ADM+JsonDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ADM+JsonDecode.swift"; sourceTree = ""; }; - 3AA3880D2B6A356900125684 /* RpcRequestModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RpcRequestModel.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 = ""; }; 3AA6DF3F2BA9941E00EA2E16 /* MediaContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaContainerView.swift; sourceTree = ""; }; 3AA6DF432BA997C000EA2E16 /* FileListContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListContainerView.swift; sourceTree = ""; }; 3AA6DF452BA9BEB700EA2E16 /* MediaContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaContentView.swift; sourceTree = ""; }; - 3AB87CD52BF6237100AE8743 /* MultipartFormDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartFormDataModel.swift; sourceTree = ""; }; 3ACD307D2BBD86B700ABF671 /* FilesStorageProprietiesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesStorageProprietiesService.swift; sourceTree = ""; }; 3ACD307F2BBD86C800ABF671 /* FilesStorageProprietiesProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesStorageProprietiesProtocol.swift; sourceTree = ""; }; 3AE0A4272BC6A64900BF7125 /* FilesNetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilesNetworkManager.swift; sourceTree = ""; }; @@ -805,7 +829,6 @@ 416380E02A51765F00F90E6D /* ChatReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatReactionsView.swift; sourceTree = ""; }; 4164A9D628F17D4000EEF16D /* ChatTransactionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTransactionService.swift; sourceTree = ""; }; 4164A9D828F17DA700EEF16D /* AdamantChatTransactionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantChatTransactionService.swift; sourceTree = ""; }; - 417BA7F328BF894F00DF94C5 /* NotificationSoundsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundsViewController.swift; sourceTree = ""; }; 4184F16D2A33023A00D7B8B9 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 4184F1722A33102800D7B8B9 /* AdamantCrashlysticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCrashlysticsService.swift; sourceTree = ""; }; 4184F1742A33106200D7B8B9 /* CrashlysticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashlysticsService.swift; sourceTree = ""; }; @@ -841,7 +864,6 @@ 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 = ""; }; - 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 = ""; }; @@ -864,8 +886,6 @@ 6449BA65235CA0930033B936 /* ERC20WalletService+Send.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ERC20WalletService+Send.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 /* 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 = ""; }; @@ -899,7 +919,6 @@ 649D6BEB21BD5A53009E727B /* UISuffixTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UISuffixTextField.swift; sourceTree = ""; }; 649D6BEF21BFF481009E727B /* AdamantChatsProvider+search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantChatsProvider+search.swift"; sourceTree = ""; }; 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 = ""; }; 64B5736C2201E196005DC968 /* BtcTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcTransactionsViewController.swift; sourceTree = ""; }; 64B5736E2209B892005DC968 /* BtcTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcTransactionDetailsViewController.swift; sourceTree = ""; }; @@ -911,8 +930,7 @@ 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 = ""; }; - 64EAB37322463E020018D9B2 /* CurrencyInfoService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyInfoService.swift; sourceTree = ""; }; - 64EAB37522463F680018D9B2 /* AdamantCurrencyInfoService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCurrencyInfoService.swift; sourceTree = ""; }; + 64EAB37322463E020018D9B2 /* InfoServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceProtocol.swift; sourceTree = ""; }; 64F085D820E2D7600006DE68 /* AdmTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmTransactionsViewController.swift; sourceTree = ""; }; 64FA53CC20E1300A006783C9 /* EthTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransactionsViewController.swift; sourceTree = ""; }; 64FA53D020E24941006783C9 /* TransactionDetailsViewControllerBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDetailsViewControllerBase.swift; sourceTree = ""; }; @@ -921,8 +939,12 @@ 9304F8BD292F88F900173F18 /* ANSPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ANSPayload.swift; sourceTree = ""; }; 9304F8C1292F895C00173F18 /* PushNotificationsTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsTokenService.swift; sourceTree = ""; }; 9304F8C3292F8A3100173F18 /* AdamantPushNotificationsTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantPushNotificationsTokenService.swift; sourceTree = ""; }; - 9304F8C5292F971600173F18 /* ApiServiceResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceResult.swift; sourceTree = ""; }; - 9304F8C7292F972600173F18 /* ApiServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceError.swift; sourceTree = ""; }; + 931224A82C7AA0E4009E0ED0 /* InfoServiceApiService+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InfoServiceApiService+Extension.swift"; sourceTree = ""; }; + 931224AA2C7AA212009E0ED0 /* InfoServiceRatesRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceRatesRequestDTO.swift; sourceTree = ""; }; + 931224AC2C7AA67B009E0ED0 /* InfoServiceHistoryRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceHistoryRequestDTO.swift; sourceTree = ""; }; + 931224AE2C7AA88E009E0ED0 /* InfoService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoService.swift; sourceTree = ""; }; + 931224B02C7ACFE6009E0ED0 /* InfoServiceAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceAssembly.swift; sourceTree = ""; }; + 931224B22C7AD5DD009E0ED0 /* InfoServiceTicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceTicker.swift; sourceTree = ""; }; 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 = ""; }; @@ -941,13 +963,9 @@ 93294B992AAD624100911109 /* WalletFactoryCompose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletFactoryCompose.swift; sourceTree = ""; }; 932B34E82974AA4A002A75BA /* ChatPreservationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreservationProtocol.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 = ""; }; + 9332C39C2C76BE7500164B80 /* FileApiServiceResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileApiServiceResult.swift; sourceTree = ""; }; + 9332C3A22C76C45A00164B80 /* ApiServiceComposeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceComposeProtocol.swift; sourceTree = ""; }; + 9332C3A42C76C4EC00164B80 /* ApiServiceCompose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceCompose.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 = ""; }; @@ -963,31 +981,45 @@ 93496BAA2A6CAED100DD062F /* Roboto_300_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Roboto_300_normal.ttf; sourceTree = ""; }; 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 = ""; }; + 934FD9A32C783D2E00336841 /* InfoServiceStatusDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceStatusDTO.swift; sourceTree = ""; }; + 934FD9A52C783DB700336841 /* InfoServiceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceStatus.swift; sourceTree = ""; }; + 934FD9A72C783E0C00336841 /* InfoServiceMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceMapper.swift; sourceTree = ""; }; + 934FD9A92C7842C800336841 /* InfoServiceResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceResponseDTO.swift; sourceTree = ""; }; + 934FD9AB2C78443600336841 /* InfoServiceHistoryItemDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceHistoryItemDTO.swift; sourceTree = ""; }; + 934FD9AD2C7846BA00336841 /* InfoServiceHistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceHistoryItem.swift; sourceTree = ""; }; + 934FD9AF2C78481500336841 /* InfoServiceApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiError.swift; sourceTree = ""; }; + 934FD9B12C7849C800336841 /* InfoServiceApiResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiResult.swift; sourceTree = ""; }; + 934FD9B32C78514E00336841 /* InfoServiceApiCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiCore.swift; sourceTree = ""; }; + 934FD9B52C78519600336841 /* InfoServiceApiCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiCommands.swift; sourceTree = ""; }; + 934FD9B72C7854AF00336841 /* InfoServiceMapperProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceMapperProtocol.swift; sourceTree = ""; }; + 934FD9B92C78565400336841 /* InfoServiceApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiService.swift; sourceTree = ""; }; + 934FD9BB2C78567300336841 /* InfoServiceApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiServiceProtocol.swift; sourceTree = ""; }; 93547BC929E2262D00B0914B /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.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 = ""; }; + 937173F42C8049E0009D5191 /* InfoService+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InfoService+Constants.swift"; sourceTree = ""; }; 9371E560295CD53100438F2C /* ChatLocalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLocalization.swift; sourceTree = ""; }; + 93760BD62C656CF8002507C3 /* DefaultNodesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultNodesProvider.swift; sourceTree = ""; }; + 93760BDD2C65A1FA002507C3 /* english.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = english.txt; sourceTree = ""; }; + 93760BDE2C65A284002507C3 /* WordList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordList.swift; sourceTree = ""; }; + 93760BE02C65A2F3002507C3 /* Mnemonic+extended.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Mnemonic+extended.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 = ""; }; + 937EDFBF2C9CF6B300F219BB /* VersionFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionFooterView.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 = ""; }; @@ -1007,13 +1039,9 @@ 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 = ""; }; @@ -1042,11 +1070,6 @@ 93E123422A6DFE27004DF33B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = ""; }; 93E123432A6DFE2E004DF33B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; 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 = ""; }; - 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 = ""; }; 93FC169A2B0197FD0062B507 /* BtcApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcApiService.swift; sourceTree = ""; }; @@ -1060,7 +1083,6 @@ A50A410F2822FC35006BDFE1 /* BtcWalletService+Send.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BtcWalletService+Send.swift"; sourceTree = ""; }; A50A41102822FC35006BDFE1 /* BtcTransferViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcTransferViewController.swift; sourceTree = ""; }; A578BDE42623051C00090141 /* DashWalletService+Transactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DashWalletService+Transactions.swift"; sourceTree = ""; }; - A5BBD810262C657300B5C40C /* ByteBackpacker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ByteBackpacker.swift; sourceTree = ""; }; A5E04226282A8BDC0076CD13 /* BtcBalanceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcBalanceResponse.swift; sourceTree = ""; }; A5E04228282A998C0076CD13 /* BtcTransactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcTransactionResponse.swift; sourceTree = ""; }; A5E0422A282AB18B0076CD13 /* BtcUnspentTransactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcUnspentTransactionResponse.swift; sourceTree = ""; }; @@ -1096,7 +1118,6 @@ E90847292196FEA80095825D /* Chatroom+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Chatroom+CoreDataProperties.swift"; sourceTree = ""; }; E90847382196FEF50095825D /* BaseTransaction+TransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseTransaction+TransactionDetails.swift"; sourceTree = ""; }; E908473A219707200095825D /* AccountViewController+StayIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountViewController+StayIn.swift"; sourceTree = ""; }; - E908473C219713300095825D /* NotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = ""; }; E90A4942204C5ED6009F6A65 /* EurekaPassphraseRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EurekaPassphraseRow.swift; sourceTree = ""; }; E90A4944204C5F60009F6A65 /* PassphraseCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PassphraseCell.xib; sourceTree = ""; }; E90A494A204D9EB8009F6A65 /* AdamantAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantAuthentication.swift; sourceTree = ""; }; @@ -1106,15 +1127,10 @@ E913C8F11FFFA51D001A83F7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; E913C8F81FFFA51D001A83F7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; E913C8FD1FFFA51E001A83F7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E913C9071FFFA943001A83F7 /* AdamantCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCore.swift; sourceTree = ""; }; E9147B5E20500E9300145913 /* MyLittlePinpad+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MyLittlePinpad+adamant.swift"; sourceTree = ""; }; E9147B602050599000145913 /* LoginViewController+QR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoginViewController+QR.swift"; sourceTree = ""; }; E9147B6220505C7500145913 /* QRCodeReader+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QRCodeReader+adamant.swift"; sourceTree = ""; }; E9147B6E205088DE00145913 /* LoginViewController+Pinpad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoginViewController+Pinpad.swift"; sourceTree = ""; }; - E91947AB20001A9A001362F8 /* ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = ""; }; - 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 /* 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 = ""; }; @@ -1158,7 +1174,6 @@ 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 /* 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 /* ShareQRFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareQRFactory.swift; sourceTree = ""; }; E94E7B0B205D5E4A0042B639 /* TransactionsListViewControllerBase.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransactionsListViewControllerBase.xib; sourceTree = ""; }; @@ -1176,9 +1191,7 @@ E957E134229B10F80019732A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; E957E136229B10F80019732A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E957E13D229B118E0019732A /* Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - E95F85682006AB9D0070534A /* NormalizedTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NormalizedTransaction.swift; sourceTree = ""; }; E95F856A200789450070534A /* JSModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSModels.swift; sourceTree = ""; }; - E95F856E2007B61D0070534A /* GetPublicKeyResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPublicKeyResponse.swift; sourceTree = ""; }; 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 = ""; }; @@ -1193,12 +1206,9 @@ E95F85C3200A540B0070534A /* Chat.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Chat.json; sourceTree = ""; }; E95F85C5200A9B070070534A /* ChatTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableViewCell.swift; sourceTree = ""; }; E95F85C6200A9B070070534A /* ChatTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ChatTableViewCell.xib; sourceTree = ""; }; - E965A52F20B594120041A3EA /* AdamantApi+States.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+States.swift"; sourceTree = ""; }; E96BBE3021F70F5E009AA738 /* ReadonlyTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadonlyTextView.swift; sourceTree = ""; }; E96BBE3221F71290009AA738 /* BuyAndSellViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyAndSellViewController.swift; sourceTree = ""; }; - E96D648C229570ED00CA5587 /* english.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = english.txt; sourceTree = ""; }; E96D64C72295C44400CA5587 /* Data+utilites.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+utilites.swift"; sourceTree = ""; }; - E96D64C92295C4A800CA5587 /* WordList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordList.swift; sourceTree = ""; }; E96D64DB2295CD4700CA5587 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; E96D64DD2295CD4700CA5587 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; E96D64DF2295CD4700CA5587 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1209,7 +1219,6 @@ E9722067201F42CC004F2AAD /* InMemoryCoreDataStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryCoreDataStack.swift; sourceTree = ""; }; E972206A201F44CA004F2AAD /* TransfersProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransfersProvider.swift; sourceTree = ""; }; E9771D7D22995C870099AAC7 /* Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Debug.entitlements; sourceTree = ""; }; - E9771D9D22997A6F0099AAC7 /* NativeCore+AdamantCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NativeCore+AdamantCore.swift"; sourceTree = ""; }; E9771DA622997F310099AAC7 /* ServerResponseWithTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerResponseWithTimestamp.swift; sourceTree = ""; }; E983AE2020E655C500497E1A /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = ""; }; E983AE2820E65F3200497E1A /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = ""; }; @@ -1253,13 +1262,7 @@ E9B994C122BFD723004CD645 /* Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; E9B994C222BFD73F004CD645 /* Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; E9C51ECE200E2D1100385EB7 /* FeeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeTests.swift; sourceTree = ""; }; - E9C51EEE20139DC600385EB7 /* TransactionIdResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionIdResponse.swift; sourceTree = ""; }; E9C51EF02013F18000385EB7 /* NewChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatViewController.swift; sourceTree = ""; }; - E9CAE8D12018AA7700345E76 /* AdamantApi+Accounts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Accounts.swift"; sourceTree = ""; }; - E9CAE8D32018AC1800345E76 /* AdamantApi+Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Keys.swift"; sourceTree = ""; }; - E9CAE8D52018AC5300345E76 /* AdamantApi+Transactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Transactions.swift"; sourceTree = ""; }; - E9CAE8D72018ACA700345E76 /* AdamantApi+Transfers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Transfers.swift"; sourceTree = ""; }; - E9CAE8D92018ACD300345E76 /* AdamantApi+Chats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Chats.swift"; sourceTree = ""; }; E9D1BE1B211DABE100E86B72 /* WalletPagingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletPagingItem.swift; sourceTree = ""; }; E9DFB71B21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmTransactionDetailsViewController.swift; sourceTree = ""; }; E9E7CD8A20026B0600DFC4DB /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = ""; }; @@ -1320,7 +1323,6 @@ 9342F6C22A6A35E300A9B39F /* CommonKit in Frameworks */, A5F0A04B262C9CA90009672A /* Swinject in Frameworks */, A5DBBABD262C7221004AC028 /* Clibsodium in Frameworks */, - A50AEB14262C837900B37C22 /* Alamofire in Frameworks */, 938F7D582955C1DA001915CA /* MessageKit in Frameworks */, A530B0D82842110D003F0210 /* (null) in Frameworks */, A5DBBADC262C729B004AC028 /* CryptoSwift in Frameworks */, @@ -1384,6 +1386,27 @@ path = Pods; sourceTree = ""; }; + 2621AB352C60E52900046D7A /* Notifications */ = { + isa = PBXGroup; + children = ( + 26F7EF3E2C9118E700E16A94 /* NotificationSounds */, + 2621AB362C60E74A00046D7A /* NotificationsView.swift */, + 2621AB382C60E7AE00046D7A /* NotificationsViewModel.swift */, + 2621AB3A2C613C8100046D7A /* NotificationsFactory.swift */, + ); + path = Notifications; + sourceTree = ""; + }; + 26F7EF3E2C9118E700E16A94 /* NotificationSounds */ = { + isa = PBXGroup; + children = ( + 269B83362C74D1F9002AA1D7 /* NotificationSoundsView.swift */, + 269B83392C74D4AA002AA1D7 /* NotificationSoundsViewModel.swift */, + 269B833C2C74E661002AA1D7 /* NotificationSoundsFactory.swift */, + ); + path = NotificationSounds; + sourceTree = ""; + }; 3A20D9392AE7F305005475A6 /* Models */ = { isa = PBXGroup; children = ( @@ -1549,6 +1572,7 @@ children = ( 3AE0A4302BC6A9C900BF7125 /* IPFSDTO.swift */, 3AE0A4342BC6AA1B00BF7125 /* FileManagerError.swift */, + 9332C39C2C76BE7500164B80 /* FileApiServiceResult.swift */, ); path = Models; sourceTree = ""; @@ -1707,6 +1731,15 @@ path = Doge; sourceTree = ""; }; + 931224A72C7A9F9C009E0ED0 /* ApiService */ = { + isa = PBXGroup; + children = ( + 934FD9B92C78565400336841 /* InfoServiceApiService.swift */, + 931224A82C7AA0E4009E0ED0 /* InfoServiceApiService+Extension.swift */, + ); + path = ApiService; + sourceTree = ""; + }; 9322E87C2970435C00B8357C /* Models */ = { isa = PBXGroup; children = ( @@ -1802,6 +1835,65 @@ path = Fonts; sourceTree = ""; }; + 934FD99E2C783C9500336841 /* InfoService */ = { + isa = PBXGroup; + children = ( + 934FD99F2C783C9E00336841 /* Models */, + 934FD9A12C783CAC00336841 /* Protocols */, + 934FD9A02C783CA700336841 /* Services */, + 931224B02C7ACFE6009E0ED0 /* InfoServiceAssembly.swift */, + 937173F42C8049E0009D5191 /* InfoService+Constants.swift */, + ); + path = InfoService; + sourceTree = ""; + }; + 934FD99F2C783C9E00336841 /* Models */ = { + isa = PBXGroup; + children = ( + 934FD9A22C783D1E00336841 /* DTO */, + 934FD9A52C783DB700336841 /* InfoServiceStatus.swift */, + 934FD9AD2C7846BA00336841 /* InfoServiceHistoryItem.swift */, + 934FD9AF2C78481500336841 /* InfoServiceApiError.swift */, + 934FD9B12C7849C800336841 /* InfoServiceApiResult.swift */, + 934FD9B52C78519600336841 /* InfoServiceApiCommands.swift */, + 931224B22C7AD5DD009E0ED0 /* InfoServiceTicker.swift */, + ); + path = Models; + sourceTree = ""; + }; + 934FD9A02C783CA700336841 /* Services */ = { + isa = PBXGroup; + children = ( + 931224A72C7A9F9C009E0ED0 /* ApiService */, + 934FD9A72C783E0C00336841 /* InfoServiceMapper.swift */, + 934FD9B32C78514E00336841 /* InfoServiceApiCore.swift */, + 931224AE2C7AA88E009E0ED0 /* InfoService.swift */, + ); + path = Services; + sourceTree = ""; + }; + 934FD9A12C783CAC00336841 /* Protocols */ = { + isa = PBXGroup; + children = ( + 64EAB37322463E020018D9B2 /* InfoServiceProtocol.swift */, + 934FD9B72C7854AF00336841 /* InfoServiceMapperProtocol.swift */, + 934FD9BB2C78567300336841 /* InfoServiceApiServiceProtocol.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + 934FD9A22C783D1E00336841 /* DTO */ = { + isa = PBXGroup; + children = ( + 934FD9A32C783D2E00336841 /* InfoServiceStatusDTO.swift */, + 934FD9A92C7842C800336841 /* InfoServiceResponseDTO.swift */, + 934FD9AB2C78443600336841 /* InfoServiceHistoryItemDTO.swift */, + 931224AA2C7AA212009E0ED0 /* InfoServiceRatesRequestDTO.swift */, + 931224AC2C7AA67B009E0ED0 /* InfoServiceHistoryRequestDTO.swift */, + ); + path = DTO; + sourceTree = ""; + }; 935F53D429BE8F4800779492 /* RichTransactionStatusService */ = { isa = PBXGroup; children = ( @@ -1829,7 +1921,6 @@ 9366588C2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift */, 9366588E2B0AB97500BDB2D3 /* CoinsNodesListMapper.swift */, 936658962B0ACB1500BDB2D3 /* CoinsNodesListViewModel.swift */, - 936658982B0AD32600BDB2D3 /* CoinsNodesListViewModel+ApiServices.swift */, ); path = ViewModel; sourceTree = ""; @@ -1843,6 +1934,14 @@ path = View; sourceTree = ""; }; + 93760BDC2C65A1FA002507C3 /* Mnemonic */ = { + isa = PBXGroup; + children = ( + 93760BDD2C65A1FA002507C3 /* english.txt */, + ); + path = Mnemonic; + sourceTree = ""; + }; 937736832B0949C700B35C7A /* NodeCell */ = { isa = PBXGroup; children = ( @@ -2026,15 +2125,6 @@ path = Localization; sourceTree = ""; }; - 93E5D4DE2930027F00439298 /* AdamantCore */ = { - isa = PBXGroup; - children = ( - E913C9071FFFA943001A83F7 /* AdamantCore.swift */, - 93E5D4DF2930029300439298 /* AdamantCore+Extensions.swift */, - ); - path = AdamantCore; - sourceTree = ""; - }; A50A41022822F8CE006BDFE1 /* Bitcoin */ = { isa = PBXGroup; children = ( @@ -2143,18 +2233,14 @@ isa = PBXGroup; children = ( 932BD15929D2F74500AA1947 /* RichMessageProviderWithStatusCheck */, - 93E5D4DE2930027F00439298 /* AdamantCore */, E9B3D398201F90320019EB36 /* DataProviders */, E9E7CD8A20026B0600DFC4DB /* AccountService.swift */, 6455E9F021075D3600B2E94C /* AddressBookService.swift */, - E91947AB20001A9A001362F8 /* ApiService.swift */, - 9338AE832AEF5EFA001D32DF /* APICoreProtocol.swift */, 3AA2D5F6280EADE3000ED971 /* SocketService.swift */, 4164A9D628F17D4000EEF16D /* ChatTransactionService.swift */, 648BCA6C213D384F00875EB5 /* AvatarService.swift */, E9A174B22057EC47003667CD /* BackgroundFetchService.swift */, E9E7CDBD2003AEFB00DFC4DB /* CellFactory.swift */, - 64EAB37322463E020018D9B2 /* CurrencyInfoService.swift */, E9E7CD8C20026B6600DFC4DB /* DialogService.swift */, E90A494C204DA932009F6A65 /* LocalAuthentication.swift */, E93D7ABD2052CEE1005D19DC /* NotificationsService.swift */, @@ -2168,8 +2254,6 @@ 3A7BD00D2AA9BCE80045AAB0 /* VibroService.swift */, 41C1698B29E7F34900FEB3CB /* RichTransactionReplyService.swift */, 3A4193902A580C85006A6B22 /* RichTransactionReactService.swift */, - 9338AE7E2AEF43DA001D32DF /* NodesStorageProtocol.swift */, - 93ADC17C2B083C3B00F2DF77 /* NodesAdditionalParamsStorageProtocol.swift */, 3A2F55FB2AC6F885000A3F26 /* CoinStorage.swift */, 3A96E37B2AED27F8001F5A52 /* PartnerQRService.swift */, 3AF08D602B4EB3C400EB82B1 /* LanguageStorageProtocol.swift */, @@ -2178,6 +2262,7 @@ 3AE0A4322BC6A9EB00BF7125 /* FileApiServiceProtocol.swift */, 3AE0A4362BC6AA6000BF7125 /* FilesNetworkManagerProtocol.swift */, 3AF9DF0A2BFE306C009A43A8 /* ChatFileProtocol.swift */, + 9332C3A22C76C45A00164B80 /* ApiServiceComposeProtocol.swift */, ); path = ServiceProtocols; sourceTree = ""; @@ -2190,13 +2275,11 @@ 41C1698A29E7F2EE00FEB3CB /* RichTransactionReplyService */, 935F53D429BE8F4800779492 /* RichTransactionStatusService */, 3AA2D5F8280EAF49000ED971 /* SocketService */, - E9CAE8D02018AA5000345E76 /* ApiService */, E9B3D39F201FA2090019EB36 /* DataProviders */, E9E7CD922002740500DFC4DB /* AdamantAccountService.swift */, 6455E9F221075D8000B2E94C /* AdamantAddressBookService.swift */, E90A494A204D9EB8009F6A65 /* AdamantAuthentication.swift */, E9E7CDBF2003AF6D00DFC4DB /* AdamantCellFactory.swift */, - 64EAB37522463F680018D9B2 /* AdamantCurrencyInfoService.swift */, E9E7CD8E20026CD300DFC4DB /* AdamantDialogService.swift */, E93D7ABF2052CF63005D19DC /* AdamantNotificationService.swift */, 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */, @@ -2206,17 +2289,12 @@ 3A7BD00F2AA9BD030045AAB0 /* AdamantVibroService.swift */, E921597420611A6A0000CA5C /* AdamantReachability.swift */, E950273F202E257E002C1098 /* RepeaterService.swift */, - E9771D9D22997A6F0099AAC7 /* NativeCore+AdamantCore.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 */, 3AF08D5E2B4EB3A200EB82B1 /* LanguageService.swift */, 3ACD307D2BBD86B700ABF671 /* FilesStorageProprietiesService.swift */, + 9332C3A42C76C4EC00164B80 /* ApiServiceCompose.swift */, ); path = Services; sourceTree = ""; @@ -2226,9 +2304,7 @@ children = ( E91947B72000326B001362F8 /* ServerResponses */, E95F859220094B8E0070534A /* CoreData */, - E91947B320002809001362F8 /* AdamantAccount.swift */, E9393FA92055D03300EE6F30 /* AdamantMessage.swift */, - 644EC34E20EFA77A00F40C73 /* Delegate.swift */, 648CE39F22999C890070A2CC /* BaseBtcTransaction.swift */, 648CE3A122999CE70070A2CC /* BTCRawTransaction.swift */, 648DD7A12237D9A000B811FD /* DogeTransaction.swift */, @@ -2237,29 +2313,14 @@ E940086A2114A70600CD2D67 /* LskAccount.swift */, E9204B5120C9762300F3B9AB /* MessageStatus.swift */, E9A03FD320DBC824007653A1 /* NodeVersion.swift */, - E95F85682006AB9D0070534A /* NormalizedTransaction.swift */, - 93E5D4DA293000BE00439298 /* UnregisteredTransaction.swift */, - 5558A437282AB9390024DDD6 /* NodeStatus.swift */, 3AF8D9E82C73ADFA007A7CBC /* IPFSNodeStatus.swift */, E9FCA1E5218334C00005E83D /* SimpleTransactionDetails.swift */, E971591921681D6900A5F904 /* TransactionStatus.swift */, 648C696E22915A12006645F5 /* DashTransaction.swift */, 9304F8BD292F88F900173F18 /* ANSPayload.swift */, - 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 */, - 3AA388022B67F47600125684 /* RPCResponseModel.swift */, - 3AA3880D2B6A356900125684 /* RpcRequestModel.swift */, - 936658902B0AB9DC00BDB2D3 /* NodeWithGroup.swift */, - 3AB87CD52BF6237100AE8743 /* MultipartFormDataModel.swift */, 3A53BD452C6B7AF100BB1EE6 /* DownloadPolicy.swift */, ); path = Models; @@ -2269,7 +2330,6 @@ isa = PBXGroup; children = ( 93775E452A674FA9009061AC /* Markdown+Adamant.swift */, - E91947B12000246A001362F8 /* AdamantError.swift */, E9061B96207501E40011F104 /* AdamantUserInfoKey.swift */, E94008862114F05B00CD2D67 /* AddressValidationResult.swift */, E940088E2119A9E800CD2D67 /* BigInt+Decimal.swift */, @@ -2292,12 +2352,10 @@ 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 */, 3AF53F8C2B3DCFA300B30312 /* NodeGroup+Constants.swift */, - 3AA3880B2B69201B00125684 /* ADM+JsonDecode.swift */, 3A5DF1782C4698EC0005369D /* EdgeInsetLabel.swift */, ); path = Helpers; @@ -2306,14 +2364,27 @@ E913C9111FFFAB05001A83F7 /* Assets */ = { isa = PBXGroup; children = ( + 93760BDC2C65A1FA002507C3 /* Mnemonic */, 93496BA12A6CAED100DD062F /* Fonts */, - E96D64CD2295C54600CA5587 /* Mnemonic */, E913C8F81FFFA51D001A83F7 /* Assets.xcassets */, E9A174B820587B83003667CD /* notification.mp3 */, 4198D57A28C8B7DA009337F2 /* so-proud-notification.mp3 */, 4198D57C28C8B7F9009337F2 /* relax-message-tone.mp3 */, 4198D57E28C8B834009337F2 /* short-success.mp3 */, 4198D58028C8B8D1009337F2 /* default.mp3 */, + 269B830F2C74A2FF002AA1D7 /* note.mp3 */, + 269B83142C74B4EB002AA1D7 /* antic.mp3 */, + 269B831B2C74B4EC002AA1D7 /* cheers.mp3 */, + 269B83172C74B4EB002AA1D7 /* chord.mp3 */, + 269B83152C74B4EB002AA1D7 /* droplet.mp3 */, + 269B83122C74B4EA002AA1D7 /* handoff.mp3 */, + 269B831A2C74B4EC002AA1D7 /* milestone.mp3 */, + 269B83162C74B4EB002AA1D7 /* passage.mp3 */, + 269B83132C74B4EA002AA1D7 /* portal.mp3 */, + 269B83182C74B4EB002AA1D7 /* rattle.mp3 */, + 269B83192C74B4EB002AA1D7 /* rebound.mp3 */, + 269B831C2C74B4EC002AA1D7 /* slide.mp3 */, + 269B831D2C74B4EC002AA1D7 /* welcome.mp3 */, E9256F752039A9A200DE86E9 /* LaunchScreen.storyboard */, 4184F16D2A33023A00D7B8B9 /* GoogleService-Info.plist */, ); @@ -2323,6 +2394,7 @@ E919479920000FFD001362F8 /* Modules */ = { isa = PBXGroup; children = ( + 934FD99E2C783C9500336841 /* InfoService */, 3A2478AF2BB45DE2009D89E9 /* StorageUsage */, 9366588B2B0AB68300BDB2D3 /* CoinsNodesList */, 3AA50DED2AEBE61C00C58FC8 /* PartnerQR */, @@ -2361,8 +2433,6 @@ isa = PBXGroup; children = ( E933475A225539390083839E /* DogeGetTransactionsResponse.swift */, - E95F856E2007B61D0070534A /* GetPublicKeyResponse.swift */, - E9C51EEE20139DC600385EB7 /* TransactionIdResponse.swift */, E9771DA622997F310099AAC7 /* ServerResponseWithTimestamp.swift */, 648C697022915CB8006645F5 /* BTCRPCServerResponce.swift */, ); @@ -2417,7 +2487,6 @@ 6403F5DC22723C2800D58779 /* Dash */, 6449BA5D235CA0930033B936 /* ERC20 */, E94008712114EACF00CD2D67 /* WalletAccount.swift */, - 9366589A2B0AD3E600BDB2D3 /* WalletApiService.swift */, E99818932120892F0018C84C /* WalletViewControllerBase.swift */, E9981897212096ED0018C84C /* WalletViewControllerBase.xib */, E9EC342020052ABB00C0E546 /* TransferViewControllerBase.swift */, @@ -2474,13 +2543,12 @@ E950651F20404997008352E5 /* Utilities */ = { isa = PBXGroup; children = ( - A5BBD810262C657300B5C40C /* ByteBackpacker.swift */, + 93760BDE2C65A284002507C3 /* WordList.swift */, E9942B83203CBFCE00C163AF /* AdamantQRTools.swift */, E950652220404C84008352E5 /* AdamantUriTools.swift */, + 93760BE02C65A2F3002507C3 /* Mnemonic+extended.swift */, 41E3C9CB2A0E20F500AF0985 /* AdamantCoinTools.swift */, E9E7CDB62003994E00DFC4DB /* AdamantUtilities+extended.swift */, - 649E9A142111B3C200686B01 /* Mnemonic+extended.swift */, - E96D64C92295C4A800CA5587 /* WordList.swift */, ); path = Utilities; sourceTree = ""; @@ -2582,14 +2650,6 @@ path = Core; sourceTree = ""; }; - E96D64CD2295C54600CA5587 /* Mnemonic */ = { - isa = PBXGroup; - children = ( - E96D648C229570ED00CA5587 /* english.txt */, - ); - path = Mnemonic; - sourceTree = ""; - }; E96D64DC2295CD4700CA5587 /* NotificationServiceExtension */ = { isa = PBXGroup; children = ( @@ -2605,6 +2665,7 @@ E982F69820235AF000566AC7 /* Settings */ = { isa = PBXGroup; children = ( + 2621AB352C60E52900046D7A /* Notifications */, 411742FE2A39B1B1008CD98A /* Contribute */, 4197B9C72952FAA2004CAF64 /* VisibleWallets */, E948E03A20235E2300975D6B /* SettingsFactory.swift */, @@ -2615,8 +2676,6 @@ E9942B7F203C058C00C163AF /* QRGeneratorViewController.swift */, E9484B7E2285C016008E10F0 /* PKGeneratorViewController.swift */, E9484B7C2285BAD8008E10F0 /* PrivateKeyGenerator.swift */, - E908473C219713300095825D /* NotificationsViewController.swift */, - 417BA7F328BF894F00DF94C5 /* NotificationSoundsViewController.swift */, ); path = Settings; sourceTree = ""; @@ -2645,26 +2704,11 @@ E9A174B42057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift */, 4164A9D828F17DA700EEF16D /* AdamantChatTransactionService.swift */, E9722067201F42CC004F2AAD /* InMemoryCoreDataStack.swift */, + 93760BD62C656CF8002507C3 /* DefaultNodesProvider.swift */, ); path = DataProviders; sourceTree = ""; }; - E9CAE8D02018AA5000345E76 /* ApiService */ = { - isa = PBXGroup; - children = ( - 93E8EDCC2AF1BD65003E163C /* AdamantApiCore.swift */, - E91947AF20002393001362F8 /* AdamantApiService.swift */, - E9CAE8D12018AA7700345E76 /* AdamantApi+Accounts.swift */, - E9CAE8D92018ACD300345E76 /* AdamantApi+Chats.swift */, - E9CAE8D32018AC1800345E76 /* AdamantApi+Keys.swift */, - E9CAE8D52018AC5300345E76 /* AdamantApi+Transactions.swift */, - E9CAE8D72018ACA700345E76 /* AdamantApi+Transfers.swift */, - E965A52F20B594120041A3EA /* AdamantApi+States.swift */, - 644EC34C20EFA60900F40C73 /* AdamantApi+Delegates.swift */, - ); - path = ApiService; - sourceTree = ""; - }; E9E7CDA52002AE1C00DFC4DB /* Account */ = { isa = PBXGroup; children = ( @@ -2690,7 +2734,6 @@ 93496B9F2A6CAE9300DD062F /* LogoFullHeader.xib */, E9942B86203D9E5100C163AF /* EurekaQRRow.swift */, E9942B88203D9ECA00C163AF /* QrCell.xib */, - E948E0472024F02700975D6B /* VersionFooter.xib */, E921534C20EE1E8700C0843F /* EurekaAlertLabelRow.swift */, E921534D20EE1E8700C0843F /* AlertLabelCell.xib */, E926E031213EC43B005E536B /* FullscreenAlertView.swift */, @@ -2699,6 +2742,7 @@ E9B4E1A9210F08BE007E77FC /* DoubleDetailsTableViewCell.xib */, 649D6BEB21BD5A53009E727B /* UISuffixTextField.swift */, 55E69E162868D7920025D82E /* CheckmarkView.swift */, + 937EDFBF2C9CF6B300F219BB /* VersionFooterView.swift */, 557AC307287B1365004699D7 /* CheckmarkRowView.swift */, 551F66E528959A5200DE5D69 /* LoadingView.swift */, 93F3914F2962F5D400BFD6AE /* SpinnerView.swift */, @@ -2763,14 +2807,14 @@ buildConfigurationList = E913C9001FFFA51E001A83F7 /* Build configuration list for PBXNativeTarget "Adamant" */; buildPhases = ( 47866E9AB7D201F2CED0064C /* [CP] Check Pods Manifest.lock */, - 418BBB14293752F800CAB719 /* Run Script - Load wallets */, + 9372E0412C9BC178006DF0B3 /* Run Script - Git Data */, E913C8EA1FFFA51D001A83F7 /* Sources */, E913C8EB1FFFA51D001A83F7 /* Frameworks */, E913C8EC1FFFA51D001A83F7 /* Resources */, 629616F00016639A2AFC5FC7 /* [CP] Embed Pods Frameworks */, E96D64E62295CD4700CA5587 /* Embed App Extensions */, A5AC8E01262E0B030053A7E2 /* Embed Frameworks */, - 41079EBC28AE974300C32DAF /* ShellScript */, + 41079EBC28AE974300C32DAF /* Run Script - SwiftLint */, ); buildRules = ( ); @@ -2788,7 +2832,6 @@ A5DBBAEF262C72EF004AC028 /* LiskKit */, A50AEB03262C815200B37C22 /* EFQRCode */, A50AEB0B262C81E300B37C22 /* QRCodeReader */, - A50AEB13262C837900B37C22 /* Alamofire */, A5F92993262C855B00C3E60A /* MarkdownKit */, A57282C9262C94CD00C96FA8 /* DateToolsSwift */, A544F0D3262C9878001F1A6D /* Eureka */, @@ -2957,7 +3000,6 @@ A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */, A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode" */, A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */, - A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire" */, A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */, A57282C8262C94CD00C96FA8 /* XCRemoteSwiftPackageReference "DateTools" */, A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka" */, @@ -3007,38 +3049,50 @@ E983AE2D20E6720D00497E1A /* AccountFooter.xib in Resources */, E95F85C8200A9B070070534A /* ChatTableViewCell.xib in Resources */, E913C8F91FFFA51D001A83F7 /* Assets.xcassets in Resources */, + 269B83282C74B4EC002AA1D7 /* chord.mp3 in Resources */, 93496BB42A6CAED100DD062F /* Roboto_400_italic.ttf in Resources */, E9942B89203D9ECA00C163AF /* QrCell.xib in Resources */, 93496BB22A6CAED100DD062F /* Exo+2_400_italic.ttf in Resources */, 93496BAD2A6CAED100DD062F /* Roboto_700_normal.ttf in Resources */, E921597D2065031D0000CA5C /* ButtonsStripe.xib in Resources */, + 269B83342C74B4EC002AA1D7 /* welcome.mp3 in Resources */, 93496BAE2A6CAED100DD062F /* Exo+2_700_normal.ttf in Resources */, 93E1232F2A6DF8EF004DF33B /* InfoPlist.strings in Resources */, + 269B83262C74B4EC002AA1D7 /* passage.mp3 in Resources */, + 269B832A2C74B4EC002AA1D7 /* rattle.mp3 in Resources */, E90A4945204C6204009F6A65 /* PassphraseCell.xib in Resources */, E941CCDF20E7B70200C96220 /* WalletCollectionViewCell.xib in Resources */, 93496BB32A6CAED100DD062F /* Exo+2_500_normal.ttf in Resources */, E926E034213EC454005E536B /* FullscreenAlertView.xib in Resources */, E90EA5C321BA8BF400A2CE25 /* DelegateDetailsViewController.xib in Resources */, + 269B83242C74B4EC002AA1D7 /* droplet.mp3 in Resources */, E94E7B01205D3F090042B639 /* ChatListViewController.xib in Resources */, E941CCDB20E786D800C96220 /* AccountHeader.xib in Resources */, + 269B83322C74B4EC002AA1D7 /* slide.mp3 in Resources */, 93496BB72A6CAED100DD062F /* Roboto_500_normal.ttf in Resources */, + 269B83102C74A2FF002AA1D7 /* note.mp3 in Resources */, 4198D58128C8B8D1009337F2 /* default.mp3 in Resources */, 93E123382A6DFD15004DF33B /* Localizable.strings in Resources */, 93496BB52A6CAED100DD062F /* Roboto_300_normal.ttf in Resources */, + 269B83202C74B4EC002AA1D7 /* portal.mp3 in Resources */, 4184F16E2A33023A00D7B8B9 /* GoogleService-Info.plist in Resources */, 93496BB62A6CAED100DD062F /* Roboto_400_normal.ttf in Resources */, E94E7B0C205D5E4A0042B639 /* TransactionsListViewControllerBase.xib in Resources */, E9484B7A227CA93B008E10F0 /* BalanceTableViewCell.xib in Resources */, 6406D74A21C7F06000196713 /* SearchResultsViewController.xib in Resources */, - E96D64CE2295C7F500CA5587 /* english.txt in Resources */, + 269B832E2C74B4EC002AA1D7 /* milestone.mp3 in Resources */, E9B4E1AA210F1803007E77FC /* DoubleDetailsTableViewCell.xib in Resources */, + 269B832C2C74B4EC002AA1D7 /* rebound.mp3 in Resources */, 6458548C211B3AB1004C5909 /* WelcomeViewController.xib in Resources */, 93496BAF2A6CAED100DD062F /* Exo+2_100_normal.ttf in Resources */, + 269B83302C74B4EC002AA1D7 /* cheers.mp3 in Resources */, + 269B831E2C74B4EC002AA1D7 /* handoff.mp3 in Resources */, 645938952378395E00A2BE7C /* EulaViewController.xib in Resources */, + 269B83222C74B4EC002AA1D7 /* antic.mp3 in Resources */, 93496BA02A6CAE9300DD062F /* LogoFullHeader.xib in Resources */, E9A174B920587B84003667CD /* notification.mp3 in Resources */, + 93760BE22C65A424002507C3 /* english.txt in Resources */, 645FEB35213E72C100D6BA2D /* OnboardViewController.xib in Resources */, - E948E0482024F02700975D6B /* VersionFooter.xib in Resources */, E9FAE5E3203ED1AE008D3A6B /* ShareQrViewController.xib in Resources */, 93496BB02A6CAED100DD062F /* Exo+2_300_normal.ttf in Resources */, E921534F20EE1E8700C0843F /* AlertLabelCell.xib in Resources */, @@ -3063,15 +3117,33 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2657A0CD2C707D800021E7E6 /* short-success.mp3 in Resources */, + 269B832D2C74B4EC002AA1D7 /* rebound.mp3 in Resources */, 4154413B2923AED000824478 /* bitcoin_notificationContent.png in Resources */, + 269B83232C74B4EC002AA1D7 /* antic.mp3 in Resources */, + 2657A0CC2C707D7E0021E7E6 /* relax-message-tone.mp3 in Resources */, E957E107229AF7CB0019732A /* adamant_notificationContent.png in Resources */, E957E108229AF7CB0019732A /* doge_notificationContent.png in Resources */, 93E123442A6DFECB004DF33B /* Localizable.strings in Resources */, + 269B83252C74B4EC002AA1D7 /* droplet.mp3 in Resources */, E957E10A229AF7CB0019732A /* ethereum_notificationContent.png in Resources */, + 2657A0CA2C707D780021E7E6 /* notification.mp3 in Resources */, + 269B832F2C74B4EC002AA1D7 /* milestone.mp3 in Resources */, 93E123452A6DFECB004DF33B /* Localizable.stringsdict in Resources */, + 2657A0CB2C707D7B0021E7E6 /* so-proud-notification.mp3 in Resources */, + 269B83332C74B4EC002AA1D7 /* slide.mp3 in Resources */, + 269B83352C74B4EC002AA1D7 /* welcome.mp3 in Resources */, + 269B83212C74B4EC002AA1D7 /* portal.mp3 in Resources */, + 269B83312C74B4EC002AA1D7 /* cheers.mp3 in Resources */, E957E109229AF7CB0019732A /* lisk_notificationContent.png in Resources */, + 269B83112C74A34F002AA1D7 /* note.mp3 in Resources */, + 269B83292C74B4EC002AA1D7 /* chord.mp3 in Resources */, 412C0ED929124A3400DE2C5E /* dash_notificationContent.png in Resources */, + 2657A0CE2C707D830021E7E6 /* default.mp3 in Resources */, + 269B83272C74B4EC002AA1D7 /* passage.mp3 in Resources */, 3A26D9522C3E7F1E003AD832 /* klayr_notificationContent.png in Resources */, + 269B832B2C74B4EC002AA1D7 /* rattle.mp3 in Resources */, + 269B831F2C74B4EC002AA1D7 /* handoff.mp3 in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3091,7 +3163,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 41079EBC28AE974300C32DAF /* ShellScript */ = { + 41079EBC28AE974300C32DAF /* Run Script - SwiftLint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -3100,6 +3172,7 @@ ); inputPaths = ( ); + name = "Run Script - SwiftLint"; outputFileListPaths = ( ); outputPaths = ( @@ -3108,26 +3181,6 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; - 418BBB14293752F800CAB719 /* Run Script - Load wallets */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 8; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$(SRCROOT)/CommonKit/Scripts/UpdateWalletsScript.sh", - "$(SRCROOT)/CommonKit/Scripts/CoinsScript.rb", - ); - name = "Run Script - Load wallets"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 1; - shellPath = /bin/sh; - shellScript = "$SCRIPT_INPUT_FILE_0 xcode\n$SCRIPT_INPUT_FILE_1 xcode\n\nrm -r $PWD/scripts\n"; - }; 47866E9AB7D201F2CED0064C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -3170,6 +3223,25 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Adamant/Pods-Adamant-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + 9372E0412C9BC178006DF0B3 /* Run Script - Git Data */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/CommonKit/Scripts/GitDataScript.sh", + ); + name = "Run Script - Git Data"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "$SCRIPT_INPUT_FILE_0 xcode\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -3188,14 +3260,13 @@ files = ( 93CCAE7E2B06DA6C00EA5B94 /* DogeBlockDTO.swift in Sources */, 6448C291235CA6E100F3F15B /* ERC20WalletService+RichMessageProviderWithStatusCheck.swift in Sources */, + 9332C3A32C76C45A00164B80 /* ApiServiceComposeProtocol.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 */, 3AFE7E522B1F6B3400718739 /* WalletServiceProtocol.swift in Sources */, 937751AB2A68BB390054BD65 /* ChatTransactionCell.swift in Sources */, 64A223D620F760BB005157CB /* Localization.swift in Sources */, @@ -3208,12 +3279,14 @@ 3AFE7E432B19E4D900718739 /* WalletServiceCompose.swift in Sources */, 3A26D93D2C3C1CC3003AD832 /* KlyNodeApiService.swift in Sources */, 93A118512993167500E144CC /* ChatMessageBackgroundColor.swift in Sources */, - E9CAE8D22018AA7700345E76 /* AdamantApi+Accounts.swift in Sources */, + 93760BD72C656CF8002507C3 /* DefaultNodesProvider.swift in Sources */, 3A26D93B2C3C1C97003AD832 /* KlyApiCore.swift in Sources */, + 2621AB372C60E74A00046D7A /* NotificationsView.swift in Sources */, 936658A32B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift in Sources */, 648CE3A6229AD1CD0070A2CC /* DashWalletService+Send.swift in Sources */, 3AF9DF0B2BFE306C009A43A8 /* ChatFileProtocol.swift in Sources */, E987024920C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift in Sources */, + 934FD9B02C78481500336841 /* InfoServiceApiError.swift in Sources */, 6403F5E222723F7500D58779 /* DashWallet.swift in Sources */, 26A975FF2B7E843E0095C367 /* SelectTextView.swift in Sources */, 93294B822AAD0BB400911109 /* BtcWalletFactory.swift in Sources */, @@ -3225,6 +3298,7 @@ 6403F5E422723F8C00D58779 /* DashWalletService.swift in Sources */, 9371E561295CD53100438F2C /* ChatLocalization.swift in Sources */, 93BF4A6629E4859900505CD0 /* DelegatesBottomPanel.swift in Sources */, + 9332C39D2C76BE7500164B80 /* FileApiServiceResult.swift in Sources */, 9322E877297042FA00B8357C /* ChatMessage.swift in Sources */, E908472F2196FEA80095825D /* BaseTransaction+CoreDataProperties.swift in Sources */, 93496B832A6C85F400DD062F /* AdamantResources+CoreData.swift in Sources */, @@ -3232,11 +3306,10 @@ E94008872114F05B00CD2D67 /* AddressValidationResult.swift in Sources */, E9E7CD8F20026CD300DFC4DB /* AdamantDialogService.swift in Sources */, E993301E212EF39700CD5200 /* EthTransferViewController.swift in Sources */, - E9CAE8DA2018ACD300345E76 /* AdamantApi+Chats.swift in Sources */, 648CE3A42299A94D0070A2CC /* DashTransactionDetailsViewController.swift in Sources */, + 934FD9AC2C78443600336841 /* InfoServiceHistoryItemDTO.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 */, 93FC16A12B01DE120062B507 /* ERC20ApiService.swift in Sources */, 3A299C692B838AA600B54C61 /* ChatMediaCell.swift in Sources */, @@ -3265,11 +3338,10 @@ E933475B225539390083839E /* DogeGetTransactionsResponse.swift in Sources */, 9340078029AC341100A20622 /* ChatAction.swift in Sources */, 648DD7A02236A59200B811FD /* DogeTransactionDetailsViewController.swift in Sources */, - 93ADC17D2B083C3B00F2DF77 /* NodesAdditionalParamsStorageProtocol.swift in Sources */, 3AA6DF402BA9941E00EA2E16 /* MediaContainerView.swift in Sources */, - 938A46A42AE6103E00FC03DB /* HealthCheckWrapper.swift in Sources */, 557AC308287B1365004699D7 /* CheckmarkRowView.swift in Sources */, 9390C5052976B53000270CDF /* ChatDialog.swift in Sources */, + 269B833D2C74E661002AA1D7 /* NotificationSoundsFactory.swift in Sources */, 6455E9F321075D8000B2E94C /* AdamantAddressBookService.swift in Sources */, 3A26D9472C3D37B5003AD832 /* KlyWalletViewController.swift in Sources */, 9324C75E297170600022D7EA /* TransactionStatusService.swift in Sources */, @@ -3277,11 +3349,10 @@ 9304F8BE292F88F900173F18 /* ANSPayload.swift in Sources */, 41CA598C29A0D84F002BFDE4 /* TaskManager.swift in Sources */, E9E7CD9120026FA100DFC4DB /* AppAssembly.swift in Sources */, - E96D64CA2295C4A800CA5587 /* WordList.swift in Sources */, - 3AA3880C2B69201B00125684 /* ADM+JsonDecode.swift in Sources */, 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */, 93B28EC22B076D31007F268B /* DashApiService.swift in Sources */, E908472B2196FEA80095825D /* RichMessageTransaction+CoreDataProperties.swift in Sources */, + 269B83372C74D1F9002AA1D7 /* NotificationSoundsView.swift in Sources */, A578BDE52623051C00090141 /* DashWalletService+Transactions.swift in Sources */, 93C794442B07725C00408826 /* DashGetAddressBalanceDTO.swift in Sources */, 6449BA69235CA0930033B936 /* ERC20TransferViewController.swift in Sources */, @@ -3298,24 +3369,25 @@ 3AE0A42A2BC6A64900BF7125 /* FilesNetworkManager.swift in Sources */, 648CE3AC229AD2190070A2CC /* DashTransferViewController.swift in Sources */, 3A7BD0102AA9BD030045AAB0 /* AdamantVibroService.swift in Sources */, + 937EDFC02C9CF6B300F219BB /* VersionFooterView.swift in Sources */, A5E04227282A8BDC0076CD13 /* BtcBalanceResponse.swift in Sources */, 64F085D920E2D7600006DE68 /* AdmTransactionsViewController.swift in Sources */, 9322E87B2970431200B8357C /* ChatMessageFactory.swift in Sources */, 648DD7AA2239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, - 9304F8C8292F972600173F18 /* ApiServiceError.swift in Sources */, E9D1BE1C211DABE100E86B72 /* WalletPagingItem.swift in Sources */, E940086E2114AA2E00CD2D67 /* WalletCoreProtocol.swift in Sources */, 645FEB34213E72C100D6BA2D /* OnboardViewController.swift in Sources */, 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 */, + 931224A92C7AA0E4009E0ED0 /* InfoServiceApiService+Extension.swift in Sources */, E95F85C7200A9B070070534A /* ChatTableViewCell.swift in Sources */, A50A41082822F8CE006BDFE1 /* BtcWalletService.swift in Sources */, 937736822B0949C500B35C7A /* NodeCell+Model.swift in Sources */, + 934FD9AA2C7842C800336841 /* InfoServiceResponseDTO.swift in Sources */, + 931224AD2C7AA67B009E0ED0 /* InfoServiceHistoryRequestDTO.swift in Sources */, 6455E9F121075D3600B2E94C /* AddressBookService.swift in Sources */, 93C794482B0778C700408826 /* DashGetBlockDTO.swift in Sources */, 6449BA6D235CA0930033B936 /* ERC20TransactionsViewController.swift in Sources */, @@ -3342,6 +3414,7 @@ 41CE153A297FF98200CC9254 /* Web3Swift+Adamant.swift in Sources */, 93CC8DC9296F01DE003772BF /* ChatTransactionContainerView+Model.swift in Sources */, E9147B6F205088DE00145913 /* LoginViewController+Pinpad.swift in Sources */, + 934FD9B62C78519600336841 /* InfoServiceApiCommands.swift in Sources */, E9FAE5E2203ED1AE008D3A6B /* ShareQrViewController.swift in Sources */, E983AE2120E655C500497E1A /* AccountHeaderView.swift in Sources */, E971591C2168209800A5F904 /* EthWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, @@ -3361,9 +3434,7 @@ 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 */, @@ -3371,7 +3442,6 @@ 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 */, E908472C2196FEA80095825D /* CoreDataAccount+CoreDataClass.swift in Sources */, @@ -3379,33 +3449,27 @@ E9393FAA2055D03300EE6F30 /* AdamantMessage.swift in Sources */, 938F7D692955C9EC001915CA /* ChatViewModel.swift in Sources */, E90A494D204DA932009F6A65 /* LocalAuthentication.swift in Sources */, - E96D64C62295C3ED00CA5587 /* Mnemonic+extended.swift in Sources */, 3A26D9352C3C1BE2003AD832 /* KlyWalletService.swift in Sources */, 41047B70294B5EE10039E956 /* VisibleWalletsViewController.swift in Sources */, + 2621AB392C60E7AE00046D7A /* NotificationsViewModel.swift in Sources */, 93B28EC82B076E68007F268B /* DashResponseDTO.swift in Sources */, - A5BBD811262C657300B5C40C /* ByteBackpacker.swift in Sources */, 648BCA6D213D384F00875EB5 /* AvatarService.swift in Sources */, - E95F856F2007B61D0070534A /* GetPublicKeyResponse.swift in Sources */, 3A26D94B2C3D3838003AD832 /* KlyTransactionsViewController.swift in Sources */, - 936658992B0AD32600BDB2D3 /* CoinsNodesListViewModel+ApiServices.swift in Sources */, - 644EC34D20EFA60900F40C73 /* AdamantApi+Delegates.swift in Sources */, E940088F2119A9E800CD2D67 /* BigInt+Decimal.swift in Sources */, + 934FD9B82C7854AF00336841 /* InfoServiceMapperProtocol.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 */, E940088B2114F63000CD2D67 /* NSRegularExpression+adamant.swift in Sources */, 3AE0A4372BC6AA6000BF7125 /* FilesNetworkManagerProtocol.swift in Sources */, 932B34E92974AA4A002A75BA /* ChatPreservationProtocol.swift in Sources */, - 3AA388032B67F47600125684 /* RPCResponseModel.swift in Sources */, E9484B7F2285C016008E10F0 /* PKGeneratorViewController.swift in Sources */, E9B3D3A9202082450019EB36 /* AdamantTransfersProvider.swift in Sources */, 6449BA71235CA0930033B936 /* ERC20WalletService+RichMessageProvider.swift in Sources */, 938F7D642955C94F001915CA /* ChatViewController.swift in Sources */, E9A174B72057F1B3003667CD /* AdamantChatsProvider+backgroundFetch.swift in Sources */, E96BBE3321F71290009AA738 /* BuyAndSellViewController.swift in Sources */, - 64EAB37422463E020018D9B2 /* CurrencyInfoService.swift in Sources */, + 64EAB37422463E020018D9B2 /* InfoServiceProtocol.swift in Sources */, 93CC8DC7296F00D6003772BF /* ChatTransactionContainerView.swift in Sources */, E9E7CDB32002B9FB00DFC4DB /* LoginFactory.swift in Sources */, E941CCDE20E7B70200C96220 /* WalletCollectionViewCell.swift in Sources */, @@ -3414,8 +3478,6 @@ 3AF08D5F2B4EB3A200EB82B1 /* LanguageService.swift in Sources */, E9AA8BFA212C166600F9249F /* EthWalletService+Send.swift in Sources */, 411743042A39B257008CD98A /* ContributeViewModel.swift in Sources */, - 93E5D4DB293000BE00439298 /* UnregisteredTransaction.swift in Sources */, - 3AB87CD62BF6237100AE8743 /* MultipartFormDataModel.swift in Sources */, 411DB8332A14D01F006AB158 /* ChatKeyboardManager.swift in Sources */, 6449BA68235CA0930033B936 /* ERC20WalletService.swift in Sources */, 3A9365A92C41332F0073D9A7 /* KLYWalletService+DynamicConstants.swift in Sources */, @@ -3424,7 +3486,6 @@ 9345769528FD0C34004E6C7A /* UIViewController+email.swift in Sources */, 64E1C831222E9617006C4DA7 /* DogeWalletService.swift in Sources */, 269E13522B594B2D008D1CA7 /* AccountFooterView.swift in Sources */, - E91947B22000246A001362F8 /* AdamantError.swift in Sources */, 3A4193912A580C85006A6B22 /* RichTransactionReactService.swift in Sources */, 93FC169B2B0197FD0062B507 /* BtcApiService.swift in Sources */, 3AA2D5F7280EADE3000ED971 /* SocketService.swift in Sources */, @@ -3432,8 +3493,6 @@ 41A1994429D2D3CF0031AD75 /* MessageModel.swift in Sources */, 93775E462A674FA9009061AC /* Markdown+Adamant.swift in Sources */, 3AE0A42C2BC6A64900BF7125 /* IPFSApiCore.swift in Sources */, - 3AA3880E2B6A356900125684 /* RpcRequestModel.swift in Sources */, - 93E8EDCD2AF1BD65003E163C /* AdamantApiCore.swift in Sources */, E9942B87203D9E5100C163AF /* EurekaQRRow.swift in Sources */, 3ACD307E2BBD86B700ABF671 /* FilesStorageProprietiesService.swift in Sources */, 3AA50DF32AEBE67C00C58FC8 /* PartnerQRFactory.swift in Sources */, @@ -3455,12 +3514,9 @@ 3A7BD00E2AA9BCE80045AAB0 /* VibroService.swift in Sources */, 3AF9DF0D2C049161009A43A8 /* CircularProgressView.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 /* TransactionStatusSubscription.swift in Sources */, - 9338AE7F2AEF43DA001D32DF /* NodesStorageProtocol.swift in Sources */, 93EE9C3329C2666200D9853F /* TransactionStatusSubscription.swift in Sources */, E913C8F21FFFA51D001A83F7 /* AppDelegate.swift in Sources */, 648CE3A222999CE70070A2CC /* BTCRawTransaction.swift in Sources */, @@ -3473,10 +3529,9 @@ 64E1C82D222E95E2006C4DA7 /* DogeWalletFactory.swift in Sources */, E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */, E90847322196FEA80095825D /* TransferTransaction+CoreDataClass.swift in Sources */, - 9304F8C6292F971600173F18 /* ApiServiceResult.swift in Sources */, + 93760BDF2C65A284002507C3 /* WordList.swift in Sources */, 93996A972968209C008D080B /* ChatMessagesCollection.swift in Sources */, 645AE06621E67D3300AD3623 /* UITextField+adamant.swift in Sources */, - 93E5D4E02930029300439298 /* AdamantCore+Extensions.swift in Sources */, 41047B72294B5F210039E956 /* VisibleWalletsTableViewCell.swift in Sources */, E90847392196FEF50095825D /* BaseTransaction+TransactionDetails.swift in Sources */, 3AE0A4312BC6A9C900BF7125 /* IPFSDTO.swift in Sources */, @@ -3491,40 +3546,34 @@ E940087B2114ED0600CD2D67 /* EthWalletService.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 */, 3AE0A42E2BC6A96B00BF7125 /* IPFS+Constants.swift in Sources */, E9FEECA62143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift in Sources */, 6403F5E622723FDA00D58779 /* DashWalletViewController.swift in Sources */, + 931224AF2C7AA88E009E0ED0 /* InfoService.swift in Sources */, 93C7944E2B077C1F00408826 /* DashSendRawTransactionDTO.swift in Sources */, 4193AE1629FBEFBF002F21BE /* NSAttributedText+Adamant.swift in Sources */, - 9338AE8F2AEF8131001D32DF /* InternalAPIError.swift in Sources */, + 931224AB2C7AA212009E0ED0 /* InfoServiceRatesRequestDTO.swift in Sources */, 41A1994829D325800031AD75 /* SwipeableView.swift in Sources */, - 5558A438282AB9390024DDD6 /* NodeStatus.swift in Sources */, - E91947AC20001A9A001362F8 /* ApiService.swift in Sources */, 4164A9D928F17DA700EEF16D /* AdamantChatTransactionService.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 /* NodeCell.swift in Sources */, 4133AF242A1CE1A3001A0A1E /* UITableView+Adamant.swift in Sources */, 41A1995629D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift in Sources */, + 934FD9BC2C78567300336841 /* InfoServiceApiServiceProtocol.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 */, 3A26D94D2C3D387B003AD832 /* KlyTransactionDetailsViewController.swift in Sources */, 648C697322916192006645F5 /* DashTransactionsViewController.swift in Sources */, 3AA6DF442BA997C000EA2E16 /* FileListContainerView.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 */, E9B3D3A1201FA26B0019EB36 /* AdamantAccountsProvider.swift in Sources */, @@ -3539,6 +3588,7 @@ 9390C5032976B42800270CDF /* ChatDialogManager.swift in Sources */, E926E02E213EAABF005E536B /* TransferViewControllerBase+Alert.swift in Sources */, 936658A52B0AE67A00BDB2D3 /* CoinsNodesListFactory.swift in Sources */, + 2621AB3B2C613C8100046D7A /* NotificationsFactory.swift in Sources */, E9B1AA572121ACC000080A2A /* AdmWalletViewController.swift in Sources */, 93C7944A2B077A1C00408826 /* DashGetUnspentTransactionsDTO.swift in Sources */, E9240BF5215D686500187B09 /* AdmWalletService+RichMessageProvider.swift in Sources */, @@ -3555,8 +3605,8 @@ E923222621135F9000A7E5AF /* EthAccount.swift in Sources */, 3A7FD6F52C076D86002AF7D9 /* FileMessageStatus.swift in Sources */, E9061B97207501E40011F104 /* AdamantUserInfoKey.swift in Sources */, + 934FD9A42C783D2E00336841 /* InfoServiceStatusDTO.swift in Sources */, 3A299C6D2B838F8F00B54C61 /* ChatMediaContainerView+Model.swift in Sources */, - E9CAE8D62018AC5300345E76 /* AdamantApi+Transactions.swift in Sources */, E93D7ABE2052CEE1005D19DC /* NotificationsService.swift in Sources */, E940087D2114EDEE00CD2D67 /* EthWallet.swift in Sources */, A5E0422B282AB18B0076CD13 /* BtcUnspentTransactionResponse.swift in Sources */, @@ -3565,6 +3615,7 @@ 93294B962AAD320B00911109 /* ScreensFactory.swift in Sources */, 3A9015A72A614A62002A2464 /* AdamantEmojiService.swift in Sources */, 93ADE0722ACA66AF008ED641 /* VibrationSelectionView.swift in Sources */, + 931224B12C7ACFE6009E0ED0 /* InfoServiceAssembly.swift in Sources */, 648C696F22915A12006645F5 /* DashTransaction.swift in Sources */, 3AF53F8F2B3EE0DA00B30312 /* DogeNodeInfo.swift in Sources */, 3A41939A2A5D554A006A6B22 /* Reaction.swift in Sources */, @@ -3574,13 +3625,12 @@ 4186B334294200C5006594A3 /* EthWalletService+DynamicConstants.swift in Sources */, 3A26D93F2C3C1CED003AD832 /* KlyServiceApiService.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 */, + 934FD9A62C783DB700336841 /* InfoServiceStatus.swift in Sources */, 93B28EC52B076E2C007F268B /* DashBlockchainInfoDTO.swift in Sources */, E98FC34420F920BD00032D65 /* UIFont+adamant.swift in Sources */, 644EC35720EFAAB700F40C73 /* DelegatesListViewController.swift in Sources */, @@ -3590,6 +3640,7 @@ E9B4E1A8210F079E007E77FC /* DoubleDetailsTableViewCell.swift in Sources */, E9502740202E257E002C1098 /* RepeaterService.swift in Sources */, E93D7AC02052CF63005D19DC /* AdamantNotificationService.swift in Sources */, + 934FD9B42C78514E00336841 /* InfoServiceApiCore.swift in Sources */, 3AF53F8D2B3DCFA300B30312 /* NodeGroup+Constants.swift in Sources */, 411743002A39B1D2008CD98A /* ContributeFactory.swift in Sources */, A5E04224282A830B0076CD13 /* BtcTransactionsViewController.swift in Sources */, @@ -3598,6 +3649,7 @@ 649D6BEC21BD5A53009E727B /* UISuffixTextField.swift in Sources */, E93B0D762028B28E00126346 /* AdamantChatsProvider.swift in Sources */, 3A33F9FA2A7A53DA002B8003 /* EmojiUpdateType.swift in Sources */, + 934FD9A82C783E0C00336841 /* InfoServiceMapper.swift in Sources */, 936658932B0AC03700BDB2D3 /* CoinsNodesListStrings.swift in Sources */, 3A5DF1792C4698EC0005369D /* EdgeInsetLabel.swift in Sources */, 3AF8D9E92C73ADFA007A7CBC /* IPFSNodeStatus.swift in Sources */, @@ -3609,7 +3661,6 @@ 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 */, @@ -3622,16 +3673,15 @@ 3A2478B12BB45DF8009D89E9 /* StorageUsageView.swift in Sources */, E90A4943204C5ED6009F6A65 /* EurekaPassphraseRow.swift in Sources */, E90847302196FEA80095825D /* ChatTransaction+CoreDataClass.swift in Sources */, - E913C9081FFFA943001A83F7 /* AdamantCore.swift in Sources */, 3A96E37A2AED27D7001F5A52 /* AdamantPartnerQRService.swift in Sources */, E9EC342120052ABB00C0E546 /* TransferViewControllerBase.swift in Sources */, + 937173F52C8049E0009D5191 /* InfoService+Constants.swift in Sources */, + 931224B32C7AD5DD009E0ED0 /* InfoServiceTicker.swift in Sources */, 9304F8C4292F8A3100173F18 /* AdamantPushNotificationsTokenService.swift in Sources */, 9300F94629D0149100FEDDB8 /* RichMessageProviderWithStatusCheck.swift in Sources */, - E9771D9E22997A6F0099AAC7 /* NativeCore+AdamantCore.swift in Sources */, + 269B833A2C74D4AA002AA1D7 /* NotificationSoundsViewModel.swift in Sources */, 416380E12A51765F00F90E6D /* ChatReactionsView.swift in Sources */, - 938A46A62AE6106300FC03DB /* BlockchainHealthCheckWrapper.swift in Sources */, E921534E20EE1E8700C0843F /* EurekaAlertLabelRow.swift in Sources */, - 9338AE8D2AEF7E9C001D32DF /* BodyStringEncoding.swift in Sources */, 3AE0A42B2BC6A64900BF7125 /* IPFSApiService.swift in Sources */, 64C65F4523893C7600DC0425 /* OnboardOverlay.swift in Sources */, 93A18C862AAEACC100D0AB98 /* AdamantWalletFactoryCompose.swift in Sources */, @@ -3639,6 +3689,7 @@ E9484B79227C617E008E10F0 /* BalanceTableViewCell.swift in Sources */, E90847352196FEA80095825D /* MessageTransaction+CoreDataProperties.swift in Sources */, E9771DA722997F310099AAC7 /* ServerResponseWithTimestamp.swift in Sources */, + 934FD9AE2C7846BA00336841 /* InfoServiceHistoryItem.swift in Sources */, E9A03FD420DBC824007653A1 /* NodeVersion.swift in Sources */, 648CE3AA229AD1F90070A2CC /* DashWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, 937751AD2A68BCE10054BD65 /* MessageCellWrapper.swift in Sources */, @@ -3646,21 +3697,24 @@ E90847342196FEA80095825D /* MessageTransaction+CoreDataClass.swift in Sources */, E9960B3521F5154300C840A8 /* DummyAccount+CoreDataClass.swift in Sources */, 64E1C833222EA0F0006C4DA7 /* DogeWalletViewController.swift in Sources */, + 934FD9B22C7849C800336841 /* InfoServiceApiResult.swift in Sources */, E93EB09F20DA3FA4001F9601 /* NodesEditorFactory.swift in Sources */, 93294B8F2AAD2C6B00911109 /* SwiftyOnboard.swift in Sources */, - 93ADC17B2B08283500F2DF77 /* ForceQueryItemsEncoding.swift in Sources */, + 93760BE12C65A2F3002507C3 /* Mnemonic+extended.swift in Sources */, 41BCB310295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift in Sources */, 93CCAE7B2B06D9B500EA5B94 /* DogeBlocksDTO.swift in Sources */, 3AF08D612B4EB3C400EB82B1 /* LanguageStorageProtocol.swift in Sources */, E9E7CDB12002B97B00DFC4DB /* AccountFactory.swift in Sources */, E9AA8BF82129F13000F9249F /* ComplexTransferViewController.swift in Sources */, E9A174B52057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift in Sources */, + 934FD9BA2C78565400336841 /* InfoServiceApiService.swift in Sources */, 9382F61329DEC0A3005E6216 /* ChatModelView.swift in Sources */, E9147B5F20500E9300145913 /* MyLittlePinpad+adamant.swift in Sources */, E9981896212095CA0018C84C /* EthWalletViewController.swift in Sources */, 648DD7A22237D9A000B811FD /* DogeTransaction.swift in Sources */, E90847372196FEA80095825D /* Chatroom+CoreDataProperties.swift in Sources */, 648CE3A8229AD1E20070A2CC /* DashWalletService+RichMessageProvider.swift in Sources */, + 9332C3A52C76C4EC00164B80 /* ApiServiceCompose.swift in Sources */, E90055FB20ECE78A00D0CB2D /* SecurityViewController+notifications.swift in Sources */, 938F7D662955C966001915CA /* ChatInputBar.swift in Sources */, A50A41122822FC35006BDFE1 /* BtcWalletService+RichMessageProvider.swift in Sources */, @@ -3674,7 +3728,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 93E5D4DD293000BE00439298 /* UnregisteredTransaction.swift in Sources */, E957E132229B10F80019732A /* NotificationViewController.swift in Sources */, 93E1234D2A6DFF62004DF33B /* NotificationStrings.swift in Sources */, ); @@ -3685,7 +3738,6 @@ buildActionMask = 2147483647; files = ( E96D64DE2295CD4700CA5587 /* NotificationService.swift in Sources */, - 93E5D4DC293000BE00439298 /* UnregisteredTransaction.swift in Sources */, 93E1234C2A6DFF62004DF33B /* NotificationStrings.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3992,7 +4044,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.8.0; + MARKETING_VERSION = 3.9.0; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4023,7 +4075,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.8.0; + MARKETING_VERSION = 3.9.0; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4327,14 +4379,6 @@ minimumVersion = 10.1.1; }; }; - A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Alamofire/Alamofire.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 5.4.2; - }; - }; A544F0D2262C9878001F1A6D /* XCRemoteSwiftPackageReference "Eureka" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/xmartlabs/Eureka.git"; @@ -4500,11 +4544,6 @@ package = A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */; productName = QRCodeReader; }; - A50AEB13262C837900B37C22 /* Alamofire */ = { - isa = XCSwiftPackageProductDependency; - package = A50AEB12262C837900B37C22 /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; - }; A5241B6F262DEDE1009FA43E /* Clibsodium */ = { isa = XCSwiftPackageProductDependency; package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */; diff --git a/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved index 88a2fda6f..2daa291d5 100644 --- a/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/Alamofire/Alamofire.git", "state": { "branch": null, - "revision": "d120af1e8638c7da36c8481fd61a66c0c08dc4fc", - "version": "5.4.4" + "revision": "bc268c28fb170f494de9e9927c371b8342979ece", + "version": "5.7.1" } }, { @@ -60,7 +60,7 @@ "repositoryURL": "https://github.com/EFPrefix/EFQRCode.git", "state": { "branch": null, - "revision": "2991c2f318ad9529d93b2a73a382a3f9c72c64ce", + "revision": "3a6c5012f1a0df404a92e55bb01b4b685ff5a2d1", "version": "6.2.2" } }, @@ -309,11 +309,11 @@ }, { "package": "swift_qrcodejs", - "repositoryURL": "https://github.com/ApolloZhu/swift_qrcodejs.git", + "repositoryURL": "https://github.com/EFPrefix/swift_qrcodejs.git", "state": { "branch": null, - "revision": "374dc7f7b9e76c6aeb393f6a84590c6d387e1ecb", - "version": "2.2.2" + "revision": "817ba220a2eba840bae888e7eeb11207bec05f8c", + "version": "2.3.0" } }, { diff --git a/Adamant/App/AppDelegate.swift b/Adamant/App/AppDelegate.swift index ad9b80d34..32a247d4d 100644 --- a/Adamant/App/AppDelegate.swift +++ b/Adamant/App/AppDelegate.swift @@ -38,7 +38,6 @@ extension StoreKey { struct application { static let welcomeScreensIsShown = "app.welcomeScreensIsShown" static let eulaAccepted = "app.eulaAccepted" - static let firstRun = "app.firstRun" private init() {} } @@ -62,10 +61,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - Lifecycle - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // MARK: 0. Migrate keychain if needed - KeychainStore.migrateIfNeeded() - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // MARK: 1. Initiating Swinject container = AppContainer() screensFactory = AdamantScreensFactory(assembler: container.assembler) @@ -82,17 +78,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { .resolve(CrashlyticsService.self)? .configureIfNeeded() - // MARK: 1.2 First run flag - let firstRun = UserDefaults.standard.bool(forKey: StoreKey.application.firstRun) - - if !firstRun { - UserDefaults.standard.set(true, forKey: StoreKey.application.firstRun) - - if let securedStore = container.resolve(SecuredStore.self) { - securedStore.purgeStore() - } - } - // MARK: 2. Init UI let window = UIWindow(frame: UIScreen.main.bounds) self.window = window @@ -263,11 +248,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { dialogService.showError(withMessage: "Failed to register AddressBookService autoupdate. Please, report a bug", supportEmail: true, error: nil) } - if let currencyInfoService = container.resolve(CurrencyInfoService.self) { + if let currencyInfoService = container.resolve(InfoServiceProtocol.self) { currencyInfoService.update() // Initial update repeater.registerForegroundCall(label: "currencyInfoService", interval: 60, queue: .global(qos: .utility), callback: currencyInfoService.update) } else { - dialogService.showError(withMessage: "Failed to register CurrencyInfoService autoupdate. Please, report a bug", supportEmail: true, error: nil) + dialogService.showError(withMessage: "Failed to register InfoServiceProtocol autoupdate. Please, report a bug", supportEmail: true, error: nil) } // MARK: 7. Logout reset diff --git a/Adamant/App/DI/AppAssembly.swift b/Adamant/App/DI/AppAssembly.swift index 9b05680e1..aa52f865f 100644 --- a/Adamant/App/DI/AppAssembly.swift +++ b/Adamant/App/DI/AppAssembly.swift @@ -29,7 +29,9 @@ struct AppAssembly: Assembly { container.register(CellFactory.self) { _ in AdamantCellFactory() }.inObjectScope(.container) // MARK: Secured Store - container.register(SecuredStore.self) { _ in KeychainStore() }.inObjectScope(.container) + container.register(SecuredStore.self) { _ in + KeychainStore(secureStorage: AdamantSecureStorage()) + }.inObjectScope(.container) // MARK: LocalAuthentication container.register(LocalAuthentication.self) { _ in AdamantAuthentication() }.inObjectScope(.container) @@ -43,16 +45,23 @@ struct AppAssembly: Assembly { // MARK: - Services with dependencies // MARK: DialogService container.register(DialogService.self) { r in - AdamantDialogService(vibroService: r.resolve(VibroService.self)!) + AdamantDialogService( + vibroService: r.resolve(VibroService.self)!, + notificationsService: r.resolve(NotificationsService.self)! + ) }.inObjectScope(.container) // MARK: Notifications container.register(NotificationsService.self) { r in - AdamantNotificationsService(securedStore: r.resolve(SecuredStore.self)!) + AdamantNotificationsService( + securedStore: r.resolve(SecuredStore.self)!, + vibroService: r.resolve(VibroService.self)! + ) }.initCompleted { (r, c) in // Weak reference Task { @MainActor in guard let service = c as? AdamantNotificationsService else { return } service.accountService = r.resolve(AccountService.self) + service.chatsProvider = r.resolve(ChatsProvider.self) } }.inObjectScope(.container) @@ -95,7 +104,7 @@ struct AppAssembly: Assembly { container.register(PushNotificationsTokenService.self) { r in AdamantPushNotificationsTokenService( securedStore: r.resolve(SecuredStore.self)!, - apiService: r.resolve(ApiService.self)!, + apiService: r.resolve(AdamantApiServiceProtocol.self)!, adamantCore: r.resolve(AdamantCore.self)!, accountService: r.resolve(AccountService.self)! ) @@ -103,7 +112,13 @@ struct AppAssembly: Assembly { // MARK: NodesStorage container.register(NodesStorageProtocol.self) { r in - NodesStorage(securedStore: r.resolve(SecuredStore.self)!) + NodesStorage( + securedStore: r.resolve(SecuredStore.self)!, + nodesMergingService: r.resolve(NodesMergingServiceProtocol.self)!, + defaultNodes: { [provider = r.resolve(DefaultNodesProvider.self)!] groups in + provider.get(groups) + } + ) }.inObjectScope(.container) // MARK: NodesAdditionalParamsStorage @@ -117,13 +132,15 @@ struct AppAssembly: Assembly { }.inObjectScope(.container) // MARK: ApiService - container.register(ApiService.self) { r in + container.register(AdamantApiServiceProtocol.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 + isActive: true, + params: NodeGroup.adm.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher ), adamantCore: r.resolve(AdamantCore.self)! ) @@ -131,14 +148,14 @@ struct AppAssembly: Assembly { // MARK: IPFSApiService container.register(IPFSApiService.self) { r in - IPFSApiService( - healthCheckWrapper: .init( - service: .init(apiCore: r.resolve(APICoreProtocol.self)!), - nodesStorage: r.resolve(NodesStorageProtocol.self)!, - nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - nodeGroup: .ipfs - ) - ) + IPFSApiService(healthCheckWrapper: .init( + service: .init(apiCore: r.resolve(APICoreProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + isActive: true, + params: NodeGroup.ipfs.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher + )) }.inObjectScope(.container) // MARK: FilesNetworkManagerProtocol @@ -152,7 +169,9 @@ struct AppAssembly: Assembly { service: .init(apiCore: r.resolve(APICoreProtocol.self)!), nodesStorage: r.resolve(NodesStorageProtocol.self)!, nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - nodeGroup: .btc + isActive: true, + params: NodeGroup.btc.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher )) }.inObjectScope(.container) @@ -162,7 +181,9 @@ struct AppAssembly: Assembly { service: .init(apiCore: r.resolve(APICoreProtocol.self)!), nodesStorage: r.resolve(NodesStorageProtocol.self)!, nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - nodeGroup: .doge + isActive: true, + params: NodeGroup.doge.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher )) }.inObjectScope(.container) @@ -172,7 +193,9 @@ struct AppAssembly: Assembly { service: .init(apiCore: r.resolve(APICoreProtocol.self)!), nodesStorage: r.resolve(NodesStorageProtocol.self)!, nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - nodeGroup: .dash + isActive: true, + params: NodeGroup.dash.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher )) }.inObjectScope(.container) @@ -182,7 +205,9 @@ struct AppAssembly: Assembly { service: .init(), nodesStorage: r.resolve(NodesStorageProtocol.self)!, nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - nodeGroup: .klyNode + isActive: true, + params: NodeGroup.klyNode.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher )) }.inObjectScope(.container) @@ -192,7 +217,9 @@ struct AppAssembly: Assembly { service: .init(), nodesStorage: r.resolve(NodesStorageProtocol.self)!, nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - nodeGroup: .klyService + isActive: true, + params: NodeGroup.klyService.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher )) }.inObjectScope(.container) @@ -207,7 +234,9 @@ struct AppAssembly: Assembly { service: .init(apiCore: r.resolve(APICoreProtocol.self)!), nodesStorage: r.resolve(NodesStorageProtocol.self)!, nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - nodeGroup: .eth + isActive: true, + params: NodeGroup.eth.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher )) }.inObjectScope(.container) @@ -222,18 +251,18 @@ struct AppAssembly: Assembly { // MARK: AccountService container.register(AccountService.self) { r in AdamantAccountService( - apiService: r.resolve(ApiService.self)!, + apiService: r.resolve(AdamantApiServiceProtocol.self)!, adamantCore: r.resolve(AdamantCore.self)!, dialogService: r.resolve(DialogService.self)!, securedStore: r.resolve(SecuredStore.self)!, - walletServiceCompose: r.resolve(WalletServiceCompose.self)! + walletServiceCompose: r.resolve(WalletServiceCompose.self)!, + currencyInfoService: r.resolve(InfoServiceProtocol.self)! ) }.inObjectScope(.container).initCompleted { (r, c) in Task { @MainActor in guard let service = c as? AdamantAccountService else { return } service.notificationsService = r.resolve(NotificationsService.self)! service.pushNotificationsTokenService = r.resolve(PushNotificationsTokenService.self)! - service.currencyInfoService = r.resolve(CurrencyInfoService.self)! service.visibleWalletService = r.resolve(VisibleWalletsService.self)! } } @@ -241,21 +270,13 @@ struct AppAssembly: Assembly { // MARK: AddressBookServeice container.register(AddressBookService.self) { r in AdamantAddressBookService( - apiService: r.resolve(ApiService.self)!, + apiService: r.resolve(AdamantApiServiceProtocol.self)!, adamantCore: r.resolve(AdamantCore.self)!, accountService: r.resolve(AccountService.self)!, dialogService: r.resolve(DialogService.self)! ) }.inObjectScope(.container) - // MARK: CurrencyInfoService - container.register(CurrencyInfoService.self) { r in - AdamantCurrencyInfoService( - securedStore: r.resolve(SecuredStore.self)!, - walletServiceCompose: r.resolve(WalletServiceCompose.self)! - ) - }.inObjectScope(.container) - // MARK: LanguageStorageProtocol container.register(LanguageStorageProtocol.self) { _ in LanguageStorageService() @@ -271,7 +292,7 @@ struct AppAssembly: Assembly { container.register(AccountsProvider.self) { r in AdamantAccountsProvider( stack: r.resolve(CoreDataStack.self)!, - apiService: r.resolve(ApiService.self)!, + apiService: r.resolve(AdamantApiServiceProtocol.self)!, addressBookService: r.resolve(AddressBookService.self)! ) }.inObjectScope(.container) @@ -279,7 +300,7 @@ struct AppAssembly: Assembly { // MARK: Transfers container.register(TransfersProvider.self) { r in AdamantTransfersProvider( - apiService: r.resolve(ApiService.self)!, + apiService: r.resolve(AdamantApiServiceProtocol.self)!, stack: r.resolve(CoreDataStack.self)!, adamantCore: r.resolve(AdamantCore.self)!, accountService: r.resolve(AccountService.self)!, @@ -312,7 +333,7 @@ struct AppAssembly: Assembly { container.register(ChatsProvider.self) { r in AdamantChatsProvider( accountService: r.resolve(AccountService.self)!, - apiService: r.resolve(ApiService.self)!, + apiService: r.resolve(AdamantApiServiceProtocol.self)!, socketService: r.resolve(SocketService.self)!, stack: r.resolve(CoreDataStack.self)!, adamantCore: r.resolve(AdamantCore.self)!, @@ -344,7 +365,7 @@ struct AppAssembly: Assembly { container.register(RichTransactionReplyService.self) { r in AdamantRichTransactionReplyService( coreDataStack: r.resolve(CoreDataStack.self)!, - apiService: r.resolve(ApiService.self)!, + apiService: r.resolve(AdamantApiServiceProtocol.self)!, adamantCore: r.resolve(AdamantCore.self)!, accountService: r.resolve(AccountService.self)!, walletServiceCompose: r.resolve(WalletServiceCompose.self)! @@ -355,7 +376,7 @@ struct AppAssembly: Assembly { container.register(RichTransactionReactService.self) { r in AdamantRichTransactionReactService( coreDataStack: r.resolve(CoreDataStack.self)!, - apiService: r.resolve(ApiService.self)!, + apiService: r.resolve(AdamantApiServiceProtocol.self)!, adamantCore: r.resolve(AdamantCore.self)!, accountService: r.resolve(AccountService.self)! ) @@ -400,5 +421,30 @@ struct AppAssembly: Assembly { } } } + + // MARK: ApiService Compose + container.register(ApiServiceComposeProtocol.self) { + ApiServiceCompose( + btc: $0.resolve(BtcApiService.self)!, + eth: $0.resolve(EthApiService.self)!, + klyNode: $0.resolve(KlyNodeApiService.self)!, + klyService: $0.resolve(KlyServiceApiService.self)!, + doge: $0.resolve(DogeApiService.self)!, + dash: $0.resolve(DashApiService.self)!, + adm: $0.resolve(AdamantApiServiceProtocol.self)!, + ipfs: $0.resolve(IPFSApiService.self)!, + infoService: $0.resolve(InfoServiceApiServiceProtocol.self)! + ) + }.inObjectScope(.transient) + + // MARK: NodesMergingService + container.register(NodesMergingServiceProtocol.self) { _ in + NodesMergingService() + }.inObjectScope(.transient) + + // MARK: DefaultNodesProvider + container.register(DefaultNodesProvider.self) { _ in + DefaultNodesProvider() + }.inObjectScope(.transient) } } diff --git a/Adamant/App/DI/AppContainer.swift b/Adamant/App/DI/AppContainer.swift index 3f8fa7602..d119dbb69 100644 --- a/Adamant/App/DI/AppContainer.swift +++ b/Adamant/App/DI/AppContainer.swift @@ -9,7 +9,10 @@ import Swinject struct AppContainer { - let assembler = Assembler([AppAssembly()]) + let assembler = Assembler([ + AppAssembly(), + InfoServiceAssembly() + ]) func resolve(_ type: T.Type) -> T? { assembler.resolve(T.self) diff --git a/Adamant/Assets/antic.mp3 b/Adamant/Assets/antic.mp3 new file mode 100644 index 000000000..641f5fd44 Binary files /dev/null and b/Adamant/Assets/antic.mp3 differ diff --git a/Adamant/Assets/cheers.mp3 b/Adamant/Assets/cheers.mp3 new file mode 100644 index 000000000..2dee5a409 Binary files /dev/null and b/Adamant/Assets/cheers.mp3 differ diff --git a/Adamant/Assets/chord.mp3 b/Adamant/Assets/chord.mp3 new file mode 100644 index 000000000..f19cab948 Binary files /dev/null and b/Adamant/Assets/chord.mp3 differ diff --git a/Adamant/Assets/droplet.mp3 b/Adamant/Assets/droplet.mp3 new file mode 100644 index 000000000..27a630e10 Binary files /dev/null and b/Adamant/Assets/droplet.mp3 differ diff --git a/Adamant/Assets/handoff.mp3 b/Adamant/Assets/handoff.mp3 new file mode 100644 index 000000000..c6850918f Binary files /dev/null and b/Adamant/Assets/handoff.mp3 differ diff --git a/Adamant/Assets/milestone.mp3 b/Adamant/Assets/milestone.mp3 new file mode 100644 index 000000000..37615d2e3 Binary files /dev/null and b/Adamant/Assets/milestone.mp3 differ diff --git a/Adamant/Assets/note.mp3 b/Adamant/Assets/note.mp3 new file mode 100644 index 000000000..2f51d1c0c Binary files /dev/null and b/Adamant/Assets/note.mp3 differ diff --git a/Adamant/Assets/passage.mp3 b/Adamant/Assets/passage.mp3 new file mode 100644 index 000000000..4bd794839 Binary files /dev/null and b/Adamant/Assets/passage.mp3 differ diff --git a/Adamant/Assets/portal.mp3 b/Adamant/Assets/portal.mp3 new file mode 100644 index 000000000..0f061ccac Binary files /dev/null and b/Adamant/Assets/portal.mp3 differ diff --git a/Adamant/Assets/rattle.mp3 b/Adamant/Assets/rattle.mp3 new file mode 100644 index 000000000..18c06fb46 Binary files /dev/null and b/Adamant/Assets/rattle.mp3 differ diff --git a/Adamant/Assets/rebound.mp3 b/Adamant/Assets/rebound.mp3 new file mode 100644 index 000000000..33518d4f8 Binary files /dev/null and b/Adamant/Assets/rebound.mp3 differ diff --git a/Adamant/Assets/slide.mp3 b/Adamant/Assets/slide.mp3 new file mode 100644 index 000000000..141220c3e Binary files /dev/null and b/Adamant/Assets/slide.mp3 differ diff --git a/Adamant/Assets/welcome.mp3 b/Adamant/Assets/welcome.mp3 new file mode 100644 index 000000000..bb7cd7279 Binary files /dev/null and b/Adamant/Assets/welcome.mp3 differ diff --git a/Adamant/Debug.entitlements b/Adamant/Debug.entitlements index 7713a96e4..8e1de5348 100644 --- a/Adamant/Debug.entitlements +++ b/Adamant/Debug.entitlements @@ -20,6 +20,10 @@ $(TeamIdentifierPrefix)$(CFBundleIdentifier) com.apple.security.app-sandbox + com.apple.security.application-groups + + group.adamant.adamant-messenger + com.apple.security.device.camera com.apple.security.network.client diff --git a/Adamant/Helpers/ApiServiceError+Extension.swift b/Adamant/Helpers/ApiServiceError+Extension.swift index b07097854..a1610bea2 100644 --- a/Adamant/Helpers/ApiServiceError+Extension.swift +++ b/Adamant/Helpers/ApiServiceError+Extension.swift @@ -7,16 +7,36 @@ // import Alamofire +import CommonKit -extension ApiServiceError { - init(error: Error) { - let afError = error as? AFError - - switch afError { - case .explicitlyCancelled: - self = .requestCancelled - default: - self = .networkError(error: error) +extension ApiServiceError: RichError { + var message: String { + localizedDescription + } + + var level: ErrorLevel { + switch self { + case .accountNotFound, .notLogged, .networkError, .requestCancelled, .noEndpointsAvailable: + return .warning + + case .serverError, .commonError: + return .error + + case .internalError: + return .internalError + } + } + + var internalError: Error? { + switch self { + case .accountNotFound, .notLogged, .serverError, .requestCancelled, .commonError, .noEndpointsAvailable: + return nil + + case .internalError(_, let error): + return error + + case .networkError(let error): + return error } } } diff --git a/Adamant/Helpers/Localization.swift b/Adamant/Helpers/Localization.swift index a27958ba4..c5c6fde4d 100644 --- a/Adamant/Helpers/Localization.swift +++ b/Adamant/Helpers/Localization.swift @@ -6,19 +6,9 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation +import CommonKit import UIKit -protocol Localizable { - var localized: String { get } -} - -extension String: Localizable { - var localized: String { - return .localized(self, comment: "") - } -} - protocol XIBLocalizable { var xibLocKey: String? { get set } } diff --git a/Adamant/Helpers/Node+UI.swift b/Adamant/Helpers/Node+UI.swift index 25a06a4c8..a3a8c26e3 100644 --- a/Adamant/Helpers/Node+UI.swift +++ b/Adamant/Helpers/Node+UI.swift @@ -10,43 +10,36 @@ import CommonKit import UIKit extension Node { - func statusString(showVersion: Bool, includeVersionTitle: Bool = true) -> String? { - guard isEnabled else { return Strings.disabled } + // swiftlint:disable switch_case_alignment + func statusString(showVersion: Bool, dateHeight: Bool) -> String? { + guard + isEnabled, + let connectionStatus = connectionStatus + else { return Strings.disabled } - switch connectionStatus { + let statusTitle = switch connectionStatus { case .allowed: - return [ - pingString, - showVersion ? versionString(includeVersionTitle: includeVersionTitle) : nil, - heightString - ] - .compactMap { $0 } - .joined(separator: " ") + pingString case .synchronizing: - return [ - Strings.synchronizing, - showVersion ? versionString(includeVersionTitle: includeVersionTitle) : nil, - heightString - ] - .compactMap { $0 } - .joined(separator: " ") + Strings.synchronizing case .offline: - return Strings.offline + Strings.offline case .notAllowed(let reason): - return [ - reason.text, - version - ] - .compactMap { $0 } - .joined(separator: " ") - case .none: - return nil + reason.text } + + return [ + statusTitle, + showVersion ? versionString : nil, + dateHeight ? dateHeightString : heightString + ] + .compactMap { $0 } + .joined(separator: " ") } func indicatorString(isRest: Bool, isWs: Bool) -> String { let connections = [ - isRest ? scheme.rawValue : nil, + isRest ? preferredOrigin.scheme.rawValue : nil, isWs ? "ws" : nil ].compactMap { $0 } @@ -65,15 +58,19 @@ extension Node { switch connectionStatus { case .allowed: - return .adamant.good + return .adamant.success case .synchronizing: - return .adamant.alert + return .adamant.attention case .offline, .notAllowed: - return .adamant.danger + return .adamant.warning case .none: return .adamant.inactive } } + + var title: String { + mainOrigin.asString() + } } private extension Node { @@ -130,6 +127,14 @@ private extension Node { height.map { " ❐ \(getFormattedHeight(from: $0))" } } + var dateHeightString: String? { + height.map { " ❐ \(Date(timeIntervalSince1970: .init($0)).humanizedTime().string)" } + } + + var versionString: String? { + version.map { "(v\($0.string))" } + } + var numberFormatter: NumberFormatter { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal @@ -140,22 +145,4 @@ private extension Node { func getFormattedHeight(from height: Int) -> String { numberFormatter.string(from: Decimal(height)) ?? String(height) } - - func versionString(includeVersionTitle: Bool) -> String? { - guard includeVersionTitle else { - return version.map { "(\($0))" } - } - - return version.map { "(v\($0))" } - } -} - -extension Node { - static func stringToDouble(_ value: String?) -> Double? { - guard let minNodeVersion = value?.replacingOccurrences(of: ".", with: ""), - let versionNumber = Double(minNodeVersion) - else { return nil } - - return versionNumber - } } diff --git a/Adamant/Helpers/NodeGroup+Constants.swift b/Adamant/Helpers/NodeGroup+Constants.swift index 47203f9fb..8fbeb847f 100644 --- a/Adamant/Helpers/NodeGroup+Constants.swift +++ b/Adamant/Helpers/NodeGroup+Constants.swift @@ -8,7 +8,7 @@ import Foundation import CommonKit -public extension NodeGroup { +extension NodeGroup { var onScreenUpdateInterval: TimeInterval { switch self { case .adm: @@ -26,7 +26,9 @@ public extension NodeGroup { case .dash: return DashWalletService.healthCheckParameters.onScreenUpdateInterval case .ipfs: - return IPFSApiService.healthCheckParameters.onScreenUpdateInterval + return 10 // TODO: Fix the adamant-wallets script and the repo itself + case .infoService: + return AdmWalletService.healthCheckParameters.onScreenServiceUpdateInterval } } @@ -48,6 +50,8 @@ public extension NodeGroup { return DashWalletService.healthCheckParameters.crucialUpdateInterval case .ipfs: return IPFSApiService.healthCheckParameters.crucialUpdateInterval + case .infoService: + return AdmWalletService.healthCheckParameters.crucialServiceUpdateInterval } } @@ -68,7 +72,9 @@ public extension NodeGroup { case .dash: return DashWalletService.healthCheckParameters.threshold case .ipfs: - return IPFSApiService.healthCheckParameters.threshold + return IPFSApiService.healthCheckParameters.nodeHeightEpsilon + case .infoService: + return InfoService.threshold } } @@ -90,34 +96,77 @@ public extension NodeGroup { return DashWalletService.healthCheckParameters.normalUpdateInterval case .ipfs: return IPFSApiService.healthCheckParameters.normalUpdateInterval + case .infoService: + return AdmWalletService.healthCheckParameters.normalServiceUpdateInterval } } - var minNodeVersion: Double { - var minNodeVersion: String? + var minNodeVersion: Version? { + let version: String? switch self { case .adm: - minNodeVersion = AdmWalletService.minNodeVersion + version = AdmWalletService.minNodeVersion case .btc: - minNodeVersion = BtcWalletService.minNodeVersion + version = BtcWalletService.minNodeVersion case .eth: - minNodeVersion = EthWalletService.minNodeVersion + version = EthWalletService.minNodeVersion case .klyNode: - minNodeVersion = KlyWalletService.minNodeVersion + version = KlyWalletService.minNodeVersion case .klyService: - minNodeVersion = KlyWalletService.minNodeVersion + version = KlyWalletService.minNodeVersion case .doge: - minNodeVersion = DogeWalletService.minNodeVersion + version = DogeWalletService.minNodeVersion case .dash: - minNodeVersion = DashWalletService.minNodeVersion - case .ipfs: - minNodeVersion = nil + version = DashWalletService.minNodeVersion + case .ipfs, .infoService: + version = nil } + guard let version = version else { return nil } - guard let versionNumber = Node.stringToDouble(minNodeVersion) else { - return .zero + return .init(version) + } + + var name: String { + switch self { + case .btc: + return BtcWalletService.tokenNetworkSymbol + case .eth: + return EthWalletService.tokenNetworkSymbol + case .klyNode: + return KlyWalletService.tokenNetworkSymbol + case .klyService: + return KlyWalletService.tokenNetworkSymbol + + " " + .adamant.coinsNodesList.serviceNode + case .doge: + return DogeWalletService.tokenNetworkSymbol + case .dash: + return DashWalletService.tokenNetworkSymbol + case .adm: + return AdmWalletService.tokenNetworkSymbol + case .ipfs: + return IPFSApiService.symbol + case .infoService: + return InfoService.name } - - return versionNumber + } + + var useDateHeight: Bool { + switch self { + case .btc, .eth, .klyNode, .klyService, .doge, .dash, .adm, .ipfs: + false + case .infoService: + true + } + } + + var blockchainHealthCheckParams: BlockchainHealthCheckParams { + .init( + group: self, + name: name, + normalUpdateInterval: normalUpdateInterval, + crucialUpdateInterval: crucialUpdateInterval, + minNodeVersion: minNodeVersion, + nodeHeightEpsilon: nodeHeightEpsilon + ) } } diff --git a/Adamant/Helpers/String+adamant.swift b/Adamant/Helpers/String+adamant.swift index a69173cc3..24db3e1ba 100644 --- a/Adamant/Helpers/String+adamant.swift +++ b/Adamant/Helpers/String+adamant.swift @@ -41,6 +41,7 @@ extension String { let address: String? var name: String? var message: String? + var amount: Double? let newUrl = self.replacingOccurrences(of: "//", with: "") @@ -57,6 +58,8 @@ extension String { name = label case .message(let urlMessage): message = urlMessage + case .amount(let value): + amount = value } } } @@ -71,6 +74,8 @@ extension String { name = label case .message(let urlMessage): message = urlMessage + case .amount(let value): + amount = value } } } @@ -88,10 +93,15 @@ extension String { } if let address = address { - return AdamantAddress(address: address, name: name, amount: nil, message: message) - } else { - return nil - } + return AdamantAddress( + address: address, + name: name, + amount: amount, + message: message + ) + } + + return nil } func addPrefixIfNeeded(prefix: String) -> String { diff --git a/Adamant/Helpers/String+localized.swift b/Adamant/Helpers/String+localized.swift index 9d0b2a613..f35bb4aa6 100644 --- a/Adamant/Helpers/String+localized.swift +++ b/Adamant/Helpers/String+localized.swift @@ -10,12 +10,6 @@ import Foundation import CommonKit extension String.adamant { - enum shared { - static var productName: String { - String.localized("ADAMANT", comment: "Product name") - } - } - enum alert { // MARK: Buttons static var cancel: String { @@ -75,70 +69,6 @@ extension String.adamant { } } - enum sharedErrors { - static var userNotLogged: String { - String.localized("Error.UserNotLogged", comment: "Shared error: User not logged") - } - static var networkError: String { - String.localized("Error.NoNetwork", comment: "Shared error: Network problems. In most cases - no connection") - } - static var requestCancelled: String { - String.localized("Error.RequestCancelled", comment: "Shared error: Request cancelled") - } - static func commonError(_ text: String) -> String { - String.localizedStringWithFormat( - .localized( - "Error.BaseErrorFormat", - comment: "Shared error: Base format, %@" - ), - text - ) - } - - static func accountNotFound(_ account: String) -> String { - String.localizedStringWithFormat(.localized("Error.AccountNotFoundFormat", comment: "Shared error: Account not found error. Using %@ for address."), account) - } - - static var accountNotInitiated: String { - String.localized("Error.AccountNotInitiated", comment: "Shared error: Account not initiated") - } - - static var unknownError: String { - String.localized("Error.UnknownError", comment: "Shared unknown error") - } - static func admNodeErrorMessage(_ coin: String) -> String { - String.localizedStringWithFormat(.localized("ApiService.InternalError.NoAdmNodesAvailable", comment: "No active ADM nodes to fetch the partner's %@ address"), coin) - } - - static var notEnoughMoney: String { - String.localized("WalletServices.SharedErrors.notEnoughMoney", comment: "Wallet Services: Shared error, user do not have enought money.") - } - - static var dustError: String { - String.localized("TransferScene.Dust.Error", comment: "Tranfser: Dust error.") - } - - static var transactionUnavailable: String { - String.localized("WalletServices.SharedErrors.transactionUnavailable", comment: "Wallet Services: Transaction unavailable") - } - - static var inconsistentTransaction: String { - String.localized("WalletServices.SharedErrors.inconsistentTransaction", comment: "Wallet Services: Cannot verify transaction") - } - - static var walletFrezzed: String { - String.localized("WalletServices.SharedErrors.walletFrezzed", comment: "Wallet Services: Wait until other transactions approved") - } - - static func internalError(message: String) -> String { - String.localizedStringWithFormat(.localized("Error.InternalErrorFormat", comment: "Shared error: Internal error format, %@ for message"), message) - } - - static func remoteServerError(message: String) -> String { - String.localizedStringWithFormat(.localized("Error.RemoteServerErrorFormat", comment: "Shared error: Remote error format, %@ for message"), message) - } - } - enum reply { static var shortUnknownMessageError: String { String.localized("Reply.ShortUnknownMessageError", comment: "Short unknown message error") diff --git a/Adamant/Models/APIResponseModel.swift b/Adamant/Models/APIResponseModel.swift deleted file mode 100644 index 2fe7c288b..000000000 --- a/Adamant/Models/APIResponseModel.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// 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/CoreData/Chatroom+CoreDataClass.swift b/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift index f1a5a78af..4fa20432b 100644 --- a/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift +++ b/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift @@ -14,6 +14,10 @@ import CoreData public class Chatroom: NSManagedObject { static let entityName = "Chatroom" + var hasUnread: Bool { + return hasUnreadMessages || (lastTransaction?.isUnread ?? false) + } + func markAsReaded() { hasUnreadMessages = false @@ -23,6 +27,11 @@ public class Chatroom: NSManagedObject { lastTransaction?.isUnread = false } + func markAsUnread() { + hasUnreadMessages = true + lastTransaction?.isUnread = true + } + func getFirstUnread() -> ChatTransaction? { if let trs = transactions as? Set { return trs.filter { $0.isUnread }.map { $0 }.first diff --git a/Adamant/Models/Delegate.swift b/Adamant/Models/Delegate.swift deleted file mode 100644 index 4c1869d7d..000000000 --- a/Adamant/Models/Delegate.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// Delegate.swift -// Adamant -// -// Created by Anton Boyarkin on 06/07/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation -import CommonKit - -final class Delegate: Decodable { - let username: String - let address: String - let publicKey: String - let voteObsolete: String - let voteFair: String - let producedblocks: Int - let missedblocks: Int - let rate: Int - let rank: Int - let approval: Double - let productivity: Double - - var voted: Bool = false - - enum CodingKeys: String, CodingKey { - case username - case address - case publicKey - case voteObsolete = "vote" - case voteFair = "votesWeight" - case producedblocks - case missedblocks - case rate - case rank - case approval - case productivity - } -} - -extension Delegate: WrappableModel { - static let ModelKey = "delegate" -} - -extension Delegate: WrappableCollection { - static let CollectionKey = "delegates" -} - -struct DelegateForgeDetails: Decodable { - let nodeTimestamp: Date - let fees: Decimal - let rewards: Decimal - let forged: Decimal - - enum CodingKeys: String, CodingKey { - case nodeTimestamp - case fees - case rewards - case forged - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let feesStr = try container.decode(String.self, forKey: .fees) - let fees = Decimal(string: feesStr) ?? 0 - self.fees = fees.shiftedFromAdamant() - - let rewardsStr = try container.decode(String.self, forKey: .forged) - let rewards = Decimal(string: rewardsStr) ?? 0 - self.rewards = rewards.shiftedFromAdamant() - - let forgedStr = try container.decode(String.self, forKey: .forged) - let forged = Decimal(string: forgedStr) ?? 0 - self.forged = forged.shiftedFromAdamant() - - let timestamp = try container.decode(UInt64.self, forKey: .nodeTimestamp) - self.nodeTimestamp = AdamantUtilities.decodeAdamant(timestamp: TimeInterval(timestamp)) - } -} - -struct DelegatesCountResult: Decodable { - let nodeTimestamp: UInt64 - let count: UInt -} - -struct NextForgersResult: Decodable { - let nodeTimestamp: Date - let currentBlock: UInt64 - let currentBlockSlot: UInt64 - let currentSlot: UInt64 - let delegates: [String] - - enum CodingKeys: String, CodingKey { - case nodeTimestamp - case currentBlock - case currentBlockSlot - case currentSlot - case delegates - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.currentBlock = try container.decode(UInt64.self, forKey: .currentBlock) - self.currentBlockSlot = try container.decode(UInt64.self, forKey: .currentBlockSlot) - self.currentSlot = try container.decode(UInt64.self, forKey: .currentSlot) - self.delegates = try container.decode([String].self, forKey: .delegates) - - let timestamp = try container.decode(UInt64.self, forKey: .nodeTimestamp) - self.nodeTimestamp = AdamantUtilities.decodeAdamant(timestamp: TimeInterval(timestamp)) - } -} - -struct Block: Decodable { - let id: String - let version: UInt - let timestamp: UInt64 - let height: UInt64 - let previousBlock:String - let numberOfTransactions: UInt - let totalAmount: UInt - let totalFee: UInt - let reward: UInt - let payloadLength: UInt - let payloadHash: String - let generatorPublicKey: String - let generatorId: String - let blockSignature: String - let confirmations: UInt - let totalForged: String -} - -extension Block: WrappableModel { - static let ModelKey = "block" -} - -extension Block: WrappableCollection { - static let CollectionKey = "blocks" -} - -/* -{ - "username": "permit", - "address": "U8339394976025567725", - "publicKey": "01c5079a2234f69feca1b00daf4ddbd8904e13dfb67ce47c21f26377468706fa", - "producedblocks": 11153, - "missedblocks": 3, - "vote": "13373617430543", - "votesWeight": "13373617430543", - "rate": 1, - "rank": 1, - "approval": 0.95, - "productivity": 99.97 -} -*/ diff --git a/Adamant/Models/MultipartFormDataModel.swift b/Adamant/Models/MultipartFormDataModel.swift deleted file mode 100644 index b77120115..000000000 --- a/Adamant/Models/MultipartFormDataModel.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// MultipartFormDataModel.swift -// Adamant -// -// Created by Stanislav Jelezoglo on 16.05.2024. -// Copyright © 2024 Adamant. All rights reserved. -// - -import Foundation - -struct MultipartFormDataModel { - let keyName: String - let fileName: String - let data: Data -} diff --git a/Adamant/Models/NodeStatusInfo.swift b/Adamant/Models/NodeStatusInfo.swift deleted file mode 100644 index 0046b8af4..000000000 --- a/Adamant/Models/NodeStatusInfo.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// 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 deleted file mode 100644 index ed4f4189f..000000000 --- a/Adamant/Models/NodeWithGroup.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// 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 .klyNode: - return KlyWalletService.tokenNetworkSymbol - case .klyService: - return KlyWalletService.tokenNetworkSymbol - + " " + .adamant.coinsNodesList.serviceNode - case .doge: - return DogeWalletService.tokenNetworkSymbol - case .dash: - return DashWalletService.tokenNetworkSymbol - case .adm: - return AdmWalletService.tokenNetworkSymbol - case .ipfs: - return IPFSApiService.symbol - } - } - - var includeVersionTitle: Bool { - switch self { - case .btc, .klyNode, .klyService, .doge, .adm: - return true - case .eth, .dash, .ipfs: - return false - } - } -} diff --git a/Adamant/Modules/Account/AccountFactory.swift b/Adamant/Modules/Account/AccountFactory.swift index cf356923d..8c5ac2a39 100644 --- a/Adamant/Modules/Account/AccountFactory.swift +++ b/Adamant/Modules/Account/AccountFactory.swift @@ -22,7 +22,7 @@ struct AccountFactory { transfersProvider: assembler.resolve(TransfersProvider.self)!, localAuth: assembler.resolve(LocalAuthentication.self)!, avatarService: assembler.resolve(AvatarService.self)!, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, languageService: assembler.resolve(LanguageStorageProtocol.self)!, walletServiceCompose: assembler.resolve(WalletServiceCompose.self)! ) diff --git a/Adamant/Modules/Account/AccountViewController.swift b/Adamant/Modules/Account/AccountViewController.swift index 0ff1df2f8..107bfe4a9 100644 --- a/Adamant/Modules/Account/AccountViewController.swift +++ b/Adamant/Modules/Account/AccountViewController.swift @@ -151,7 +151,7 @@ final class AccountViewController: FormViewController { private let notificationsService: NotificationsService private let transfersProvider: TransfersProvider private let avatarService: AvatarService - private let currencyInfoService: CurrencyInfoService + private let currencyInfoService: InfoServiceProtocol private let languageService: LanguageStorageProtocol private let walletServiceCompose: WalletServiceCompose @@ -169,7 +169,12 @@ final class AccountViewController: FormViewController { private var initiated = false - private var walletViewControllers = [WalletViewController]() + private var walletViewControllers: [WalletViewController] = [] { + didSet { + makeWalletModels() + } + } + private var notificationsSet: Set = [] // MARK: StayIn @@ -197,6 +202,8 @@ final class AccountViewController: FormViewController { return refreshControl }() + private var walletModels: [String: WalletItemModel] = [:] + // MARK: - Init init( @@ -208,7 +215,7 @@ final class AccountViewController: FormViewController { transfersProvider: TransfersProvider, localAuth: LocalAuthentication, avatarService: AvatarService, - currencyInfoService: CurrencyInfoService, + currencyInfoService: InfoServiceProtocol, languageService: LanguageStorageProtocol, walletServiceCompose: WalletServiceCompose ) { @@ -276,12 +283,11 @@ final class AccountViewController: FormViewController { let footerView = AccountFooterView(frame: CGRect(x: .zero, y: .zero, width: self.view.frame.width, height: 100)) tableView.tableFooterView = footerView - // MARK: Wallet pages setupWalletsVC() pagingViewController = PagingViewController() - pagingViewController.register(UINib(nibName: "WalletCollectionViewCell", bundle: nil), for: WalletPagingItem.self) + pagingViewController.register(UINib(nibName: "WalletCollectionViewCell", bundle: nil), for: WalletItemModel.self) pagingViewController.menuItemSize = .fixed(width: 110, height: 110) pagingViewController.indicatorColor = UIColor.adamant.primary pagingViewController.indicatorOptions = .visible(height: 2, zIndex: Int.max, spacing: UIEdgeInsets.zero, insets: UIEdgeInsets.zero) @@ -302,8 +308,18 @@ final class AccountViewController: FormViewController { pagingViewController.borderColor = UIColor.clear - let callback: ((Notification) -> Void) = { [weak self] _ in - self?.pagingViewController.reloadData() + let callback: ((Notification) -> Void) = { [weak self] data in + guard let account = data.userInfo?[AdamantUserInfoKey.WalletService.wallet] as? WalletAccount + else { + return + } + + var model = self?.walletModels[account.unicId]?.model + model?.balance = account.balance + model?.isBalanceInitialized = account.isBalanceInitialized + model?.notifications = account.notifications + + self?.walletModels[account.unicId]?.model = model ?? .default } for walletService in walletServiceCompose.getWallets() { @@ -1094,35 +1110,10 @@ extension AccountViewController: PagingViewControllerDataSource, PagingViewContr func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem { guard let service = walletViewControllers[index].service?.core else { - return WalletPagingItem( - index: index, - currencySymbol: "", - currencyImage: .asset(named: "adamant_wallet") ?? .init(), - isBalanceInitialized: false) - } - - var network = "" - if ERC20Token.supportedTokens.contains(where: { token in - return token.symbol == service.tokenSymbol - }) { - network = type(of: service).tokenNetworkSymbol - } - - let item = WalletPagingItem( - index: index, - currencySymbol: service.tokenSymbol, - currencyImage: service.tokenLogo, - isBalanceInitialized: service.wallet?.isBalanceInitialized, - currencyNetwork: network) - - if let wallet = service.wallet { - item.balance = wallet.balance - item.notifications = wallet.notifications - } else { - item.balance = nil + return WalletItemModel(model: .default) } - return item + return walletModels[service.tokenUnicID] ?? WalletItemModel(model: .default) } func pagingViewController(_ pagingViewController: PagingViewController, didScrollToItem pagingItem: PagingItem, startingViewController: UIViewController?, destinationViewController: UIViewController, transitionSuccessful: Bool) { @@ -1179,3 +1170,38 @@ extension AccountViewController: WalletViewControllerDelegate { } } } + +private extension AccountViewController { + func makeWalletModels() { + for (index, wallet) in walletViewControllers.enumerated() { + guard let service = wallet.service?.core else { + continue + } + + var network = "" + if ERC20Token.supportedTokens.contains(where: { token in + return token.symbol == service.tokenSymbol + }) { + network = type(of: service).tokenNetworkSymbol + } + + var item = WalletItem( + index: index, + currencySymbol: service.tokenSymbol, + currencyImage: service.tokenLogo, + isBalanceInitialized: service.wallet?.isBalanceInitialized, + currencyNetwork: network + ) + + if let wallet = service.wallet { + item.balance = wallet.balance + item.notifications = wallet.notifications + } else { + item.balance = nil + } + + let model = WalletItemModel(model: item) + walletModels[service.tokenUnicID] = model + } + } +} diff --git a/Adamant/Modules/Account/WalletCollectionViewCell.swift b/Adamant/Modules/Account/WalletCollectionViewCell.swift index 5505a01b4..c96b44244 100644 --- a/Adamant/Modules/Account/WalletCollectionViewCell.swift +++ b/Adamant/Modules/Account/WalletCollectionViewCell.swift @@ -10,6 +10,7 @@ import UIKit import FreakingSimpleRoundImageView import Parchment import CommonKit +import Combine class WalletCollectionViewCell: PagingCell { @IBOutlet weak var currencyImageView: UIImageView! @@ -17,11 +18,35 @@ class WalletCollectionViewCell: PagingCell { @IBOutlet weak var currencySymbolLabel: UILabel! @IBOutlet weak var accessoryContainerView: AccessoryContainerView! - override func setPagingItem(_ pagingItem: PagingItem, selected: Bool, options: PagingOptions) { - guard let item = pagingItem as? WalletPagingItem else { + private var cancellables = Set() + + override func prepareForReuse() { + cancellables.removeAll() + } + + override func setPagingItem( + _ pagingItem: PagingItem, + selected: Bool, + options: PagingOptions + ) { + guard let item = pagingItem as? WalletItemModel else { return } + update(item: item.model) + + cancellables.removeAll() + item.$model + .removeDuplicates() + .sink { [weak self] item in + self?.update(item: item) + } + .store(in: &cancellables) + } +} + +private extension WalletCollectionViewCell { + func update(item: WalletItem) { currencyImageView.image = item.currencyImage if item.currencyNetwork.isEmpty { currencySymbolLabel.text = item.currencySymbol diff --git a/Adamant/Modules/Account/WalletPagingItem.swift b/Adamant/Modules/Account/WalletPagingItem.swift index 0106fabbc..c77101c67 100644 --- a/Adamant/Modules/Account/WalletPagingItem.swift +++ b/Adamant/Modules/Account/WalletPagingItem.swift @@ -8,14 +8,15 @@ import UIKit import Parchment +import CommonKit +import Combine -class WalletPagingItem: PagingItem, Hashable, Comparable { - let index: Int - let currencySymbol: String - let currencyImage: UIImage - let currencyNetwork: String - let isBalanceInitialized: Bool - +struct WalletItem: Equatable { + var index: Int + var currencySymbol: String + var currencyImage: UIImage + var currencyNetwork: String + var isBalanceInitialized: Bool var balance: Decimal? var notifications: Int = 0 @@ -24,31 +25,45 @@ class WalletPagingItem: PagingItem, Hashable, Comparable { currencySymbol symbol: String, currencyImage image: UIImage, isBalanceInitialized: Bool?, - currencyNetwork network: String = "" + currencyNetwork network: String = .empty, + balance: Decimal? = nil ) { self.index = index self.isBalanceInitialized = isBalanceInitialized ?? false self.currencySymbol = symbol self.currencyImage = image self.currencyNetwork = network + self.balance = balance } - // MARK: Hashable, Comparable - var hashValue: Int { - return index.hashValue &+ currencySymbol.hashValue + static let `default` = Self( + index: .zero, + currencySymbol: .empty, + currencyImage: .init(), + isBalanceInitialized: nil + ) +} + +final class WalletItemModel: ObservableObject, PagingItem, Hashable, Comparable { + @Published var model: WalletItem = .default + + init(model: WalletItem) { + self.model = model } + // MARK: Hashable, Comparable + func hash(into hasher: inout Hasher) { - hasher.combine(index) - hasher.combine(currencySymbol) + hasher.combine(model.index) + hasher.combine(model.currencySymbol) } - static func < (lhs: WalletPagingItem, rhs: WalletPagingItem) -> Bool { - return lhs.index < rhs.index + static func < (lhs: WalletItemModel, rhs: WalletItemModel) -> Bool { + lhs.model.index < rhs.model.index } - static func == (lhs: WalletPagingItem, rhs: WalletPagingItem) -> Bool { - return lhs.index == rhs.index && - lhs.currencySymbol == rhs.currencySymbol + static func == (lhs: WalletItemModel, rhs: WalletItemModel) -> Bool { + lhs.model.index == rhs.model.index && + lhs.model.currencySymbol == rhs.model.currencySymbol } } diff --git a/Adamant/Modules/Chat/ChatFactory.swift b/Adamant/Modules/Chat/ChatFactory.swift index 0ab315a81..e07d30c44 100644 --- a/Adamant/Modules/Chat/ChatFactory.swift +++ b/Adamant/Modules/Chat/ChatFactory.swift @@ -13,6 +13,7 @@ import Combine import Swinject import FilesStorageKit import FilesPickerKit +import CommonKit @MainActor struct ChatFactory { @@ -32,7 +33,7 @@ struct ChatFactory { let filesStorage: FilesStorageProtocol let chatFileService: ChatFileProtocol let filesStorageProprieties: FilesStorageProprietiesProtocol - let nodesStorage: NodesStorageProtocol + let apiServiceCompose: ApiServiceComposeProtocol let reachabilityMonitor: ReachabilityMonitor let filesPickerKit: FilesPickerProtocol @@ -52,7 +53,7 @@ struct ChatFactory { filesStorage = assembler.resolve(FilesStorageProtocol.self)! chatFileService = assembler.resolve(ChatFileProtocol.self)! filesStorageProprieties = assembler.resolve(FilesStorageProprietiesProtocol.self)! - nodesStorage = assembler.resolve(NodesStorageProtocol.self)! + apiServiceCompose = assembler.resolve(ApiServiceComposeProtocol.self)! reachabilityMonitor = assembler.resolve(ReachabilityMonitor.self)! filesPickerKit = assembler.resolve(FilesPickerProtocol.self)! } @@ -131,8 +132,8 @@ private extension ChatFactory { filesStorage: filesStorage, chatFileService: chatFileService, filesStorageProprieties: filesStorageProprieties, - nodesStorage: nodesStorage, - reachabilityMonitor: reachabilityMonitor, + apiServiceCompose: apiServiceCompose, + reachabilityMonitor: reachabilityMonitor, filesPicker: filesPickerKit ) } diff --git a/Adamant/Modules/Chat/View/ChatViewController.swift b/Adamant/Modules/Chat/View/ChatViewController.swift index dc38bad0a..9bc33b991 100644 --- a/Adamant/Modules/Chat/View/ChatViewController.swift +++ b/Adamant/Modules/Chat/View/ChatViewController.swift @@ -50,6 +50,11 @@ final class ChatViewController: MessagesViewController { private lazy var replyView = ReplyView() private lazy var filesToolbarView = FilesToolbarView() private lazy var chatDropView = ChatDropView() + private lazy var dateHeaderLabel = EdgeInsetLabel( + font: .adamantPrimary(ofSize: 13), + textColor: .adamant.textColor, + numberOfLines: 1 + ) private var sendTransaction: SendTransaction @@ -189,11 +194,24 @@ final class ChatViewController: MessagesViewController { super.collectionView(collectionView, willDisplay: cell, forItemAt: indexPath) } + override func scrollViewDidEndDecelerating(_: UIScrollView) { + scrollDidStop() + } + + override func scrollViewDidEndDragging(_: UIScrollView, willDecelerate: Bool) { + guard !willDecelerate else { return } + scrollDidStop() + } + override func scrollViewDidScroll(_ scrollView: UIScrollView) { super.scrollViewDidScroll(scrollView) updateIsScrollPositionNearlyTheBottom() updateScrollDownButtonVisibility() + if scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating { + updateDateHeaderIfNeeded() + } + guard viewAppeared, scrollView.contentOffset.y <= viewModel.minOffsetForStartLoadNewMessages @@ -269,6 +287,10 @@ extension ChatViewController { // MARK: Observers private extension ChatViewController { + func scrollDidStop() { + viewModel.startHideDateTimer() + } + func setupObservers() { NotificationCenter.default .publisher(for: UITextView.textDidChangeNotification, object: inputBar.inputTextView) @@ -356,6 +378,7 @@ private extension ChatViewController { .sink { [weak self] in if $0 { self?.updatingIndicatorView.startAnimate() + self?.viewModel.refreshDateHeadersIfNeeded() } else { self?.updatingIndicatorView.stopAnimate() } @@ -391,6 +414,16 @@ private extension ChatViewController { .sink { [weak self] in self?.animateScroll(isStarted: $0) } .store(in: &subscriptions) + viewModel.$dateHeader + .removeDuplicates() + .sink { [weak self] in self?.dateHeaderLabel.text = $0 } + .store(in: &subscriptions) + + viewModel.$dateHeaderHidden + .removeDuplicates() + .sink { [weak self] in self?.dateHeaderLabel.isHidden = $0 } + .store(in: &subscriptions) + viewModel.updateChatRead .sink { [weak self] in self?.checkIsChatWasRead() } .store(in: &subscriptions) @@ -492,6 +525,16 @@ private extension ChatViewController { navigationItem.titleView?.addGestureRecognizer(tapGesture) navigationItem.titleView?.addGestureRecognizer(longPressGesture) + + view.addSubview(dateHeaderLabel) + dateHeaderLabel.backgroundColor = .adamant.chatSenderBackground + dateHeaderLabel.textInsets = .init(top: 4, left: 7, bottom: 4, right: 7) + dateHeaderLabel.layer.cornerRadius = 10 + dateHeaderLabel.clipsToBounds = true + dateHeaderLabel.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide).offset(10) + make.centerX.equalToSuperview() + } } func configureHeaderRightButton() { @@ -669,6 +712,27 @@ private extension ChatViewController { func updateScrollDownButtonVisibility() { scrollDownButton.isHidden = isScrollPositionNearlyTheBottom } + + func updateDateHeaderIfNeeded() { + guard viewAppeared else { return } + + let targetY: CGFloat = targetYOffset + view.safeAreaInsets.top + let visibleIndexPaths = messagesCollectionView.indexPathsForVisibleItems + + for indexPath in visibleIndexPaths { + guard let cell = messagesCollectionView.cellForItem(at: indexPath) + else { continue } + + let cellRect = messagesCollectionView.convert(cell.frame, to: self.view) + + guard cellRect.minY <= targetY && cellRect.maxY >= targetY else { + continue + } + + viewModel.checkTopMessage(indexPath: indexPath) + break + } + } } // MARK: Making entities @@ -1032,3 +1096,4 @@ private var replyAction: Bool = false private var canReplyVibrate: Bool = true private var oldContentOffset: CGPoint? private let filesToolbarViewHeight: CGFloat = 140 +private let targetYOffset: CGFloat = 20 diff --git a/Adamant/Modules/Chat/View/Helpers/ChatReactionsView.swift b/Adamant/Modules/Chat/View/Helpers/ChatReactionsView.swift index ea402d3f5..1e9eeca8a 100644 --- a/Adamant/Modules/Chat/View/Helpers/ChatReactionsView.swift +++ b/Adamant/Modules/Chat/View/Helpers/ChatReactionsView.swift @@ -17,7 +17,7 @@ protocol ChatReactionsViewDelegate: AnyObject { struct ChatReactionsView: View { private let emojis: [String] - private let defaultEmojis = ["😂", "🤔", "😁", "👍", "👌"] + private let defaultEmojis = ["😂", "🤔", "😁", "👍", "👌", "🤝"] private let selectedEmoji: String? private let messageId: String @@ -37,8 +37,8 @@ struct ChatReactionsView: View { var body: some View { HStack(spacing: 10) { ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 10) { - ForEach(emojis, id: \.self) { emoji in + HStack(spacing: 5) { + ForEach(emojis.prefix(6), id: \.self) { emoji in ChatReactionButton( emoji: emoji ) diff --git a/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift b/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift index f2055198e..697e6a294 100644 --- a/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift +++ b/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift @@ -31,7 +31,7 @@ enum FileMessageStatus: Equatable { var imageTintColor: UIColor { switch self { case .busy, .needToDownload, .success: return .adamant.primary - case .failed: return .adamant.alert + case .failed: return .adamant.attention } } } diff --git a/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift index 3dbb6e878..2d3fcc368 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift @@ -503,7 +503,7 @@ private extension ChatDialogManager { } func getUpperContentViewSize() -> CGSize { - .init(width: 310, height: 50) + .init(width: 335, height: 50) } func getUpperContentView( diff --git a/Adamant/Modules/Chat/View/Managers/ChatLayoutManager.swift b/Adamant/Modules/Chat/View/Managers/ChatLayoutManager.swift index f4d9e97a5..9f6af55bf 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatLayoutManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatLayoutManager.swift @@ -29,7 +29,7 @@ final class ChatLayoutManager: MessagesLayoutDelegate { at indexPath: IndexPath, in _: MessagesCollectionView ) -> CGFloat { - message.fullModel.dateHeader == nil + message.fullModel.dateHeaderIsHidden ? .zero : labelHeight } diff --git a/Adamant/Modules/Chat/View/Managers/FixedTextMessageSizeCalculator.swift b/Adamant/Modules/Chat/View/Managers/FixedTextMessageSizeCalculator.swift index b79e334fe..9d8c6013a 100644 --- a/Adamant/Modules/Chat/View/Managers/FixedTextMessageSizeCalculator.swift +++ b/Adamant/Modules/Chat/View/Managers/FixedTextMessageSizeCalculator.swift @@ -81,9 +81,16 @@ } if case let .file(model) = getMessages()[indexPath.section].fullModel.content { - let contentViewHeight: CGFloat = model.value.height() + let messageSize = labelSize( + for: model.value.content.comment, + considering: model.value.content.width() + - commenthorizontalInsets * 2 + ) + + let contentViewHeight = model.value.height() messageContainerSize.width = maxWidth messageContainerSize.height = contentViewHeight + + messageSize.height } return messageContainerSize @@ -131,6 +138,8 @@ for attributedText: NSAttributedString, considering maxWidth: CGFloat ) -> CGSize { + guard !attributedText.string.isEmpty else { return .zero } + let textContainer = NSTextContainer( size: CGSize(width: maxWidth, height: .greatestFiniteMagnitude) ) @@ -180,3 +189,4 @@ /// Additional width to fix incorrect size calculating private let additionalWidth: CGFloat = 5 private let additionalHeight: CGFloat = 5 +private let commenthorizontalInsets: CGFloat = 12 diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView.swift index 8cb0ffcaf..a25aaf33c 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView.swift @@ -247,7 +247,20 @@ private extension ChatMediaContentView { } } +extension ChatMediaContentView.FileModel { + func width() -> CGFloat { + guard UIDevice.current.userInterfaceIdiom == .phone else { + return defaultStackWidth + } + return UIScreen.main.bounds.width - screenSpace + } +} + extension ChatMediaContentView.Model { + func width() -> CGFloat { + fileModel.width() + } + func height() -> CGFloat { let replyViewDynamicHeight: CGFloat = isReply ? replyViewHeight : .zero @@ -258,41 +271,18 @@ extension ChatMediaContentView.Model { } if !comment.string.isEmpty { - spaceCount += 2 + spaceCount += 3 } - let stackWidth = MediaContainerView.stackWidth - return fileModel.height() + spaceCount * verticalInsets - + labelSize(for: comment, considering: stackWidth - horizontalInsets * 2).height + replyViewDynamicHeight } - - func labelSize( - for attributedText: NSAttributedString, - considering maxWidth: CGFloat - ) -> CGSize { - guard !attributedText.string.isEmpty else { return .zero } - - let textContainer = NSTextContainer( - size: CGSize(width: maxWidth, height: .greatestFiniteMagnitude) - ) - let layoutManager = NSLayoutManager() - - layoutManager.addTextContainer(textContainer) - - let textStorage = NSTextStorage(attributedString: attributedText) - textStorage.addLayoutManager(layoutManager) - - let rect = layoutManager.usedRect(for: textContainer) - - return .init(width: rect.width, height: rect.height + additionalHeight) - } } private let verticalInsets: CGFloat = 8 private let horizontalInsets: CGFloat = 12 private let replyViewHeight: CGFloat = 25 -private let additionalHeight: CGFloat = 2 private let imageSize: CGFloat = 70 +private let defaultStackWidth: CGFloat = 280 +private let screenSpace: CGFloat = 110 diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContainerView.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContainerView.swift index acdeb9cbf..f99b71a67 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContainerView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContainerView.swift @@ -55,11 +55,6 @@ final class MediaContainerView: UIView { var actionHandler: (ChatAction) -> Void = { _ in } - static var stackWidth: CGFloat { - guard !isMacOS else { return defaultStackWidth } - return UIScreen.main.bounds.width - screenSpace - } - // MARK: - Init override init(frame: CGRect) { @@ -80,7 +75,7 @@ private extension MediaContainerView { addSubview(filesStack) filesStack.snp.makeConstraints { $0.directionalEdges.equalToSuperview() - $0.width.equalTo(Self.stackWidth) + $0.width.equalTo(model.width()) } addSubview(previewDownloadNotAllowedLabel) @@ -171,7 +166,7 @@ private extension MediaContainerView { isHorizontal: Bool, fileList: [ChatFile] ) { - let filesStackWidth = Self.stackWidth + let filesStackWidth = model.width() let minimumWidth = calculateMinimumWidth(availableWidth: filesStackWidth) let maximumWidth = calculateMaximumWidth(availableWidth: filesStackWidth) diff --git a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift index 4ffd6ab26..5cd22dc49 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift @@ -61,6 +61,10 @@ final class ChatTransactionContainerView: UIView, ChatModelView { stack.addArrangedSubview(statusButton) stack.addArrangedSubview(ownReactionLabel) stack.addArrangedSubview(opponentReactionLabel) + + stack.snp.makeConstraints { + $0.width.equalTo(Self.maxVStackWidth) + } return stack }() @@ -118,11 +122,11 @@ final class ChatTransactionContainerView: UIView, ChatModelView { private lazy var chatMenuManager = ChatMenuManager(delegate: self) private let ownReactionSize = CGSize(width: 40, height: 27) - private let opponentReactionSize = CGSize(width: opponentReactionWidth, height: 27) + private let opponentReactionSize = CGSize(width: maxVStackWidth, height: 27) private let opponentReactionImageSize = CGSize(width: 12, height: 12) - static let opponentReactionWidth: CGFloat = 55 static let horizontalStackSpacing: CGFloat = 12 + static let maxVStackWidth: CGFloat = 55 var isSelected: Bool = false { didSet { @@ -278,7 +282,7 @@ private extension TransactionStatus { case .notInitiated: return .adamant.secondary case .pending, .registered, .noNetwork, .noNetworkFinal: return .adamant.primary case .success: return .adamant.active - case .failed, .inconsistent: return .adamant.alert + case .failed, .inconsistent: return .adamant.attention } } } @@ -343,6 +347,7 @@ extension ChatTransactionContainerView { view.contentView.model = model.content view.updateStatus(model.status) view.updateLayout() + view.contentView.setFixWidth(width: contentView.frame.width) return view } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift index dd7079104..662660947 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift @@ -125,11 +125,17 @@ final class ChatTransactionContentView: UIView { super.init(coder: coder) configure() } + + func setFixWidth(width: CGFloat) { + snp.remakeConstraints { + $0.width.lessThanOrEqualTo(width) + } + } } extension ChatTransactionContentView.Model { func height(for width: CGFloat) -> CGFloat { - let opponentReactionWidth = ChatTransactionContainerView.opponentReactionWidth + let opponentReactionWidth = ChatTransactionContainerView.maxVStackWidth let containerHorizontalOffset = ChatTransactionContainerView.horizontalStackSpacing * 2 let contentHorizontalOffset = horizontalInsets * 2 diff --git a/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarView.swift b/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarView.swift index 8fe5294ba..03b98f85c 100644 --- a/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarView.swift +++ b/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarView.swift @@ -41,7 +41,7 @@ final class FilesToolbarView: UIView { private lazy var closeBtn: UIButton = { let btn = UIButton() btn.setImage( - UIImage(systemName: "xmark")?.withTintColor(.adamant.alert), + UIImage(systemName: "xmark")?.withTintColor(.adamant.attention), for: .normal ) btn.addTarget(self, action: #selector(didTapCloseBtn), for: .touchUpInside) diff --git a/Adamant/Modules/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Modules/Chat/ViewModel/ChatMessageFactory.swift index 13fff642a..708325a1a 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatMessageFactory.swift @@ -108,10 +108,9 @@ struct ChatMessageFactory { status: status, expireDate: &expireDate ).map { .init(string: $0) }, - dateHeader: dateHeaderOn - ? makeDateHeader(sentDate: sentDate) - : nil, - topSpinnerOn: topSpinnerOn + dateHeader: makeDateHeader(sentDate: sentDate), + topSpinnerOn: topSpinnerOn, + dateHeaderIsHidden: !dateHeaderOn ) } } @@ -261,7 +260,7 @@ private extension ChatMessageFactory { : transaction.senderAddress let coreService = walletServiceCompose.getWallet(by: transfer.type)?.core - let defaultIcon: UIImage = .asset(named: "no-token") ?? .init() + let defaultIcon: UIImage = .asset(named: "no-token")?.withTintColor(.adamant.primary) ?? .init() return .transaction(.init(value: .init( id: id, @@ -521,7 +520,7 @@ private extension ChatMessageFactory { func makeDateHeader(sentDate: Date) -> ComparableAttributedString { .init(string: .init( - string: sentDate.humanizedDay(), + string: sentDate.humanizedDay(useTimeFormat: false), attributes: [ .font: UIFont.boldSystemFont(ofSize: 10), .foregroundColor: UIColor.adamant.secondary diff --git a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift index edc8eae10..a068075df 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift @@ -37,7 +37,7 @@ final class ChatViewModel: NSObject { private let filesStorage: FilesStorageProtocol private let chatFileService: ChatFileProtocol private let filesStorageProprieties: FilesStorageProprietiesProtocol - private let nodesStorage: NodesStorageProtocol + private let apiServiceCompose: ApiServiceComposeProtocol private let reachabilityMonitor: ReachabilityMonitor private let filesPicker: FilesPickerProtocol @@ -65,12 +65,15 @@ final class ChatViewModel: NSObject { private(set) var chatroom: Chatroom? private(set) var chatTransactions: [ChatTransaction] = [] private var tempCancellables = Set() + private var hideHeaderTimer: AnyCancellable? private let minDiffCountForOffset = 5 private let minDiffCountForAnimateScroll = 20 private let partnerImageSize: CGFloat = 25 private let maxMessageLenght: Int = 10000 private var previousArg: ChatContextMenuArguments? + private var lastDateHeaderUpdate: Date = Date() private var havePartnerName: Bool = false + private let delayHideHeaderInSeconds: Double = 2.0 let minIndexForStartLoadNewMessages = 4 let minOffsetForStartLoadNewMessages: CGFloat = 100 @@ -104,6 +107,8 @@ final class ChatViewModel: NSObject { @ObservableValue private(set) var partnerName: String? @ObservableValue private(set) var partnerImage: UIImage? @ObservableValue private(set) var isNeedToAnimateScroll = false + @ObservableValue private(set) var dateHeader: String? + @ObservableValue private(set) var dateHeaderHidden: Bool = true @ObservableValue var swipeState: SwipeableView.State = .ended @ObservableValue var inputText = "" @ObservableValue var replyMessage: MessageModel? @@ -162,7 +167,7 @@ final class ChatViewModel: NSObject { filesStorage: FilesStorageProtocol, chatFileService: ChatFileProtocol, filesStorageProprieties: FilesStorageProprietiesProtocol, - nodesStorage: NodesStorageProtocol, + apiServiceCompose: ApiServiceComposeProtocol, reachabilityMonitor: ReachabilityMonitor, filesPicker: FilesPickerProtocol ) { @@ -184,7 +189,7 @@ final class ChatViewModel: NSObject { self.filesStorage = filesStorage self.chatFileService = chatFileService self.filesStorageProprieties = filesStorageProprieties - self.nodesStorage = nodesStorage + self.apiServiceCompose = apiServiceCompose self.reachabilityMonitor = reachabilityMonitor self.filesPicker = filesPicker @@ -288,9 +293,9 @@ final class ChatViewModel: NSObject { return } - guard nodesStorage.haveActiveNode(in: .adm) else { + guard apiServiceCompose.hasActiveNode(group: .adm) else { dialog.send(.alert(ApiServiceError.noEndpointsAvailable( - coin: NodeGroup.adm.name + nodeGroupName: NodeGroup.adm.name ).localizedDescription)) return } @@ -703,9 +708,9 @@ final class ChatViewModel: NSObject { return false } - guard nodesStorage.haveActiveNode(in: .adm) else { + guard apiServiceCompose.hasActiveNode(group: .adm) else { dialog.send(.alert(ApiServiceError.noEndpointsAvailable( - coin: NodeGroup.adm.name + nodeGroupName: NodeGroup.adm.name ).localizedDescription)) return false } @@ -713,6 +718,17 @@ final class ChatViewModel: NSObject { return true } + /// If the user opens the app from the background + /// update messages to refresh the header dates. + func refreshDateHeadersIfNeeded() { + guard !Calendar.current.isDate(Date(), inSameDayAs: lastDateHeaderUpdate) else { + return + } + + lastDateHeaderUpdate = Date() + updateMessages(resetLoadingProperty: false) + } + func openFile(messageId: String, file: ChatFile) { let tx = chatTransactions.first(where: { $0.txId == messageId }) let message = messages.first(where: { $0.messageId == messageId }) @@ -1014,6 +1030,28 @@ extension ChatViewModel { processFileResult(.failure(error)) } } + + func checkTopMessage(indexPath: IndexPath) { + guard let message = messages[safe: indexPath.section], + let date = message.dateHeader?.string.string, + message.sentDate != .adamantNullDate + else { return } + dateHeader = date + dateHeaderHidden = false + hideHeaderTimer?.cancel() + hideHeaderTimer = nil + } + + func startHideDateTimer() { + hideHeaderTimer?.cancel() + hideHeaderTimer = Timer + .publish(every: delayHideHeaderInSeconds, on: .main, in: .common) + .autoconnect() + .first() + .sink { [weak self] _ in + self?.dateHeaderHidden = true + } + } } extension ChatViewModel: NSFetchedResultsControllerDelegate { @@ -1024,9 +1062,9 @@ extension ChatViewModel: NSFetchedResultsControllerDelegate { private extension ChatViewModel { func sendFiles(with text: String) async throws { - guard nodesStorage.haveActiveNode(in: .ipfs) else { + guard apiServiceCompose.hasActiveNode(group: .ipfs) else { dialog.send(.alert(ApiServiceError.noEndpointsAvailable( - coin: NodeGroup.ipfs.name + nodeGroupName: NodeGroup.ipfs.name ).localizedDescription)) return } diff --git a/Adamant/Modules/Chat/ViewModel/Models/ChatMessage.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatMessage.swift index 115e9065f..0e7bcb115 100644 --- a/Adamant/Modules/Chat/ViewModel/Models/ChatMessage.swift +++ b/Adamant/Modules/Chat/ViewModel/Models/ChatMessage.swift @@ -20,6 +20,7 @@ struct ChatMessage: Identifiable, Equatable { let bottomString: ComparableAttributedString? let dateHeader: ComparableAttributedString? let topSpinnerOn: Bool + let dateHeaderIsHidden: Bool static let `default` = Self( id: "", @@ -30,7 +31,8 @@ struct ChatMessage: Identifiable, Equatable { backgroundColor: .failed, bottomString: nil, dateHeader: nil, - topSpinnerOn: false + topSpinnerOn: false, + dateHeaderIsHidden: true ) } diff --git a/Adamant/Modules/ChatsList/ChatListFactory.swift b/Adamant/Modules/ChatsList/ChatListFactory.swift index ad84877f5..b31502e58 100644 --- a/Adamant/Modules/ChatsList/ChatListFactory.swift +++ b/Adamant/Modules/ChatsList/ChatListFactory.swift @@ -8,6 +8,7 @@ import UIKit import Swinject +import CommonKit struct ChatListFactory { let assembler: Assembler diff --git a/Adamant/Modules/ChatsList/ChatListViewController.swift b/Adamant/Modules/ChatsList/ChatListViewController.swift index 27039571e..f3c3b9f48 100644 --- a/Adamant/Modules/ChatsList/ChatListViewController.swift +++ b/Adamant/Modules/ChatsList/ChatListViewController.swift @@ -129,6 +129,7 @@ final class ChatListViewController: KeyboardObservingViewController { private var onMessagesLoadedActions = [() -> Void]() private var areMessagesLoaded = false + private var lastDatesUpdate: Date = Date() // MARK: Tasks @@ -347,9 +348,23 @@ final class ChatListViewController: KeyboardObservingViewController { if case .updating = newState { updatingIndicatorView.startAnimate() + refreshDatesIfNeeded() } } + /// If the user opens the app from the background and new chats are not loaded, + /// update specific rows in the tableView to refresh the dates. + private func refreshDatesIfNeeded() { + guard !isBusy, + let indexPaths = tableView.indexPathsForVisibleRows + else { + return + } + + lastDatesUpdate = Date() + tableView.reloadRows(at: indexPaths, with: .none) + } + private func updateChats() { guard accountService.account?.address != nil, accountService.keypair?.privateKey != nil @@ -666,7 +681,7 @@ extension ChatListViewController { } if let date = chatroom.updatedAt as Date?, date != .adamantNullDate { - cell.dateLabel.text = date.humanizedDay() + cell.dateLabel.text = date.humanizedDay(useTimeFormat: true) } else { cell.dateLabel.text = nil } @@ -1058,12 +1073,6 @@ extension ChatListViewController { let more = makeMooreContextualAction(for: chatroom) actions.append(more) - // Mark as read - if chatroom.hasUnreadMessages || (chatroom.lastTransaction?.isUnread ?? false) { - let markAsRead = makeMarkAsReadContextualAction(for: chatroom) - actions.append(markAsRead) - } - // Block let block = makeBlockContextualAction(for: chatroom) actions.append(block) @@ -1071,6 +1080,22 @@ extension ChatListViewController { return UISwipeActionsConfiguration(actions: actions) } + func tableView( + _ tableView: UITableView, + leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath + ) -> UISwipeActionsConfiguration? { + guard let chatroom = chatsController?.fetchedObjects?[safe: indexPath.row] else { + return nil + } + + var actions: [UIContextualAction] = [] + + let markAsRead = makeMarkAsReadContextualAction(for: chatroom) + actions.append(markAsRead) + + return UISwipeActionsConfiguration(actions: actions) + } + private func blockChat(with address: String, for chatroom: Chatroom?) { Task { chatroom?.isHidden = true @@ -1112,7 +1137,8 @@ extension ChatListViewController { ) } - block.image = .asset(named: "swipe_block") + block.image = .asset(named: "swipe_block")?.withTintColor(.adamant.warning, renderingMode: .alwaysOriginal) + block.backgroundColor = .adamant.swipeBlockColor return block } @@ -1120,15 +1146,18 @@ extension ChatListViewController { private func makeMarkAsReadContextualAction(for chatroom: Chatroom) -> UIContextualAction { let markAsRead = UIContextualAction( style: .normal, - title: nil + title: "👀" ) { (_, _, completionHandler) in - chatroom.markAsReaded() + if chatroom.hasUnread { + chatroom.markAsReaded() + } else { + chatroom.markAsUnread() + } try? chatroom.managedObjectContext?.save() completionHandler(true) } - - markAsRead.image = .asset(named: "swipe_mark-as-read") - markAsRead.backgroundColor = UIColor.adamant.primary + + markAsRead.backgroundColor = UIColor.adamant.contextMenuDefaultBackgroundColor return markAsRead } @@ -1181,14 +1210,19 @@ extension ChatListViewController { return } + let closeAction: (() -> Void)? = { [completionHandler] in + completionHandler(true) + } + let share = self.makeShareAction( for: address, encodedAddress: encodedAddress, - sender: view + sender: view, + completion: closeAction ) - let rename = self.makeRenameAction(for: address) - let cancel = self.makeCancelAction() + let rename = self.makeRenameAction(for: address, completion: closeAction) + let cancel = self.makeCancelAction(completion: closeAction) self.dialogService.showAlert( title: nil, @@ -1197,19 +1231,18 @@ extension ChatListViewController { actions: [share, rename, cancel], from: .view(view) ) - - completionHandler(true) } more.image = .asset(named: "swipe_more") - more.backgroundColor = .adamant.secondary + more.backgroundColor = .adamant.swipeMoreColor return more } private func makeShareAction( for address: String, encodedAddress: String, - sender: UIView + sender: UIView, + completion: (() -> Void)? = nil ) -> UIAlertAction { .init( title: ShareType.share.localized, @@ -1231,12 +1264,15 @@ extension ChatListViewController { excludedActivityTypes: ShareContentType.address.excludedActivityTypes, animated: true, from: sender, - completion: nil + completion: completion ) } } - private func makeRenameAction(for address: String) -> UIAlertAction { + private func makeRenameAction( + for address: String, + completion: (() -> Void)? = nil + ) -> UIAlertAction { .init( title: .adamant.chat.rename, style: .default @@ -1244,6 +1280,7 @@ extension ChatListViewController { guard let alert = self?.makeRenameAlert(for: address) else { return } self?.dialogService.present(alert, animated: true) { self?.dialogService.selectAllTextFields(in: alert) + completion?() } } } @@ -1282,8 +1319,13 @@ extension ChatListViewController { return alert } - private func makeCancelAction() -> UIAlertAction { - .init(title: .adamant.alert.cancel, style: .cancel, handler: nil) + private func makeCancelAction(completion: (() -> Void)? = nil) -> UIAlertAction { + .init( + title: .adamant.alert.cancel, + style: .cancel + ) { _ in + completion?() + } } } diff --git a/Adamant/Modules/ChatsList/ComplexTransferViewController.swift b/Adamant/Modules/ChatsList/ComplexTransferViewController.swift index 1eda62f01..bb31fb215 100644 --- a/Adamant/Modules/ChatsList/ComplexTransferViewController.swift +++ b/Adamant/Modules/ChatsList/ComplexTransferViewController.swift @@ -68,7 +68,7 @@ final class ComplexTransferViewController: UIViewController { // MARK: PagingViewController pagingViewController = PagingViewController() - pagingViewController.register(UINib(nibName: "WalletCollectionViewCell", bundle: nil), for: WalletPagingItem.self) + pagingViewController.register(UINib(nibName: "WalletCollectionViewCell", bundle: nil), for: WalletItemModel.self) pagingViewController.menuItemSize = .fixed(width: 110, height: 114) pagingViewController.indicatorColor = UIColor.adamant.primary pagingViewController.indicatorOptions = .visible(height: 2, zIndex: Int.max, spacing: UIEdgeInsets.zero, insets: UIEdgeInsets.zero) @@ -119,54 +119,51 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { @MainActor func pagingViewController(_ pagingViewController: PagingViewController, viewControllerAt index: Int) -> UIViewController { let service = services[index] - + let admService = services.first { $0.core.nodeGroups.contains(.adm) } let vc = screensFactory.makeTransferVC(service: service) - guard let v = vc as? TransferViewControllerBase else { return vc } - - v.delegate = self + vc.delegate = self guard let address = partner?.address else { return vc } let name = partner?.chatroom?.getName(addressBookService: addressBookService) - v.replyToMessageId = replyToMessageId - v.admReportRecipient = address - v.recipientIsReadonly = true - v.commentsEnabled = service.core.commentsEnabledForRichMessages && partner?.isDummy != true - v.showProgressView(animated: false) + vc.replyToMessageId = replyToMessageId + vc.admReportRecipient = address + vc.recipientIsReadonly = true + vc.commentsEnabled = service.core.commentsEnabledForRichMessages && partner?.isDummy != true + vc.showProgressView(animated: false) Task { - let groupsWithoutActiveNode = service.core.nodeGroups.filter { - !nodesStorage.haveActiveNode(in: $0) - } - - if let group = groupsWithoutActiveNode.first { - v.showAlertView( + guard service.core.hasActiveNode else { + vc.showAlertView( title: nil, - message: ApiServiceError.noEndpointsAvailable(coin: group.name).errorDescription ?? String.adamant.sharedErrors.unknownError, + message: ApiServiceError.noEndpointsAvailable( + nodeGroupName: service.core.tokenName + ).errorDescription ?? .adamant.sharedErrors.unknownError, animated: true ) return } - if !nodesStorage.haveActiveNode(in: .adm) { - v.showAlertView( + guard admService?.core.hasActiveNode ?? false else { + vc.showAlertView( title: nil, - message: String.adamant.sharedErrors.admNodeErrorMessage(service.core.tokenSymbol), + message: .adamant.sharedErrors.admNodeErrorMessage(service.core.tokenSymbol), animated: true ) return } + do { let walletAddress = try await service.core .getWalletAddress( byAdamantAddress: address ) - v.recipientAddress = walletAddress - v.recipientName = name - v.hideProgress(animated: true) + vc.recipientAddress = walletAddress + vc.recipientName = name + vc.hideProgress(animated: true) if ERC20Token.supportedTokens.contains( where: { token in @@ -177,16 +174,16 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { by: EthWalletService.richMessageType )?.core - v.rootCoinBalance = ethWallet?.wallet?.balance + vc.rootCoinBalance = ethWallet?.wallet?.balance } } catch let error as WalletServiceError { - v.showAlertView( + vc.showAlertView( title: nil, message: error.message, animated: true ) } catch { - v.showAlertView( + vc.showAlertView( title: nil, message: String.adamant.sharedErrors.unknownError, animated: true @@ -201,11 +198,7 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { let service = services[index].core guard let wallet = service.wallet else { - return WalletPagingItem( - index: index, - currencySymbol: "", - currencyImage: .asset(named: "adamant_wallet") ?? .init(), - isBalanceInitialized: false) + return WalletItemModel(model: .default) } var network = "" @@ -215,16 +208,16 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { network = type(of: service).tokenNetworkSymbol } - let item = WalletPagingItem( + let item = WalletItem( index: index, currencySymbol: service.tokenSymbol, currencyImage: service.tokenLogo, isBalanceInitialized: wallet.isBalanceInitialized, - currencyNetwork: network) - - item.balance = wallet.balance + currencyNetwork: network, + balance: wallet.balance + ) - return item + return WalletItemModel(model: item) } } diff --git a/Adamant/Modules/ChatsList/NewChatViewController.swift b/Adamant/Modules/ChatsList/NewChatViewController.swift index f77c4be7c..c09d71772 100644 --- a/Adamant/Modules/ChatsList/NewChatViewController.swift +++ b/Adamant/Modules/ChatsList/NewChatViewController.swift @@ -333,11 +333,9 @@ final class NewChatViewController: FormViewController { case .address(address: let addr, params: let params): if let params = params?.first { switch params { - case .address: - break case .label(label: let label): startNewChat(with: addr, name: label, message: nil) - case .message: + case .address, .message, .amount: break } } else { diff --git a/Adamant/Modules/ChatsList/SearchResultsViewController.swift b/Adamant/Modules/ChatsList/SearchResultsViewController.swift index ebd2f7ffb..ad06b4ac5 100644 --- a/Adamant/Modules/ChatsList/SearchResultsViewController.swift +++ b/Adamant/Modules/ChatsList/SearchResultsViewController.swift @@ -202,7 +202,7 @@ final class SearchResultsViewController: UITableViewController { cell.lastMessageLabel.attributedText = shortDescription(for: message) if let date = message.dateValue, date != .adamantNullDate { - cell.dateLabel.text = date.humanizedDay() + cell.dateLabel.text = date.humanizedDay(useTimeFormat: false) } else { cell.dateLabel.text = nil } diff --git a/Adamant/Modules/CoinsNodesList/CoinsNodesListFactory.swift b/Adamant/Modules/CoinsNodesList/CoinsNodesListFactory.swift index e760451ba..3fac3e1da 100644 --- a/Adamant/Modules/CoinsNodesList/CoinsNodesListFactory.swift +++ b/Adamant/Modules/CoinsNodesList/CoinsNodesListFactory.swift @@ -16,14 +16,17 @@ enum CoinsNodesListContext { } struct CoinsNodesListFactory { - private let assembler: Assembler + private let parent: Assembler + private let assemblies = [CoinsNodesListAssembly()] init(parent: Assembler) { - assembler = .init([CoinsNodesListAssembly()], parent: parent) + self.parent = parent } + @MainActor func makeViewController(context: CoinsNodesListContext) -> UIViewController { - let viewModel = assembler.resolve(CoinsNodesListViewModel.self)! + let assembler = Assembler(assemblies, parent: parent) + let viewModel = { assembler.resolver.resolve(CoinsNodesListViewModel.self)! } let view = CoinsNodesListView(viewModel: viewModel) switch context { @@ -38,7 +41,7 @@ struct CoinsNodesListFactory { private struct CoinsNodesListAssembly: Assembly { func assemble(container: Container) { container.register(CoinsNodesListViewModel.self) { - let processedGroups = Set(NodeGroup.allCases).subtracting([.adm]) + let processedGroups = NodeGroup.allCases.filter { $0 != .adm } return .init( mapper: .init(processedGroups: processedGroups), @@ -47,17 +50,10 @@ private struct CoinsNodesListAssembly: Assembly { NodesAdditionalParamsStorageProtocol.self )!, processedGroups: processedGroups, - apiServices: .init( - btc: $0.resolve(BtcApiService.self)!, - eth: $0.resolve(EthApiService.self)!, - klyNode: $0.resolve(KlyNodeApiService.self)!, - klyService: $0.resolve(KlyServiceApiService.self)!, - doge: $0.resolve(DogeApiService.self)!, - dash: $0.resolve(DashApiService.self)!, - adm: $0.resolve(ApiService.self)!, - ipfs: $0.resolve(IPFSApiService.self)! - ) + apiServiceCompose: $0.resolve( + ApiServiceComposeProtocol.self + )! ) - }.inObjectScope(.weak) + }.inObjectScope(.transient) } } diff --git a/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView.swift b/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView.swift index 0f20bb046..7b6b0a74a 100644 --- a/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView.swift +++ b/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView.swift @@ -31,8 +31,8 @@ struct CoinsNodesListView: View { .navigationTitle(String.adamant.coinsNodesList.title) } - init(viewModel: CoinsNodesListViewModel) { - _viewModel = .init(wrappedValue: viewModel) + init(viewModel: @escaping () -> CoinsNodesListViewModel) { + _viewModel = .init(wrappedValue: viewModel()) } } @@ -44,7 +44,13 @@ private extension CoinsNodesListView { ForEach(model.rows) { row in Row( model: row, - setIsEnabled: { viewModel.setIsEnabled(id: row.id, value: $0) } + setIsEnabled: { + viewModel.setIsEnabled( + id: row.id, + group: row.group, + value: $0 + ) + } ).listRowBackground(Color(uiColor: .adamant.cellColor)) } } @@ -69,6 +75,7 @@ private extension CoinsNodesListView { Section { Button(action: showResetAlert) { Text(String.adamant.coinsNodesList.reset) + .foregroundStyle(Color(uiColor: .adamant.textColor)) .expanded(axes: .horizontal) }.listRowBackground(Color(uiColor: .adamant.cellColor)) } diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListMapper.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListMapper.swift index 7bc790405..f6e7f7868 100644 --- a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListMapper.swift +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListMapper.swift @@ -10,25 +10,13 @@ import CommonKit import SwiftUI struct CoinsNodesListMapper { - let processedGroups: Set + let processedGroups: [NodeGroup] - 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 { + func map(items: [NodeGroup: [Node]], restNodeIds: [UUID]) -> [CoinsNodesListState.Section] { + processedGroups.map { map( group: $0, - nodes: nodesDict[$0] ?? .init(), + nodes: items[$0] ?? .init(), restNodeIds: restNodeIds ) }.sorted { $0.title < $1.title } @@ -45,32 +33,29 @@ private extension CoinsNodesListMapper { id: group, title: group.name, rows: nodes.map { - map(node: $0, restNodeIds: restNodeIds, includeVersionTitle: group.includeVersionTitle) + map(node: $0, group: group, isRest: restNodeIds.contains($0.id)) } ) } func map( node: Node, - restNodeIds: [UUID], - includeVersionTitle: Bool + group: NodeGroup, + isRest: Bool ) -> CoinsNodesListState.Section.Row { - let indicatorString = node.indicatorString( - isRest: restNodeIds.contains(node.id), - isWs: false - ) - + let indicatorString = node.indicatorString(isRest: isRest, isWs: false) var indicatorAttrString = AttributedString(stringLiteral: indicatorString) indicatorAttrString.foregroundColor = .init(uiColor: node.indicatorColor) return .init( id: node.id, + group: group, isEnabled: node.isEnabled, - title: node.asString(), + title: node.title, connectionStatus: indicatorAttrString, description: node.statusString( showVersion: true, - includeVersionTitle: includeVersionTitle + dateHeight: group.useDateHeight ) ?? .empty ) } diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListState.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListState.swift index 9a868bbf3..640753860 100644 --- a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListState.swift +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListState.swift @@ -32,6 +32,7 @@ extension CoinsNodesListState { extension CoinsNodesListState.Section { struct Row: Equatable, Identifiable { let id: UUID + let group: NodeGroup let isEnabled: Bool let title: String let connectionStatus: AttributedString diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel+ApiServices.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel+ApiServices.swift deleted file mode 100644 index a4c8ca2aa..000000000 --- a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel+ApiServices.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// 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 klyNode: WalletApiService - let klyService: WalletApiService - let doge: WalletApiService - let dash: WalletApiService - let adm: WalletApiService - let ipfs: WalletApiService - } -} - -extension CoinsNodesListViewModel.ApiServices { - func getApiService(group: NodeGroup) -> WalletApiService { - switch group { - case .btc: - return btc - case .eth: - return eth - case .klyNode: - return klyNode - case .klyService: - return klyService - case .doge: - return doge - case .dash: - return dash - case .adm: - return adm - case .ipfs: - return ipfs - } - } -} diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel.swift index d9f126b7b..1f4c357cc 100644 --- a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel.swift +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel.swift @@ -17,33 +17,31 @@ final class CoinsNodesListViewModel: ObservableObject { private let mapper: CoinsNodesListMapper private let nodesStorage: NodesStorageProtocol private let nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol - private let processedGroups: Set - private let apiServices: ApiServices + private let processedGroups: [NodeGroup] + private let apiServiceCompose: ApiServiceComposeProtocol private var subscriptions = Set() nonisolated init( mapper: CoinsNodesListMapper, nodesStorage: NodesStorageProtocol, nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol, - processedGroups: Set, - apiServices: ApiServices + processedGroups: [NodeGroup], + apiServiceCompose: ApiServiceComposeProtocol ) { self.mapper = mapper self.nodesStorage = nodesStorage self.nodesAdditionalParamsStorage = nodesAdditionalParamsStorage self.processedGroups = processedGroups - self.apiServices = apiServices + self.apiServiceCompose = apiServiceCompose Task { @MainActor in setup() } } - func setIsEnabled(id: UUID, value: Bool) { - nodesStorage.updateNodeParams(id: id, isEnabled: value) + func setIsEnabled(id: UUID, group: NodeGroup, value: Bool) { + nodesStorage.updateNode(id: id, group: group) { $0.isEnabled = value } } func reset() { - processedGroups.forEach { - nodesStorage.resetNodes(group: $0) - } + nodesStorage.resetNodes(.init(processedGroups)) } } @@ -61,7 +59,7 @@ private extension CoinsNodesListViewModel { guard let someGroup = processedGroups.first else { return } - nodesStorage.nodesWithGroupsPublisher + nodesStorage.nodesPublisher .combineLatest(nodesAdditionalParamsStorage.fastestNodeMode(group: someGroup)) .receive(on: DispatchQueue.main) .sink { [weak self] in self?.updateSections(items: $0.0) } @@ -76,25 +74,25 @@ private extension CoinsNodesListViewModel { healthCheck() } - func updateSections(items: [NodeWithGroup]) { + func updateSections(items: [NodeGroup: [Node]]) { state.sections = mapper.map( items: items, - restNodeIds: processedGroups.flatMap { - apiServices.getApiService(group: $0).preferredNodeIds + restNodeIds: processedGroups.compactMap { + apiServiceCompose.chosenFastestNodeId(group: $0) } ) } func saveFastestNodeMode(_ value: Bool) { nodesAdditionalParamsStorage.setFastestNodeMode( - groups: processedGroups, + groups: .init(processedGroups), value: value ) } func healthCheck() { processedGroups.forEach { - apiServices.getApiService(group: $0).healthCheck() + apiServiceCompose.healthCheck(group: $0) } } } diff --git a/Adamant/Modules/Delegates/AdamantDelegateCell.swift b/Adamant/Modules/Delegates/AdamantDelegateCell.swift index 4cadf52a5..d4f030be6 100644 --- a/Adamant/Modules/Delegates/AdamantDelegateCell.swift +++ b/Adamant/Modules/Delegates/AdamantDelegateCell.swift @@ -58,8 +58,8 @@ final class AdamantDelegateCell: UITableViewCell { var isUpvoted: Bool = false { didSet { checkmarkRowView.checkmarkImage = isUpvoted ? .asset(named: "Downvote") : .asset(named: "Upvote") - checkmarkRowView.checkmarkImageBorderColor = isUpvoted ? UIColor.adamant.good.cgColor : UIColor.adamant.secondary.cgColor - checkmarkRowView.checkmarkImageTintColor = isUpvoted ? .adamant.danger : .adamant.good + checkmarkRowView.checkmarkImageBorderColor = isUpvoted ? UIColor.adamant.success.cgColor : UIColor.adamant.secondary.cgColor + checkmarkRowView.checkmarkImageTintColor = isUpvoted ? .adamant.warning : .adamant.success } } diff --git a/Adamant/Modules/Delegates/DelegateDetailsViewController.swift b/Adamant/Modules/Delegates/DelegateDetailsViewController.swift index 29d8b01eb..2511c1c98 100644 --- a/Adamant/Modules/Delegates/DelegateDetailsViewController.swift +++ b/Adamant/Modules/Delegates/DelegateDetailsViewController.swift @@ -73,7 +73,7 @@ final class DelegateDetailsViewController: UIViewController { } // MARK: - Dependencies - var apiService: ApiService! + var apiService: AdamantApiServiceProtocol! var accountService: AccountService! var dialogService: DialogService! diff --git a/Adamant/Modules/Delegates/DelegatesFactory.swift b/Adamant/Modules/Delegates/DelegatesFactory.swift index f079897ab..8b4929dff 100644 --- a/Adamant/Modules/Delegates/DelegatesFactory.swift +++ b/Adamant/Modules/Delegates/DelegatesFactory.swift @@ -8,13 +8,14 @@ import UIKit import Swinject +import CommonKit struct DelegatesFactory { let assembler: Assembler func makeDelegatesListVC(screensFactory: ScreensFactory) -> UIViewController { DelegatesListViewController( - apiService: assembler.resolve(ApiService.self)!, + apiService: assembler.resolve(AdamantApiServiceProtocol.self)!, accountService: assembler.resolve(AccountService.self)!, dialogService: assembler.resolve(DialogService.self)!, screensFactory: screensFactory @@ -23,7 +24,7 @@ struct DelegatesFactory { func makeDelegateDetails() -> DelegateDetailsViewController { let c = DelegateDetailsViewController(nibName: "DelegateDetailsViewController", bundle: nil) - c.apiService = assembler.resolve(ApiService.self) + c.apiService = assembler.resolve(AdamantApiServiceProtocol.self) c.accountService = assembler.resolve(AccountService.self) c.dialogService = assembler.resolve(DialogService.self) return c diff --git a/Adamant/Modules/Delegates/DelegatesListViewController.swift b/Adamant/Modules/Delegates/DelegatesListViewController.swift index 1387ea5f3..8532a097d 100644 --- a/Adamant/Modules/Delegates/DelegatesListViewController.swift +++ b/Adamant/Modules/Delegates/DelegatesListViewController.swift @@ -9,6 +9,8 @@ import UIKit import SnapKit import CommonKit +import MarkdownKit +import SafariServices // MARK: - Localization extension String.adamant { @@ -42,7 +44,7 @@ final class DelegatesListViewController: KeyboardObservingViewController { // MARK: - Dependencies - private let apiService: ApiService + private let apiService: AdamantApiServiceProtocol private let accountService: AccountService private let dialogService: DialogService private let screensFactory: ScreensFactory @@ -57,8 +59,39 @@ final class DelegatesListViewController: KeyboardObservingViewController { // MARK: - Properties + private var headerTextView: UITextView { + let textView = UITextView() + textView.backgroundColor = .clear + textView.isEditable = false + textView.delegate = self + + let attributedString = NSMutableAttributedString( + attributedString: MarkdownParser( + font: UIFont.preferredFont(forTextStyle: .subheadline), + color: .adamant.chatPlaceholderTextColor + ).parse(.localized("Delegates.HeaderText")) + ) + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 10 + paragraphStyle.headIndent = 10 + + attributedString.addAttribute( + .paragraphStyle, + value: paragraphStyle, + range: .init(location: .zero, length: attributedString.length) + ) + + textView.attributedText = attributedString + textView.linkTextAttributes = [NSAttributedString.Key.foregroundColor : UIColor.adamant.active] + + textView.sizeToFit() + + return textView + } + private lazy var tableView: UITableView = { - let tableView = UITableView() + let tableView = UITableView(frame: .zero, style: .grouped) tableView.register(AdamantDelegateCell.self, forCellReuseIdentifier: cellIdentifier) tableView.rowHeight = 50 tableView.backgroundColor = .clear @@ -99,7 +132,7 @@ final class DelegatesListViewController: KeyboardObservingViewController { // MARK: - Lifecycle init( - apiService: ApiService, + apiService: AdamantApiServiceProtocol, accountService: AccountService, dialogService: DialogService, screensFactory: ScreensFactory @@ -192,6 +225,13 @@ final class DelegatesListViewController: KeyboardObservingViewController { ) } + private func openURL(_ url: URL) { + let safari = SFSafariViewController(url: url) + safari.preferredControlTintColor = UIColor.adamant.primary + safari.modalPresentationStyle = .overFullScreen + present(safari, animated: true, completion: nil) + } + private func setupViews() { view.addSubview(tableView) view.addSubview(bottomPanel) @@ -222,19 +262,11 @@ extension DelegatesListViewController: UITableViewDataSource, UITableViewDelegat } func tableView(_: UITableView, viewForHeaderInSection _: Int) -> UIView? { - UIView() - } - - func tableView(_: UITableView, viewForFooterInSection _: Int) -> UIView? { - UIView() + return self.headerTextView } func tableView(_: UITableView, heightForHeaderInSection _: Int) -> CGFloat { - .zero - } - - func tableView(_: UITableView, heightForFooterInSection _: Int) -> CGFloat { - .zero + return UITableView.automaticDimension } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -409,8 +441,8 @@ private extension DelegatesListViewController { let totalVoted = delegates.reduce(0) { $0 + ($1.delegate.voted ? 1 : 0) } + upvoted - downvoted let votingEnabled = changes.count > 0 && changes.count <= maxVotes && totalVoted <= maxTotalVotes - let newVotesColor = changes.count > maxVotes ? UIColor.adamant.alert : UIColor.adamant.primary - let totalVotesColor = totalVoted > maxTotalVotes ? UIColor.adamant.alert : UIColor.adamant.primary + let newVotesColor = changes.count > maxVotes ? UIColor.adamant.attention : UIColor.adamant.primary + let totalVotesColor = totalVoted > maxTotalVotes ? UIColor.adamant.attention : UIColor.adamant.primary DispatchQueue.onMainAsync { [self] in bottomPanel.model = .init( @@ -451,3 +483,16 @@ private extension DelegatesListViewController { ) } } + +// MARK: - UITextViewDelegate +extension DelegatesListViewController: UITextViewDelegate { + func textView( + _ textView: UITextView, + shouldInteractWith URL: URL, + in characterRange: NSRange, + interaction: UITextItemInteraction + ) -> Bool { + openURL(URL) + return false + } +} diff --git a/Adamant/Modules/InfoService/InfoService+Constants.swift b/Adamant/Modules/InfoService/InfoService+Constants.swift new file mode 100644 index 000000000..c0014450a --- /dev/null +++ b/Adamant/Modules/InfoService/InfoService+Constants.swift @@ -0,0 +1,17 @@ +// +// InfoService+Constants.swift +// Adamant +// +// Created by Andrew G on 29.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import CommonKit + +extension InfoService { + static let threshold = 1800 + + nonisolated static var name: String { + .localized("InfoService.InfoService") + } +} diff --git a/Adamant/Modules/InfoService/InfoServiceAssembly.swift b/Adamant/Modules/InfoService/InfoServiceAssembly.swift new file mode 100644 index 000000000..e016a11d7 --- /dev/null +++ b/Adamant/Modules/InfoService/InfoServiceAssembly.swift @@ -0,0 +1,46 @@ +// +// InfoServiceAssembly.swift +// Adamant +// +// Created by Andrew G on 24.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Swinject +import CommonKit + +struct InfoServiceAssembly: Assembly { + func assemble(container: Container) { + container.register(InfoServiceProtocol.self) { r in + InfoService( + securedStore: r.resolve(SecuredStore.self)!, + walletServiceCompose: r.resolve(WalletServiceCompose.self)!, + api: r.resolve(InfoServiceApiServiceProtocol.self)! + ) + }.inObjectScope(.container) + + container.register(InfoServiceApiServiceProtocol.self) { r in + InfoServiceApiService(core: .init( + service: .init( + apiCore: r.resolve(APICoreProtocol.self)!, + mapper: r.resolve(InfoServiceMapperProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + isActive: true, + params: NodeGroup.infoService.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher + )) + }.inObjectScope(.container) + + container.register(InfoServiceMapperProtocol.self) { _ in + InfoServiceMapper() + }.inObjectScope(.transient) + + container.register(InfoServiceApiCore.self) { r in + InfoServiceApiCore( + apiCore: r.resolve(APICoreProtocol.self)!, + mapper: r.resolve(InfoServiceMapperProtocol.self)! + ) + }.inObjectScope(.transient) + } +} diff --git a/Adamant/Modules/InfoService/Models/DTO/InfoServiceHistoryItemDTO.swift b/Adamant/Modules/InfoService/Models/DTO/InfoServiceHistoryItemDTO.swift new file mode 100644 index 000000000..e95231bfc --- /dev/null +++ b/Adamant/Modules/InfoService/Models/DTO/InfoServiceHistoryItemDTO.swift @@ -0,0 +1,15 @@ +// +// InfoServiceHistoryItemDTO.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation + +struct InfoServiceHistoryItemDTO: Codable { + let _id: String + let date: Int + let tickers: [String: Decimal]? +} diff --git a/Adamant/Modules/InfoService/Models/DTO/InfoServiceHistoryRequestDTO.swift b/Adamant/Modules/InfoService/Models/DTO/InfoServiceHistoryRequestDTO.swift new file mode 100644 index 000000000..59037c8ef --- /dev/null +++ b/Adamant/Modules/InfoService/Models/DTO/InfoServiceHistoryRequestDTO.swift @@ -0,0 +1,14 @@ +// +// InfoServiceHistoryRequestDTO.swift +// Adamant +// +// Created by Andrew G on 24.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation + +struct InfoServiceHistoryRequestDTO: Codable { + let timestamp: String + let coin: String +} diff --git a/Adamant/Modules/InfoService/Models/DTO/InfoServiceRatesRequestDTO.swift b/Adamant/Modules/InfoService/Models/DTO/InfoServiceRatesRequestDTO.swift new file mode 100644 index 000000000..21a31fc95 --- /dev/null +++ b/Adamant/Modules/InfoService/Models/DTO/InfoServiceRatesRequestDTO.swift @@ -0,0 +1,13 @@ +// +// InfoServiceRatesRequestDTO.swift +// Adamant +// +// Created by Andrew G on 24.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation + +struct InfoServiceRatesRequestDTO: Codable { + let coin: String +} diff --git a/Adamant/Modules/InfoService/Models/DTO/InfoServiceResponseDTO.swift b/Adamant/Modules/InfoService/Models/DTO/InfoServiceResponseDTO.swift new file mode 100644 index 000000000..5b56cb37f --- /dev/null +++ b/Adamant/Modules/InfoService/Models/DTO/InfoServiceResponseDTO.swift @@ -0,0 +1,13 @@ +// +// InfoServiceResponseDTO.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +struct InfoServiceResponseDTO: Codable { + let success: Bool + let date: Int + let result: Body? +} diff --git a/Adamant/Modules/InfoService/Models/DTO/InfoServiceStatusDTO.swift b/Adamant/Modules/InfoService/Models/DTO/InfoServiceStatusDTO.swift new file mode 100644 index 000000000..821db64af --- /dev/null +++ b/Adamant/Modules/InfoService/Models/DTO/InfoServiceStatusDTO.swift @@ -0,0 +1,17 @@ +// +// InfoServiceStatusDTO.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +struct InfoServiceStatusDTO: Codable { + let success: Bool + let date: Int + let ready: Bool + let updating: Bool + let next_update: Int + let last_updated: Int? + let version: String +} diff --git a/Adamant/Modules/InfoService/Models/InfoServiceApiCommands.swift b/Adamant/Modules/InfoService/Models/InfoServiceApiCommands.swift new file mode 100644 index 000000000..8de0abb4d --- /dev/null +++ b/Adamant/Modules/InfoService/Models/InfoServiceApiCommands.swift @@ -0,0 +1,15 @@ +// +// InfoServiceApiCommands.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation + +enum InfoServiceApiCommands { + static let status = "/status" + static let get = "/get" + static let getHistory = "/getHistory" +} diff --git a/Adamant/Modules/InfoService/Models/InfoServiceApiError.swift b/Adamant/Modules/InfoService/Models/InfoServiceApiError.swift new file mode 100644 index 000000000..ca8fadd41 --- /dev/null +++ b/Adamant/Modules/InfoService/Models/InfoServiceApiError.swift @@ -0,0 +1,50 @@ +// +// InfoServiceApiError.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +enum InfoServiceApiError: Error { + case unknown + case parsingError + case inconsistentData + case apiError(ApiServiceError) +} + +extension InfoServiceApiError: RichError { + var message: String { + switch self { + case .unknown: + .adamant.sharedErrors.unknownError + case .parsingError: + .localized("ApiService.InternalError.ParsingFailed") + case .inconsistentData: + .localized("InfoService.InconsistentData") + case let .apiError(error): + error.message + } + } + + var internalError: Error? { + switch self { + case .unknown, .parsingError, .inconsistentData: + nil + case let .apiError(error): + error + } + } + + var level: ErrorLevel { + switch self { + case .unknown, .parsingError, .inconsistentData: + .error + case let .apiError(error): + error.level + } + } +} diff --git a/Adamant/Modules/InfoService/Models/InfoServiceApiResult.swift b/Adamant/Modules/InfoService/Models/InfoServiceApiResult.swift new file mode 100644 index 000000000..2fbe537d0 --- /dev/null +++ b/Adamant/Modules/InfoService/Models/InfoServiceApiResult.swift @@ -0,0 +1,9 @@ +// +// InfoServiceApiResult.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +typealias InfoServiceApiResult = Result diff --git a/Adamant/Modules/InfoService/Models/InfoServiceHistoryItem.swift b/Adamant/Modules/InfoService/Models/InfoServiceHistoryItem.swift new file mode 100644 index 000000000..73c4bc8d9 --- /dev/null +++ b/Adamant/Modules/InfoService/Models/InfoServiceHistoryItem.swift @@ -0,0 +1,14 @@ +// +// InfoServiceHistoryItem.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation + +struct InfoServiceHistoryItem { + let date: Date + let tickers: [InfoServiceTicker: Decimal] +} diff --git a/Adamant/Modules/InfoService/Models/InfoServiceStatus.swift b/Adamant/Modules/InfoService/Models/InfoServiceStatus.swift new file mode 100644 index 000000000..0c67ad745 --- /dev/null +++ b/Adamant/Modules/InfoService/Models/InfoServiceStatus.swift @@ -0,0 +1,15 @@ +// +// InfoServiceStatus.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +struct InfoServiceStatus { + let lastUpdated: Date + let version: Version +} diff --git a/Adamant/Modules/InfoService/Models/InfoServiceTicker.swift b/Adamant/Modules/InfoService/Models/InfoServiceTicker.swift new file mode 100644 index 000000000..ef5d05425 --- /dev/null +++ b/Adamant/Modules/InfoService/Models/InfoServiceTicker.swift @@ -0,0 +1,15 @@ +// +// InfoServiceTicker.swift +// Adamant +// +// Created by Andrew G on 25.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +struct InfoServiceTicker: Hashable { + let crypto: String + let fiat: String +} diff --git a/Adamant/Modules/InfoService/Protocols/InfoServiceApiServiceProtocol.swift b/Adamant/Modules/InfoService/Protocols/InfoServiceApiServiceProtocol.swift new file mode 100644 index 000000000..7e74c1d0e --- /dev/null +++ b/Adamant/Modules/InfoService/Protocols/InfoServiceApiServiceProtocol.swift @@ -0,0 +1,21 @@ +// +// InfoServiceApiServiceProtocol.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +protocol InfoServiceApiServiceProtocol: ApiServiceProtocol { + func loadRates( + coins: [String] + ) async -> InfoServiceApiResult<[InfoServiceTicker: Decimal]> + + func getHistory( + coin: String, + date: Date + ) async -> InfoServiceApiResult +} diff --git a/Adamant/Modules/InfoService/Protocols/InfoServiceMapperProtocol.swift b/Adamant/Modules/InfoService/Protocols/InfoServiceMapperProtocol.swift new file mode 100644 index 000000000..4798e951a --- /dev/null +++ b/Adamant/Modules/InfoService/Protocols/InfoServiceMapperProtocol.swift @@ -0,0 +1,34 @@ +// +// InfoServiceMapperProtocol.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +protocol InfoServiceMapperProtocol { + func mapToModel(_ dto: InfoServiceStatusDTO) -> InfoServiceStatus + + func mapRatesToModel( + _ dto: InfoServiceResponseDTO<[String: Decimal]> + ) -> InfoServiceApiResult<[InfoServiceTicker: Decimal]> + + func mapToModel( + _ dto: InfoServiceResponseDTO<[InfoServiceHistoryItemDTO]> + ) -> InfoServiceApiResult + + func mapToNodeStatusInfo( + ping: TimeInterval, + status: InfoServiceStatus + ) -> NodeStatusInfo + + func mapToRatesRequestDTO(_ coins: [String]) -> InfoServiceRatesRequestDTO + + func mapToHistoryRequestDTO( + date: Date, + coin: String + ) -> InfoServiceHistoryRequestDTO +} diff --git a/Adamant/ServiceProtocols/CurrencyInfoService.swift b/Adamant/Modules/InfoService/Protocols/InfoServiceProtocol.swift similarity index 82% rename from Adamant/ServiceProtocols/CurrencyInfoService.swift rename to Adamant/Modules/InfoService/Protocols/InfoServiceProtocol.swift index 7a8547491..14aabc11d 100644 --- a/Adamant/ServiceProtocols/CurrencyInfoService.swift +++ b/Adamant/Modules/InfoService/Protocols/InfoServiceProtocol.swift @@ -1,5 +1,5 @@ // -// CurrencyInfoService.swift +// InfoServiceProtocol.swift // Adamant // // Created by Anton Boyarkin on 23/03/2019. @@ -15,8 +15,14 @@ extension Notification.Name { } } +extension StoreKey { + struct CoinInfo { + static let selectedCurrency = "coinInfo.selectedCurrency" + } +} + // MARK: - Currencies -enum Currency: String { +enum Currency: String, CaseIterable { case RUB = "RUB" case USD = "USD" case EUR = "EUR" @@ -37,7 +43,8 @@ enum Currency: String { } // MARK: - protocol -protocol CurrencyInfoService: AnyObject { +@MainActor +protocol InfoServiceProtocol: AnyObject { var currentCurrency: Currency { get set } // Check rates for list of coins @@ -46,12 +53,10 @@ protocol CurrencyInfoService: AnyObject { // Get rate for pair Crypto / Fiat currencies func getRate(for coin: String) -> Decimal? - func getHistory(for coin: String, timestamp: Date, completion: @escaping (ApiServiceResult<[String:Decimal]?>) -> Void) - func getHistory( for coin: String, - timestamp: Date - ) async throws -> [String: Decimal] + date: Date + ) async -> InfoServiceApiResult<[InfoServiceTicker: Decimal]> } // MARK: - AdamantBalanceFormat fiat formatter diff --git a/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService+Extension.swift b/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService+Extension.swift new file mode 100644 index 000000000..c0969c4de --- /dev/null +++ b/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService+Extension.swift @@ -0,0 +1,61 @@ +// +// InfoServiceApiService+Extension.swift +// Adamant +// +// Created by Andrew G on 24.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +extension InfoServiceApiService: ApiServiceProtocol { + var chosenFastestNodeId: UUID? { + core.chosenFastestNodeId + } + + func healthCheck() { + core.healthCheck() + } + + var hasActiveNode: Bool { + !core.sortedAllowedNodes.isEmpty + } +} + +extension InfoServiceApiService: InfoServiceApiServiceProtocol { + func loadRates( + coins: [String] + ) async -> InfoServiceApiResult<[InfoServiceTicker: Decimal]> { + await request { core, origin in + await core.sendRequestJsonResponse( + origin: origin, + path: InfoServiceApiCommands.get, + method: .get, + parameters: mapper.mapToRatesRequestDTO(coins), + encoding: .url + ) + }.flatMap { mapper.mapRatesToModel($0) } + } + + func getHistory( + coin: String, + date: Date + ) async -> InfoServiceApiResult { + await request { core, origin in + await core.sendRequestJsonResponse( + origin: origin, + path: InfoServiceApiCommands.getHistory, + method: .get, + parameters: mapper.mapToHistoryRequestDTO(date: date, coin: coin), + encoding: .url + ) + }.flatMap { mapper.mapToModel($0) } + } +} + +private extension InfoServiceApiService { + var mapper: InfoServiceMapperProtocol { + core.service.mapper + } +} diff --git a/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService.swift b/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService.swift new file mode 100644 index 000000000..1d2b8a39e --- /dev/null +++ b/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService.swift @@ -0,0 +1,29 @@ +// +// InfoServiceApiService.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +final class InfoServiceApiService { + let core: BlockchainHealthCheckWrapper + + func request( + _ request: @Sendable ( + APICoreProtocol, + NodeOrigin + ) async -> ApiServiceResult + ) async -> InfoServiceApiResult { + await core.request { core, origin in + await request(core.apiCore, origin) + }.mapError { .apiError($0) } + } + + init(core: BlockchainHealthCheckWrapper) { + self.core = core + } +} diff --git a/Adamant/Modules/InfoService/Services/InfoService.swift b/Adamant/Modules/InfoService/Services/InfoService.swift new file mode 100644 index 000000000..90b8df859 --- /dev/null +++ b/Adamant/Modules/InfoService/Services/InfoService.swift @@ -0,0 +1,106 @@ +// +// InfoService.swift +// Adamant +// +// Created by Andrew G on 24.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Combine +import CommonKit +import UIKit + +@MainActor +final class InfoService: InfoServiceProtocol { + typealias Rates = [InfoServiceTicker: Decimal] + + private let securedStore: SecuredStore + private let api: InfoServiceApiServiceProtocol + private let rateCoins: [String] + + private var rates = Rates() + private var currentCurrencyValue: Currency = .default + private var subscriptions = Set() + private var isUpdating = false + + var currentCurrency: Currency { + get { currentCurrencyValue } + set { updateCurrency(newValue) } + } + + nonisolated init( + securedStore: SecuredStore, + walletServiceCompose: WalletServiceCompose, + api: InfoServiceApiServiceProtocol + ) { + self.securedStore = securedStore + self.api = api + rateCoins = walletServiceCompose.getWallets().map { $0.core.tokenSymbol } + Task { @MainActor in configure() } + } + + func update() { + Task { + guard !isUpdating else { return } + isUpdating = true + defer { isUpdating = false } + + guard let newRates = try? await api.loadRates(coins: rateCoins).get() else { return } + rates = newRates + sendRatesChangedNotification() + } + } + + func getRate(for coin: String) -> Decimal? { + rates[.init(crypto: coin, fiat: currentCurrency.rawValue)] + } + + func getHistory( + for coin: String, + date: Date + ) async -> InfoServiceApiResult<[InfoServiceTicker: Decimal]> { + await api.getHistory(coin: coin, date: date).flatMap { + abs(date.timeIntervalSince($0.date)) < historyThreshold + ? .success($0.tickers) + : .failure(.inconsistentData) + } + } +} + +private extension InfoService { + func configure() { + setupCurrency() + + NotificationCenter.default + .publisher(for: UIApplication.didBecomeActiveNotification) + .sink { _ in Task { [weak self] in self?.update() } } + .store(in: &subscriptions) + } + + func sendRatesChangedNotification() { + NotificationCenter.default.post( + name: .AdamantCurrencyInfoService.currencyRatesUpdated, + object: nil + ) + } + + func updateCurrency(_ newValue: Currency) { + guard newValue != currentCurrencyValue else { return } + currentCurrencyValue = newValue + securedStore.set(currentCurrencyValue.rawValue, for: StoreKey.CoinInfo.selectedCurrency) + sendRatesChangedNotification() + } + + func setupCurrency() { + if + let id: String = securedStore.get(StoreKey.CoinInfo.selectedCurrency), + let currency = Currency(rawValue: id) + { + currentCurrency = currency + } else { + currentCurrency = .default + } + } +} + +private let historyThreshold: TimeInterval = 86400 diff --git a/Adamant/Modules/InfoService/Services/InfoServiceApiCore.swift b/Adamant/Modules/InfoService/Services/InfoServiceApiCore.swift new file mode 100644 index 000000000..754724e78 --- /dev/null +++ b/Adamant/Modules/InfoService/Services/InfoServiceApiCore.swift @@ -0,0 +1,41 @@ +// +// InfoServiceApiCore.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +struct InfoServiceApiCore { + let apiCore: APICoreProtocol + let mapper: InfoServiceMapperProtocol + + func getNodeStatus( + origin: NodeOrigin + ) async -> ApiServiceResult { + await apiCore.sendRequestJsonResponse( + origin: origin, + path: InfoServiceApiCommands.status + ) + } +} + +extension InfoServiceApiCore: BlockchainHealthCheckableService { + func getStatusInfo( + origin: NodeOrigin + ) async -> ApiServiceResult { + let startTimestamp = Date.now.timeIntervalSince1970 + let statusResponse = await getNodeStatus(origin: origin) + let ping = Date.now.timeIntervalSince1970 - startTimestamp + + return statusResponse.map { statusDto in + mapper.mapToNodeStatusInfo( + ping: ping, + status: mapper.mapToModel(statusDto) + ) + } + } +} diff --git a/Adamant/Modules/InfoService/Services/InfoServiceMapper.swift b/Adamant/Modules/InfoService/Services/InfoServiceMapper.swift new file mode 100644 index 000000000..29f0ba3d3 --- /dev/null +++ b/Adamant/Modules/InfoService/Services/InfoServiceMapper.swift @@ -0,0 +1,106 @@ +// +// InfoServiceMapper.swift +// Adamant +// +// Created by Andrew G on 23.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +struct InfoServiceMapper: InfoServiceMapperProtocol { + private let currencies = Set(Currency.allCases.map { $0.rawValue }) + + func mapToModel(_ dto: InfoServiceStatusDTO) -> InfoServiceStatus { + .init( + lastUpdated: dto.last_updated.map { + Date(timeIntervalSince1970: .init(milliseconds: $0)) + } ?? .adamantNullDate, + version: .init(dto.version) ?? .zero + ) + } + + func mapRatesToModel( + _ dto: InfoServiceResponseDTO<[String: Decimal]> + ) -> InfoServiceApiResult<[InfoServiceTicker: Decimal]> { + mapResponseDTO(dto).map { mapToTickers($0) } + } + + func mapToModel( + _ dto: InfoServiceResponseDTO<[InfoServiceHistoryItemDTO]> + ) -> InfoServiceApiResult { + mapResponseDTO(dto).flatMap { + guard + let item = $0.first, + let tickers = item.tickers + else { return .failure(.parsingError) } + + return .success(.init( + date: .init(timeIntervalSince1970: .init(milliseconds: item.date)), + tickers: mapToTickers(tickers) + )) + } + } + + func mapToNodeStatusInfo( + ping: TimeInterval, + status: InfoServiceStatus + ) -> NodeStatusInfo { + .init( + ping: ping, + height: Int(status.lastUpdated.timeIntervalSince1970), + wsEnabled: false, + wsPort: nil, + version: status.version + ) + } + + func mapToRatesRequestDTO(_ coins: [String]) -> InfoServiceRatesRequestDTO { + .init(coin: coins.joined(separator: ",")) + } + + func mapToHistoryRequestDTO( + date: Date, + coin: String + ) -> InfoServiceHistoryRequestDTO { + .init( + timestamp: .init(format: "%.0f", date.timeIntervalSince1970), + coin: coin + ) + } +} + +private extension InfoServiceMapper { + func mapToTickers(_ rawTickers: [String: Decimal]) -> [InfoServiceTicker: Decimal] { + var dict = [InfoServiceTicker: Decimal]() + + for raw in rawTickers { + guard let ticker = mapToTicker(raw.key) else { continue } + dict[ticker] = raw.value + } + + return dict + } + + func mapToTicker(_ string: String) -> InfoServiceTicker? { + let list: [String] = string.split(separator: "/").map { .init($0) } + + guard + list.count == 2, + let crypto = list.first, + let fiat = list.last + else { return nil } + + return currencies.contains(fiat) + ? .init(crypto: crypto, fiat: fiat) + : nil + } + + func mapResponseDTO( + _ dto: InfoServiceResponseDTO + ) -> InfoServiceApiResult { + guard dto.success else { return .failure(.unknown) } + return dto.result.map { .success($0) } ?? .failure(.parsingError) + } +} diff --git a/Adamant/Modules/Login/LoginFactory.swift b/Adamant/Modules/Login/LoginFactory.swift index dd1fb2c78..5cc26a48e 100644 --- a/Adamant/Modules/Login/LoginFactory.swift +++ b/Adamant/Modules/Login/LoginFactory.swift @@ -8,6 +8,7 @@ import UIKit import Swinject +import CommonKit struct LoginFactory { let assembler: Assembler @@ -19,7 +20,7 @@ struct LoginFactory { dialogService: assembler.resolve(DialogService.self)!, localAuth: assembler.resolve(LocalAuthentication.self)!, screensFactory: screenFactory, - apiService: assembler.resolve(ApiService.self)! + apiService: assembler.resolve(AdamantApiServiceProtocol.self)! ) } } diff --git a/Adamant/Modules/Login/LoginViewController.swift b/Adamant/Modules/Login/LoginViewController.swift index 0392fa815..680a052bb 100644 --- a/Adamant/Modules/Login/LoginViewController.swift +++ b/Adamant/Modules/Login/LoginViewController.swift @@ -139,7 +139,7 @@ final class LoginViewController: FormViewController { let adamantCore: AdamantCore let localAuth: LocalAuthentication let screensFactory: ScreensFactory - let apiService: ApiService + let apiService: AdamantApiServiceProtocol let dialogService: DialogService // MARK: Properties @@ -147,6 +147,8 @@ final class LoginViewController: FormViewController { private var firstTimeActive: Bool = true internal var hidingImagePicker: Bool = false + private lazy var versionFooterView = VersionFooterView() + /// On launch, request user biometry (TouchID/FaceID) if has an account with biometry active var requestBiometryOnFirstTimeActive: Bool = true @@ -158,7 +160,7 @@ final class LoginViewController: FormViewController { dialogService: DialogService, localAuth: LocalAuthentication, screensFactory: ScreensFactory, - apiService: ApiService + apiService: AdamantApiServiceProtocol ) { self.accountService = accountService self.adamantCore = adamantCore @@ -179,6 +181,8 @@ final class LoginViewController: FormViewController { override func viewDidLoad() { super.viewDidLoad() navigationOptions = RowNavigationOptions.Disabled + tableView.tableFooterView = versionFooterView + setVersion() // MARK: Header & Footer if let header = UINib(nibName: "LogoFullHeader", bundle: nil).instantiate(withOwner: nil, options: nil).first as? UIView { @@ -190,14 +194,6 @@ final class LoginViewController: FormViewController { } } - if let footer = UINib(nibName: "VersionFooter", bundle: nil).instantiate(withOwner: nil, options: nil).first as? UIView { - if let label = footer.viewWithTag(555) as? UILabel { - label.text = AdamantUtilities.applicationVersion - label.textColor = UIColor.adamant.primary - tableView.tableFooterView = footer - } - } - // MARK: Login section form +++ Section(Sections.login.localized) { $0.tag = Sections.login.tag @@ -387,12 +383,24 @@ final class LoginViewController: FormViewController { setColors() } + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + versionFooterView.sizeToFit() + } + // MARK: - Other private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear } + + private func setVersion() { + versionFooterView.model = .init( + version: AdamantUtilities.applicationVersion, + commit: nil + ) + } } // MARK: - Login functions @@ -419,7 +427,7 @@ extension LoginViewController { } func generateNewPassphrase() { - let passphrase = adamantCore.generateNewPassphrase() + let passphrase = (try? Mnemonic.generate().joined(separator: " ")) ?? .empty hideNewPassphrase = false diff --git a/Adamant/Modules/NodesEditor/NodeCell/NodeCell.swift b/Adamant/Modules/NodesEditor/NodeCell/NodeCell.swift index c1e426fd4..1a0338615 100644 --- a/Adamant/Modules/NodesEditor/NodeCell/NodeCell.swift +++ b/Adamant/Modules/NodesEditor/NodeCell/NodeCell.swift @@ -19,7 +19,7 @@ final class NodeCell: Cell, CellType { private var model: Model = .default { didSet { guard model != oldValue else { return } - baseRow.baseValue = model + baseRow?.baseValue = model update() } } diff --git a/Adamant/Modules/NodesEditor/NodeEditorViewController.swift b/Adamant/Modules/NodesEditor/NodeEditorViewController.swift index e4c4422be..0d34e410b 100644 --- a/Adamant/Modules/NodesEditor/NodeEditorViewController.swift +++ b/Adamant/Modules/NodesEditor/NodeEditorViewController.swift @@ -93,7 +93,7 @@ final class NodeEditorViewController: FormViewController { // MARK: - Dependencies var dialogService: DialogService! - var apiService: ApiService! + var apiService: AdamantApiServiceProtocol! var nodesStorage: NodesStorageProtocol! // MARK: - Properties @@ -114,7 +114,7 @@ final class NodeEditorViewController: FormViewController { super.viewDidLoad() if let node = node { - self.navigationItem.title = node.host + self.navigationItem.title = node.mainOrigin.host } else { self.navigationItem.title = String.adamant.nodesEditor.newNodeTitle } @@ -131,7 +131,7 @@ final class NodeEditorViewController: FormViewController { $0.tag = Rows.host.tag $0.placeholder = Rows.host.placeholder - $0.value = node?.host + $0.value = node?.mainOrigin.host } // Port @@ -140,18 +140,18 @@ final class NodeEditorViewController: FormViewController { $0.tag = Rows.port.tag if let node = node { - $0.value = node.port - $0.placeholder = String(node.scheme.defaultPort) + $0.value = node.mainOrigin.port + $0.placeholder = String(node.mainOrigin.scheme.defaultPort) } else { - $0.placeholder = String(Node.URLScheme.default.defaultPort) + $0.placeholder = String(NodeOrigin.URLScheme.default.defaultPort) } } // Scheme - <<< PickerInlineRow { + <<< PickerInlineRow { $0.title = Rows.scheme.localized $0.tag = Rows.scheme.tag - $0.value = node?.scheme ?? Node.URLScheme.default + $0.value = node?.mainOrigin.scheme ?? NodeOrigin.URLScheme.default $0.options = [.https, .http] $0.baseCell.detailTextLabel?.textColor = .adamant.textColor }.onExpandInlineRow { (cell, _, inlineRow) in @@ -161,7 +161,7 @@ final class NodeEditorViewController: FormViewController { if let scheme = row.value { portRow.placeholder = String(scheme.defaultPort) } else { - portRow.placeholder = String(Node.URLScheme.default.defaultPort) + portRow.placeholder = String(NodeOrigin.URLScheme.default.defaultPort) } portRow.updateCell() @@ -223,11 +223,11 @@ extension NodeEditorViewController { } let host = rawUrl.trimmingCharacters(in: .whitespaces) - let scheme: Node.URLScheme + let scheme: NodeOrigin.URLScheme if let row = form.rowBy(tag: Rows.scheme.tag), - let value = row.baseValue as? Node.URLScheme + let value = row.baseValue as? NodeOrigin.URLScheme { scheme = value } else { @@ -243,21 +243,30 @@ extension NodeEditorViewController { let result: NodeEditorResult if let node = node { - nodesStorage.updateNodeParams( - id: node.id, - scheme: scheme, - host: host, - port: port - ) + nodesStorage.updateNode(id: node.id, group: .adm) { node in + node.mainOrigin.scheme = scheme + node.mainOrigin.host = host + node.mainOrigin.port = port + } result = .nodeUpdated } else { - result = .new(node: Node( - scheme: scheme, - host: host, + result = .new(node: .init( + id: .init(), isEnabled: true, wsEnabled: false, - port: port + mainOrigin: .init( + scheme: scheme, + host: host, + port: port + ), + altOrigin: nil, + version: nil, + height: nil, + ping: nil, + connectionStatus: nil, + preferMainOrigin: nil, + type: .custom )) } diff --git a/Adamant/Modules/NodesEditor/NodesEditorFactory.swift b/Adamant/Modules/NodesEditor/NodesEditorFactory.swift index 8d6dffdfd..97fcd6f3e 100644 --- a/Adamant/Modules/NodesEditor/NodesEditorFactory.swift +++ b/Adamant/Modules/NodesEditor/NodesEditorFactory.swift @@ -20,7 +20,7 @@ struct NodesEditorFactory { screensFactory: screensFactory, nodesStorage: assembler.resolve(NodesStorageProtocol.self)!, nodesAdditionalParamsStorage: assembler.resolve(NodesAdditionalParamsStorageProtocol.self)!, - apiService: assembler.resolve(ApiService.self)!, + apiService: assembler.resolve(AdamantApiServiceProtocol.self)!, socketService: assembler.resolve(SocketService.self)! ) } @@ -28,7 +28,7 @@ struct NodesEditorFactory { func makeNodeEditorVC() -> NodeEditorViewController { let c = NodeEditorViewController() c.dialogService = assembler.resolve(DialogService.self) - c.apiService = assembler.resolve(ApiService.self) + c.apiService = assembler.resolve(AdamantApiServiceProtocol.self) c.nodesStorage = assembler.resolve(NodesStorageProtocol.self) return c } diff --git a/Adamant/Modules/NodesEditor/NodesListViewController.swift b/Adamant/Modules/NodesEditor/NodesListViewController.swift index 69a7cf3a6..8b4cb93ed 100644 --- a/Adamant/Modules/NodesEditor/NodesListViewController.swift +++ b/Adamant/Modules/NodesEditor/NodesListViewController.swift @@ -86,14 +86,14 @@ final class NodesListViewController: FormViewController { private let screensFactory: ScreensFactory private let nodesStorage: NodesStorageProtocol private let nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol - private let apiService: ApiService + private let apiService: AdamantApiServiceProtocol private let socketService: SocketService // Properties @ObservableValue private var nodesList = [Node]() @ObservableValue private var currentSocketsNodeId: UUID? - @ObservableValue private var currentRestNodesIds = [UUID]() + @ObservableValue private var chosenFastestNodeId: UUID? private var nodesHaveBeenDisplayed = false private var timerSubsctiption: AnyCancellable? @@ -107,7 +107,7 @@ final class NodesListViewController: FormViewController { screensFactory: ScreensFactory, nodesStorage: NodesStorageProtocol, nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol, - apiService: ApiService, + apiService: AdamantApiServiceProtocol, socketService: SocketService ) { self.dialogService = dialogService @@ -221,7 +221,7 @@ final class NodesListViewController: FormViewController { private func setNewNodesList(_ newNodes: [Node]) { nodesList = newNodes - currentRestNodesIds = apiService.preferredNodeIds + chosenFastestNodeId = apiService.chosenFastestNodeId if !nodesHaveBeenDisplayed { UIView.performWithoutAnimation { @@ -250,7 +250,7 @@ extension NodesListViewController { guard let index = getNodeIndex(nodeId: nodeId) else { return } getNodesSection()?.remove(at: index) - nodesStorage.removeNode(id: nodeId) + nodesStorage.removeNode(id: nodeId, group: .adm) } func getNodeIndex(nodeId: UUID) -> Int? { @@ -281,7 +281,7 @@ extension NodesListViewController { func resetToDefault(silent: Bool = false) { if silent { - nodesStorage.resetNodes(group: nodeGroup) + nodesStorage.resetNodes([nodeGroup]) return } @@ -290,7 +290,7 @@ extension NodesListViewController { alert.addAction(UIAlertAction( title: Rows.reset.localized, style: .destructive, - handler: { [weak self] _ in self?.nodesStorage.resetNodes(group: nodeGroup) } + handler: { [weak self] _ in self?.nodesStorage.resetNodes([nodeGroup]) } )) alert.modalPresentationStyle = .overFullScreen present(alert, animated: true, completion: nil) @@ -335,7 +335,7 @@ extension NodesListViewController: NodeEditorDelegate { extension NodesListViewController { func loadDefaultNodes(showAlert: Bool) { - nodesStorage.resetNodes(group: nodeGroup) + nodesStorage.resetNodes([nodeGroup]) if showAlert { dialogService.showSuccess(withMessage: String.adamant.nodesList.defaultNodesWasLoaded) @@ -414,22 +414,18 @@ extension NodesListViewController { } private func makeNodeCellModel(node: Node) -> NodeCell.Model { - let connectionStatus = node.isEnabled - ? node.connectionStatus - : .none - - return .init( + .init( id: node.id, - title: node.asString(), + title: node.title, indicatorString: node.indicatorString( - isRest: currentRestNodesIds.contains(node.id), + isRest: chosenFastestNodeId == node.id, isWs: currentSocketsNodeId == node.id ), indicatorColor: node.indicatorColor, - statusString: node.statusString(showVersion: true) ?? .empty, + statusString: node.statusString(showVersion: true, dateHeight: false) ?? .empty, isEnabled: node.isEnabled, nodeUpdateAction: .init(id: node.id.uuidString) { [nodesStorage] isEnabled in - nodesStorage.updateNodeParams(id: node.id, isEnabled: isEnabled) + nodesStorage.updateNode(id: node.id, group: .adm) { $0.isEnabled = isEnabled } } ) } @@ -437,7 +433,7 @@ extension NodesListViewController { private func makeNodeCellPublisher(nodeId: UUID) -> some Observable { $nodesList.combineLatest( $currentSocketsNodeId, - $currentRestNodesIds + $chosenFastestNodeId ).compactMap { [weak self] tuple in let nodes = tuple.0 diff --git a/Adamant/Modules/Onboard/OnboardPage.swift b/Adamant/Modules/Onboard/OnboardPage.swift index 313f66fa6..6be25d660 100644 --- a/Adamant/Modules/Onboard/OnboardPage.swift +++ b/Adamant/Modules/Onboard/OnboardPage.swift @@ -67,6 +67,7 @@ final class OnboardPage: SwiftyOnboardPage { make.horizontalEdges.equalToSuperview().inset(20) make.bottom.equalToSuperview().offset(-150) make.height.equalTo(260) + make.top.greaterThanOrEqualTo(mainImageView.snp.bottom).offset(20) } } } diff --git a/Adamant/Modules/PartnerQR/PartnerQRFactory.swift b/Adamant/Modules/PartnerQR/PartnerQRFactory.swift index 27801872d..a7805eed2 100644 --- a/Adamant/Modules/PartnerQR/PartnerQRFactory.swift +++ b/Adamant/Modules/PartnerQR/PartnerQRFactory.swift @@ -11,24 +11,24 @@ import SwiftUI import CommonKit struct PartnerQRFactory { - private let assembler: Assembler + private let parent: Assembler + private let assemblies = [PartnerQRAssembly()] init(parent: Assembler) { - assembler = .init([PartnerQRAssembly()], parent: parent) + self.parent = parent } @MainActor func makeViewController(partner: CoreDataAccount) -> UIViewController { - let viewModel = assembler.resolve(PartnerQRViewModel.self)! - viewModel.setup(partner: partner) + let assembler = Assembler(assemblies, parent: parent) - let view = PartnerQRView( - viewModel: viewModel - ) + let viewModel = { + let viewModel = assembler.resolver.resolve(PartnerQRViewModel.self)! + viewModel.setup(partner: partner) + return viewModel + } - return UIHostingController( - rootView: view - ) + return UIHostingController(rootView: PartnerQRView(viewModel: viewModel)) } } @@ -47,6 +47,6 @@ private struct PartnerQRAssembly: Assembly { avatarService: $0.resolve(AvatarService.self)!, partnerQRService: $0.resolve(PartnerQRService.self)! ) - }.inObjectScope(.weak) + }.inObjectScope(.transient) } } diff --git a/Adamant/Modules/PartnerQR/PartnerQRView.swift b/Adamant/Modules/PartnerQR/PartnerQRView.swift index 2d8117d0d..f872bf394 100644 --- a/Adamant/Modules/PartnerQR/PartnerQRView.swift +++ b/Adamant/Modules/PartnerQR/PartnerQRView.swift @@ -26,6 +26,10 @@ struct PartnerQRView: View { } } } + + init(viewModel: @escaping () -> PartnerQRViewModel) { + _viewModel = .init(wrappedValue: viewModel()) + } } private extension PartnerQRView { diff --git a/Adamant/Modules/ScreensFactory/AdamantScreensFactory.swift b/Adamant/Modules/ScreensFactory/AdamantScreensFactory.swift index a2b5af184..59795f7c1 100644 --- a/Adamant/Modules/ScreensFactory/AdamantScreensFactory.swift +++ b/Adamant/Modules/ScreensFactory/AdamantScreensFactory.swift @@ -8,9 +8,11 @@ import UIKit import Swinject +import SwiftUI @MainActor struct AdamantScreensFactory: ScreensFactory { + private let walletFactoryCompose: WalletFactoryCompose private let admWalletFactory: AdmWalletFactory private let chatListFactory: ChatListFactory @@ -27,8 +29,10 @@ struct AdamantScreensFactory: ScreensFactory { private let partnerQRFactory: PartnerQRFactory private let coinsNodesListFactory: CoinsNodesListFactory private let chatSelectTextFactory: ChatSelectTextViewFactory + private let notificationsFactory: NotificationsFactory + private let notificationSoundsFactory: NotificationSoundsFactory private let storageUsageFactory: StorageUsageFactory - + init(assembler: Assembler) { admWalletFactory = .init(assembler: assembler) chatListFactory = .init(assembler: assembler) @@ -45,6 +49,8 @@ struct AdamantScreensFactory: ScreensFactory { partnerQRFactory = .init(parent: assembler) coinsNodesListFactory = .init(parent: assembler) chatSelectTextFactory = .init() + notificationsFactory = .init(parent: assembler) + notificationSoundsFactory = .init(parent: assembler) storageUsageFactory = .init(parent: assembler) walletFactoryCompose = AdamantWalletFactoryCompose( @@ -159,7 +165,11 @@ struct AdamantScreensFactory: ScreensFactory { } func makeNotifications() -> UIViewController { - settingsFactory.makeNotificationsVC() + notificationsFactory.makeViewController() + } + + func makeNotificationSounds(target: NotificationTarget) -> NotificationSoundsView { + notificationSoundsFactory.makeView(target: target) } func makeVisibleWallets() -> UIViewController { diff --git a/Adamant/Modules/ScreensFactory/ScreensFactory.swift b/Adamant/Modules/ScreensFactory/ScreensFactory.swift index b5ecfa2f8..5b6f27f62 100644 --- a/Adamant/Modules/ScreensFactory/ScreensFactory.swift +++ b/Adamant/Modules/ScreensFactory/ScreensFactory.swift @@ -7,6 +7,7 @@ // import UIKit +import SwiftUI @MainActor protocol ScreensFactory { @@ -63,4 +64,5 @@ protocol ScreensFactory { func makeLogin() -> LoginViewController func makeVibrationSelection() -> UIViewController func makePartnerQR(partner: CoreDataAccount) -> UIViewController + func makeNotificationSounds(target: NotificationTarget) -> NotificationSoundsView } diff --git a/Adamant/Modules/Settings/AboutViewController.swift b/Adamant/Modules/Settings/AboutViewController.swift index d69c88907..ff2151d41 100644 --- a/Adamant/Modules/Settings/AboutViewController.swift +++ b/Adamant/Modules/Settings/AboutViewController.swift @@ -14,12 +14,20 @@ import CommonKit // MARK: - Localization extension String.adamant { - struct about { + enum about { static var title: String { String.localized("About.Title", comment: "About page: scene title") } - private init() { } + static func commit(_ commit: String) -> String { + String.localizedStringWithFormat( + String.localized( + "About.Version.Commit", + comment: "Commit Hash" + ), + commit + ) + } } } @@ -120,6 +128,8 @@ final class AboutViewController: FormViewController { private var numerOfTap = 0 private let maxNumerOfTap = 10 + private lazy var versionFooterView = VersionFooterView() + // MARK: Init init( @@ -149,6 +159,8 @@ final class AboutViewController: FormViewController { navigationItem.largeTitleDisplayMode = .always navigationItem.title = String.adamant.about.title + tableView.tableFooterView = versionFooterView + setVersion() // MARK: Header & Footer if let header = UINib(nibName: "LogoFullHeader", bundle: nil).instantiate(withOwner: nil, options: nil).first as? UIView { @@ -166,14 +178,6 @@ final class AboutViewController: FormViewController { } } - if let footer = UINib(nibName: "VersionFooter", bundle: nil).instantiate(withOwner: nil, options: nil).first as? UIView { - if let label = footer.viewWithTag(555) as? UILabel { - label.text = AdamantUtilities.applicationVersion - label.textColor = UIColor.adamant.primary - tableView.tableFooterView = footer - } - } - // MARK: About form +++ Section(Sections.about.localized) { $0.tag = Sections.about.tag @@ -281,6 +285,11 @@ final class AboutViewController: FormViewController { } } + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + versionFooterView.sizeToFit() + } + // MARK: - Other private func setColors() { @@ -421,4 +430,13 @@ private extension AboutViewController { appSection.append(vibrationRow) } + + func setVersion() { + versionFooterView.model = .init( + version: AdamantUtilities.applicationVersion, + commit: AdamantUtilities.Git.commitHash.map { + .adamant.about.commit(.init($0.prefix(20))) + } + ) + } } diff --git a/Adamant/Modules/Settings/Contribute/ContributeFactory.swift b/Adamant/Modules/Settings/Contribute/ContributeFactory.swift index a8c13a4ec..cdda294ca 100644 --- a/Adamant/Modules/Settings/Contribute/ContributeFactory.swift +++ b/Adamant/Modules/Settings/Contribute/ContributeFactory.swift @@ -10,18 +10,17 @@ import Swinject import SwiftUI struct ContributeFactory { - private let assembler: Assembler + private let parent: Assembler + private let assemblies = [ContributeAssembly()] init(parent: Assembler) { - assembler = .init([ContributeAssembly()], parent: parent) + self.parent = parent } func makeViewController() -> UIViewController { - UIHostingController( - rootView: ContributeView( - viewModel: assembler.resolve(ContributeViewModel.self)! - ) - ) + let assembler = Assembler(assemblies, parent: parent) + let viewModel = { assembler.resolver.resolve(ContributeViewModel.self)! } + return UIHostingController(rootView: ContributeView(viewModel: viewModel)) } } @@ -31,6 +30,6 @@ private struct ContributeAssembly: Assembly { ContributeViewModel( crashliticsService: $0.resolve(CrashlyticsService.self)! ) - }.inObjectScope(.weak) + }.inObjectScope(.transient) } } diff --git a/Adamant/Modules/Settings/Contribute/ContributeView.swift b/Adamant/Modules/Settings/Contribute/ContributeView.swift index cc252b3de..99f4719d8 100644 --- a/Adamant/Modules/Settings/Contribute/ContributeView.swift +++ b/Adamant/Modules/Settings/Contribute/ContributeView.swift @@ -39,8 +39,8 @@ struct ContributeView: View { } } - init(viewModel: ContributeViewModel) { - _viewModel = .init(wrappedValue: viewModel) + init(viewModel: @escaping () -> ContributeViewModel) { + _viewModel = .init(wrappedValue: viewModel()) } } diff --git a/Adamant/Modules/Settings/NotificationSoundsViewController.swift b/Adamant/Modules/Settings/NotificationSoundsViewController.swift deleted file mode 100644 index 4d5f1e2b8..000000000 --- a/Adamant/Modules/Settings/NotificationSoundsViewController.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// NotificationSoundsViewController.swift -// Adamant -// -// Created by Stanislav Jelezoglo on 31.08.2022. -// Copyright © 2022 Adamant. All rights reserved. -// - -import UIKit -import Eureka -import AudioToolbox -import AVFoundation - -final class NotificationSoundsViewController: FormViewController { - - // MARK: Sections & Rows - enum Sections { - case alerts - - var tag: String { - switch self { - case .alerts: return "al" - } - } - - var localized: String { - switch self { - case .alerts: return .localized("Notifications.Alert.Tones", comment: "Notifications: Select Alert Tones") - } - } - } - - var notificationsService: NotificationsService! - private var selectSound:NotificationSound = .inputDefault - private var section = SelectableSection>() - var audioPlayer: AVAudioPlayer! - - override func viewDidLoad() { - super.viewDidLoad() - self.title = .localized("Notifications.Sounds.Name", comment: "Notifications: Select Sounds") - - selectSound = notificationsService.notificationsSound - - section = SelectableSection>(Sections.alerts.localized, selectionType: .singleSelection(enableDeselection: false)) - - let sounds: [NotificationSound] = [.none, .noteDefault, .inputDefault, .proud, .relax, .success] - for sound in sounds { - section <<< ListCheckRow { listRow in - listRow.title = sound.localized - listRow.selectableValue = sound - if sound == selectSound { - listRow.value = sound - } else { - listRow.value = nil - } - } - } - - section.onSelectSelectableRow = { [weak self] _, row in - guard let value = row.selectableValue else { return } - self?.playSound(value) - } - - form.append(section) - - addBtns() - } - - private func addBtns() { - self.navigationItem.rightBarButtonItem = UIBarButtonItem( - title: .localized("Notifications.Alert.Save", comment: "Notifications: Select Alert Save"), - style: .done, - target: self, - action: #selector(save) - ) - self.navigationItem.leftBarButtonItem = UIBarButtonItem( - title: .localized("Notifications.Alert.Cancel", comment: "Notifications: Alerts Cancel"), - style: .done, - target: self, - action: #selector(close) - ) - } - - @objc private func save() { - guard let value = section.selectedRow()?.selectableValue else { return } - setNotificationSound(value) - close() - } - - @objc private func close() { - self.dismiss(animated: true) - } - - private func setNotificationSound(_ sound: NotificationSound) { - notificationsService.setNotificationSound(sound) - } - - private func playSound(_ sound: NotificationSound) { - switch sound { - case .none: - break - default: - playSound(by: sound.fileName) - } - } - - private func playSound(by fileName: String) { - guard let url = Bundle.main.url(forResource: fileName.replacingOccurrences(of: ".mp3", with: ""), withExtension: "mp3") else { - return - } - - do { - try AVAudioSession.sharedInstance().setCategory(.playback) - try AVAudioSession.sharedInstance().setActive(true) - self.audioPlayer = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.mp3.rawValue) - audioPlayer.volume = 1.0 - audioPlayer.play() - } catch let error as NSError { - print("error: \(error.localizedDescription)") - } - } -} diff --git a/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsFactory.swift b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsFactory.swift new file mode 100644 index 000000000..485c9f5c8 --- /dev/null +++ b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsFactory.swift @@ -0,0 +1,43 @@ +// +// NotificationSoundsFactory.swift +// Adamant +// +// Created by Yana Silosieva on 20.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Swinject +import SwiftUI + +struct NotificationSoundsFactory { + private let parent: Assembler + private let assemblies = [NotificationSoundAssembly()] + + init(parent: Assembler) { + self.parent = parent + } + + @MainActor + func makeView(target: NotificationTarget) -> NotificationSoundsView { + let assembler = Assembler(assemblies, parent: parent) + let viewModel = { + assembler.resolver.resolve(NotificationSoundsViewModel.self, argument: target)! + } + + let view = NotificationSoundsView(viewModel: viewModel) + + return view + } +} + +private struct NotificationSoundAssembly: Assembly { + func assemble(container: Container) { + container.register(NotificationSoundsViewModel.self) { (r, target: NotificationTarget) in + NotificationSoundsViewModel( + notificationsService: r.resolve(NotificationsService.self)!, + target: target, + dialogService: r.resolve(DialogService.self)! + ) + }.inObjectScope(.transient) + } +} diff --git a/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsView.swift b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsView.swift new file mode 100644 index 000000000..b3523c161 --- /dev/null +++ b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsView.swift @@ -0,0 +1,134 @@ +// +// NotificationSoundsView.swift +// Adamant +// +// Created by Yana Silosieva on 20.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import SwiftUI +import CommonKit + +struct NotificationSoundsView: View { + @StateObject var viewModel: NotificationSoundsViewModel + + @Environment(\.dismiss) var dismiss + + init(viewModel: @escaping () -> NotificationSoundsViewModel) { + _viewModel = .init(wrappedValue: viewModel()) + } + + var body: some View { + GeometryReader { _ in + Form { + Section { + listSounds() + } header: { + Text(alertHeader) + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(action: { + dismiss() + }, label: { + HStack { + Text(cancelTitle) + } + }) + } + + ToolbarItem(placement: .principal) { + Text(toolbarTitle) + .font(.headline) + .minimumScaleFactor(0.7) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .center) + } + + ToolbarItem(placement: .topBarTrailing) { + Button(action: { + viewModel.save() + }, label: { + HStack { + Text(saveTitle) + } + }) + } + } + .onReceive(viewModel.dismissAction) { + dismiss() + } + } + } +} + +private extension NotificationSoundsView { + func toolbar() -> some View { + HStack { + Button(action: { + dismiss() + }, label: { + HStack { + Text(cancelTitle) + } + }) + + Spacer() + + Text(toolbarTitle) + .font(.headline) + .minimumScaleFactor(0.7) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .center) + + Spacer() + + Button(action: { + viewModel.save() + }, label: { + HStack { + Text(saveTitle) + } + }) + } + } + + func listSounds() -> some View { + List { + ForEach(viewModel.sounds, id: \.self) { sound in + Button( + action: { viewModel.selectSound(sound) }, + label: { + HStack { + Text(sound.localized) + Spacer() + if viewModel.selectedSound == sound { + Image(systemName: "checkmark") + .foregroundColor(Color(uiColor: .adamant.textColor)) + .frame(width: 30, height: 30) + } + } + } + ) + } + } + } +} + +private var toolbarTitle: String { + .localized("Notifications.Sounds.Name") +} + +private var cancelTitle: String { + .localized("Notifications.Alert.Cancel") +} + +private var saveTitle: String { + .localized("Notifications.Alert.Save") +} + +private var alertHeader: String { + .localized("Notifications.Alert.Tones") +} diff --git a/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsViewModel.swift b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsViewModel.swift new file mode 100644 index 000000000..d5e68b6dd --- /dev/null +++ b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsViewModel.swift @@ -0,0 +1,94 @@ +// +// NotificationSoundsViewModel.swift +// Adamant +// +// Created by Yana Silosieva on 20.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import SwiftUI +import CommonKit +import Combine +import AVFoundation + +@MainActor +final class NotificationSoundsViewModel: ObservableObject { + private let notificationsService: NotificationsService + private var notificationTarget: NotificationTarget + private let dialogService: DialogService + + let dismissAction = PassthroughSubject() + @Published var isPresented: Bool = false + @Published var selectedSound: NotificationSound = .inputDefault + @Published var sounds: [NotificationSound] = [.none, .noteDefault, .inputDefault, .proud, .relax, .success, .note, .antic, .cheers, .chord, .droplet, .handoff, .milestone, .passage, .portal, .rattle, .rebound, .slide, .welcome] + + private var audioPlayer: AVAudioPlayer? + + nonisolated init( + notificationsService: NotificationsService, + target: NotificationTarget, + dialogService: DialogService + ) { + self.notificationsService = notificationsService + self.notificationTarget = target + self.dialogService = dialogService + + Task { @MainActor in + switch notificationTarget { + case .baseMessage: + self.selectedSound = notificationsService.notificationsSound + case .reaction: + self.selectedSound = notificationsService.notificationsReactionSound + } + } + } + + func setup(notificationTarget: NotificationTarget) { + self.notificationTarget = notificationTarget + } + + func save() { + setNotificationSound(selectedSound) + dismissAction.send() + } + + func setNotificationSound(_ sound: NotificationSound) { + notificationsService.setNotificationSound(sound, for: notificationTarget) + } + + func selectSound(_ sound: NotificationSound) { + selectedSound = sound + playSound(sound) + } + + func playSound(_ sound: NotificationSound) { + switch sound { + case .none: + break + default: + playSound(by: sound.fileName) + } + } +} + +private extension NotificationSoundsViewModel { + func playSound(by fileName: String) { + guard let url = Bundle.main.url(forResource: fileName.replacingOccurrences(of: ".mp3", with: ""), withExtension: "mp3") else { + return + } + + do { + try AVAudioSession.sharedInstance().setCategory(.playback) + try AVAudioSession.sharedInstance().setActive(true) + audioPlayer = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.mp3.rawValue) + audioPlayer?.volume = 1.0 + audioPlayer?.play() + } catch { + dialogService.showError( + withMessage: error.localizedDescription, + supportEmail: true, + error: error + ) + } + } +} diff --git a/Adamant/Modules/Settings/Notifications/NotificationsFactory.swift b/Adamant/Modules/Settings/Notifications/NotificationsFactory.swift new file mode 100644 index 000000000..8bdc2abc0 --- /dev/null +++ b/Adamant/Modules/Settings/Notifications/NotificationsFactory.swift @@ -0,0 +1,52 @@ +// +// NotificationsFactory.swift +// Adamant +// +// Created by Yana Silosieva on 05.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Swinject +import SwiftUI + +struct NotificationsFactory { + private let parent: Assembler + private let assemblies = [NotificationsAssembly()] + + init(parent: Assembler) { + self.parent = parent + } + + @MainActor + func makeViewController() -> UIViewController { + let assembler = Assembler(assemblies, parent: parent) + let viewModel = { assembler.resolver.resolve(NotificationsViewModel.self)! } + + let baseSoundsFactory = NotificationSoundsFactory(parent: assembler) + let reactionSoundsFactory = NotificationSoundsFactory(parent: assembler) + + let baseSoundsView = { baseSoundsFactory.makeView(target: .baseMessage).eraseToAnyView() } + let reactionSoundsView = { reactionSoundsFactory.makeView(target: .reaction).eraseToAnyView() } + + let view = NotificationsView( + viewModel: viewModel, + baseSoundsView: baseSoundsView, + reactionSoundsView: reactionSoundsView + ) + + return UIHostingController( + rootView: view + ) + } +} + +private struct NotificationsAssembly: Assembly { + func assemble(container: Container) { + container.register(NotificationsViewModel.self) { r in + NotificationsViewModel( + dialogService: r.resolve(DialogService.self)!, + notificationsService: r.resolve(NotificationsService.self)! + ) + }.inObjectScope(.transient) + } +} diff --git a/Adamant/Modules/Settings/Notifications/NotificationsView.swift b/Adamant/Modules/Settings/Notifications/NotificationsView.swift new file mode 100644 index 000000000..a7ff49e2a --- /dev/null +++ b/Adamant/Modules/Settings/Notifications/NotificationsView.swift @@ -0,0 +1,198 @@ +// +// NotificationsView.swift +// Adamant +// +// Created by Yana Silosieva on 05.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import SwiftUI +import CommonKit + +struct NotificationsView: View { + @StateObject var viewModel: NotificationsViewModel + private let baseSoundsView: () -> AnyView + private let reactionSoundsView: () -> AnyView + + init( + viewModel: @escaping () -> NotificationsViewModel, + baseSoundsView: @escaping () -> AnyView, + reactionSoundsView: @escaping () -> AnyView + ) { + _viewModel = .init(wrappedValue: viewModel()) + self.baseSoundsView = baseSoundsView + self.reactionSoundsView = reactionSoundsView + } + + var body: some View { + Form { + notificationsSection() + messageSoundSection() + messageReactionsSection() + inAppNotificationsSection() + settingsSection() + moreDetailsSection() + } + .withoutListBackground() + .background(Color(.adamant.secondBackgroundColor)) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + toolbar() + } + } + .sheet(isPresented: $viewModel.presentSoundsPicker, content: { + NavigationView(content: { baseSoundsView() }) + }) + .sheet(isPresented: $viewModel.presentReactionSoundsPicker, content: { + NavigationView(content: { reactionSoundsView() }) + }) + .fullScreenCover(isPresented: $viewModel.openSafariURL) { + SafariWebView(url: viewModel.safariURL).ignoresSafeArea() + } + } +} + +private extension NotificationsView { + func toolbar() -> some View { + HStack { + Text(viewModel.notificationsTitle) + .font(.headline) + .minimumScaleFactor(0.7) + .lineLimit(1) + } + .frame(alignment: .center) + } + + func notificationsSection() -> some View { + Section { + NavigationButton(action: { viewModel.showAlert() }) { + HStack { + Text(viewModel.notificationsTitle) + Spacer() + Text(viewModel.notificationsMode.localized) + .foregroundColor(.gray) + } + } + } header: { + Text(viewModel.notificationsTitle) + } + } + + func messageSoundSection() -> some View { + Section { + NavigationButton(action: { viewModel.presentNotificationSoundsPicker() }) { + HStack { + Text(soundTitle) + Spacer() + Text(viewModel.notificationSound.localized) + .foregroundColor(.gray) + } + } + } header: { + Text(messagesHeader) + } + } + + func messageReactionsSection() -> some View { + Section { + NavigationButton(action: { viewModel.presentReactionNotificationSoundsPicker() }) { + HStack { + Text(soundTitle) + Spacer() + Text(viewModel.notificationReactionSound.localized) + .foregroundColor(.gray) + } + } + } header: { + Text(reactionsHeader) + } + } + + func inAppNotificationsSection() -> some View { + Section { + Toggle(isOn: $viewModel.inAppSounds) { + Text(soundsTitle) + } + .tint(.init(uiColor: .adamant.active)) + + Toggle(isOn: $viewModel.inAppVibrate) { + Text(vibrateTitle) + } + .tint(.init(uiColor: .adamant.active)) + + Toggle(isOn: $viewModel.inAppToasts) { + Text(toastsTitle) + } + .tint(.init(uiColor: .adamant.active)) + } header: { + Text(inAppNotifications) + } + } + + func settingsSection() -> some View { + Section { + NavigationButton(action: { viewModel.openAppSettings() }) { + HStack { + Text(settingsHeader) + Spacer() + } + } + } header: { + Text(settingsHeader) + } + } + + func moreDetailsSection() -> some View { + Section { + if let description = viewModel.parsedMarkdownDescription { + Text(description) + } + NavigationButton(action: { viewModel.presentSafariURL() }) { + HStack { + Image(uiImage: githubRowImage) + Text(visitGithub) + Spacer() + } + } + } + } +} + +private let githubRowImage: UIImage = .asset(named: "row_github") ?? UIImage() + +private var messagesHeader: String { + .localized("SecurityPage.Section.Messages") +} + +private var soundTitle: String { + .localized("Notifications.Sound.Name") +} + +private var settingsHeader: String { + .localized("Notifications.Settings.System") +} + +private var visitGithub: String { + .localized("SecurityPage.Row.VisitGithub") +} + +private var reactionsHeader: String { + .localized("Notifications.Reactions.Header") +} + +private var inAppNotifications: String { + .localized("Notifications.InAppNotifications.Header") +} + +private var soundsTitle: String { + .localized("Notifications.Sounds.Name") +} + +private var vibrateTitle: String { + .localized("Notifications.Vibrate.Title") +} + +private var toastsTitle: String { + .localized("Notifications.Toasts.Title") +} diff --git a/Adamant/Modules/Settings/Notifications/NotificationsViewModel.swift b/Adamant/Modules/Settings/Notifications/NotificationsViewModel.swift new file mode 100644 index 000000000..1e3f17425 --- /dev/null +++ b/Adamant/Modules/Settings/Notifications/NotificationsViewModel.swift @@ -0,0 +1,222 @@ +// +// NotificationsViewModel.swift +// Adamant +// +// Created by Yana Silosieva on 05.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import SwiftUI +import CommonKit +import SafariServices +import MarkdownKit +import Combine + +@MainActor +final class NotificationsViewModel: ObservableObject { + + @Published var notificationsMode: NotificationsMode = .disabled + @Published var notificationSound: NotificationSound = .inputDefault + @Published var notificationReactionSound: NotificationSound = .none + @Published var presentSoundsPicker: Bool = false + @Published var presentReactionSoundsPicker: Bool = false + @Published var openSafariURL: Bool = false + @Published var inAppSounds: Bool = false + @Published var inAppVibrate: Bool = true + @Published var inAppToasts: Bool = true + + let notificationsTitle: String = .localized("SecurityPage.Row.Notifications") + let safariURL = URL(string: "https://github.com/Adamant-im")! + + private let descriptionText: String = .localized("SecurityPage.Row.Notifications.ModesDescription") + + private let dialogService: DialogService + private let notificationsService: NotificationsService + + private var subscriptions = Set() + private var cancellables = Set() + + var parsedMarkdownDescription: AttributedString? { + guard let attributedString = parseMarkdown(descriptionText) else { + return nil + } + return AttributedString(attributedString) + } + + nonisolated init(dialogService: DialogService, notificationsService: NotificationsService) { + self.dialogService = dialogService + self.notificationsService = notificationsService + + Task { + await configure() + await addObservers() + } + } + + func presentNotificationSoundsPicker() { + presentSoundsPicker = true + } + + func presentReactionNotificationSoundsPicker() { + presentReactionSoundsPicker = true + } + + func presentSafariURL() { + openSafariURL = true + } + + func applyInAppSounds(value: Bool) { + notificationsService.setInAppSound(value) + } + + func applyInAppVibrate(value: Bool) { + notificationsService.setInAppVibrate(value) + } + + func applyInAppToasts(value: Bool) { + notificationsService.setInAppToasts(value) + } + + func showAlert() { + dialogService.showAlert( + title: notificationsTitle, + message: nil, + style: .actionSheet, + actions: [ + makeAction( + title: NotificationsMode.disabled.localized, + action: { [weak self] _ in + self?.setNotificationMode(.disabled) + } + ), + makeAction( + title: NotificationsMode.backgroundFetch.localized, + action: { [weak self] _ in + self?.setNotificationMode(.backgroundFetch) + } + ), + makeAction( + title: NotificationsMode.push.localized, + action: { [weak self] _ in + self?.setNotificationMode(.push) + } + ), + makeCancelAction() + ], + from: nil + ) + } + + func setNotificationMode(_ mode: NotificationsMode) { + guard mode != notificationsService.notificationsMode else { + return + } + + notificationsMode = mode + notificationsService.setNotificationsMode(mode) { [weak self] result in + DispatchQueue.onMainAsync { + switch result { + case .success: + return + case .failure(let error): + switch error { + case .notEnoughMoney, .notStayedLoggedIn: + self?.dialogService.showRichError(error: error) + case .denied: + self?.presentNotificationsDeniedError() + } + } + } + } + } + + func openAppSettings() { + if let settingsURL = URL(string: UIApplication.openSettingsURLString) { + if UIApplication.shared.canOpenURL(settingsURL) { + UIApplication.shared.open(settingsURL) + } + } + } + + func parseMarkdown(_ text: String) -> NSAttributedString? { + let parser = MarkdownParser( + font: UIFont.systemFont(ofSize: UIFont.systemFontSize), + color: UIColor.adamant.textColor + ) + parser.link.color = UIColor.adamant.secondary + return parser.parse(text) + } +} + +private extension NotificationsViewModel { + func addObservers() { + NotificationCenter.default + .publisher(for: .AdamantNotificationService.notificationsSoundChanged) + .sink { [weak self] _ in self?.configure() } + .store(in: &subscriptions) + + $inAppSounds + .sink { [weak self] value in + self?.applyInAppSounds(value: value) + } + .store(in: &cancellables) + + $inAppVibrate + .sink { [weak self] value in + self?.applyInAppVibrate(value: value) + } + .store(in: &cancellables) + + $inAppToasts + .sink { [weak self] value in + self?.applyInAppToasts(value: value) + } + .store(in: &cancellables) + } + + func configure() { + notificationsMode = notificationsService.notificationsMode + notificationSound = notificationsService.notificationsSound + notificationReactionSound = notificationsService.notificationsReactionSound + inAppSounds = notificationsService.inAppSound + inAppVibrate = notificationsService.inAppVibrate + inAppToasts = notificationsService.inAppToasts + } +} + +private extension NotificationsViewModel { + func makeAction(title: String, action: ((UIAlertAction) -> Void)?) -> UIAlertAction { + .init( + title: title, + style: .default, + handler: action + ) + } + + func makeCancelAction() -> UIAlertAction { + .init( + title: .adamant.alert.cancel, + style: .cancel, + handler: nil + ) + } + + func presentNotificationsDeniedError() { + dialogService.showAlert( + title: nil, + message: NotificationStrings.notificationsDisabled, + style: .alert, + actions: [ + makeAction( + title: .adamant.alert.settings, + action: { [weak self] _ in self?.openAppSettings() } + ), + makeAction( + title: String.adamant.alert.cancel, + action: nil + ) + ], + from: nil + ) + } +} diff --git a/Adamant/Modules/Settings/NotificationsViewController.swift b/Adamant/Modules/Settings/NotificationsViewController.swift deleted file mode 100644 index 66d1b2e58..000000000 --- a/Adamant/Modules/Settings/NotificationsViewController.swift +++ /dev/null @@ -1,288 +0,0 @@ -// -// NotificationsViewController.swift -// Adamant -// -// Created by Anokhov Pavel on 10/11/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import Eureka -import SafariServices -import MarkdownKit -import ProcedureKit -import CommonKit - -final class NotificationsViewController: FormViewController { - - // MARK: Sections & Rows - enum Sections { - case notifications - case aboutNotificationTypes - case messages - case settings - - var tag: String { - switch self { - case .notifications: return "st" - case .aboutNotificationTypes: return "ans" - case .messages: return "ms" - case .settings: return "settings" - } - } - - var localized: String { - switch self { - case .notifications: return .localized("SecurityPage.Section.NotificationsType", comment: "Security: Selected notifications types") - case .aboutNotificationTypes: return .localized("SecurityPage.Section.AboutNotificationTypes", comment: "Security: About Notification types") - case .messages: return .localized("SecurityPage.Section.Messages", comment: "Security: Messages Notification sound") - case .settings: return .localized("SecurityPage.Section.Settings", comment: "Security: Settings Notification") - } - } - } - - enum Rows { - case notificationsMode - case description, github - case systemSettings - case sound - - var tag: String { - switch self { - case .notificationsMode: return "rn" - case .description: return "rd" - case .github: return "git" - case .systemSettings: return "ss" - case .sound: return "sd" - } - } - - var localized: String { - switch self { - case .notificationsMode: return .localized("SecurityPage.Row.Notifications", comment: "Security: Show notifications") - case .description: return .localized("SecurityPage.Row.Notifications.ModesDescription", comment: "Security: Notification modes description. Markdown supported.") - case .github: return .localized("SecurityPage.Row.VisitGithub", comment: "Security: Visit Github") - case .systemSettings: return .localized("Notifications.Settings.System", comment: "Notifications: Open system Settings") - case .sound: return .localized("Notifications.Sound.Name", comment: "Notifications: Select Sound") - } - } - } - - // MARK: - Dependencies - - var dialogService: DialogService! - var notificationsService: NotificationsService! - - private lazy var markdownParser: MarkdownParser = { - let parser = MarkdownParser(font: UIFont.systemFont(ofSize: UIFont.systemFontSize), color: UIColor.adamant.textColor) - parser.link.color = UIColor.adamant.secondary - return parser - }() - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.largeTitleDisplayMode = .always - navigationItem.title = String.adamant.security.title - navigationOptions = .Disabled - - // MARK: Notifications - // Type - let nType = ActionSheetRow { - $0.tag = Rows.notificationsMode.tag - $0.title = Rows.notificationsMode.localized - $0.selectorTitle = Rows.notificationsMode.localized - $0.options = [.disabled, .backgroundFetch, .push] - }.cellUpdate { [weak self] cell, row in - cell.accessoryType = .disclosureIndicator - - guard let notificationsMode = self?.notificationsService.notificationsMode else { return } - row.value = notificationsMode - }.onChange { [weak self] row in - let mode = row.value ?? NotificationsMode.disabled - self?.setNotificationMode(mode, completion: row.updateCell) - row.updateCell() - } - - // Section - let notificationsSection = Section(Sections.notifications.localized) { - $0.tag = Sections.notifications.tag - } - - notificationsSection.append(nType) - form.append(notificationsSection) - - // MARK: Messages - // Sound - let soundRow = LabelRow { - $0.tag = Rows.sound.tag - $0.title = Rows.sound.localized - $0.value = notificationsService.notificationsSound.localized - }.cellSetup { (cell, _) in - cell.selectionStyle = .gray - }.cellUpdate { (cell, _) in - cell.accessoryType = .disclosureIndicator - }.onCellSelection { [weak self] _, row in - guard let self = self else { return } - row.deselect() - let soundsVC = NotificationSoundsViewController() - soundsVC.notificationsService = self.notificationsService - let navigationController = UINavigationController(rootViewController: soundsVC) - self.present(navigationController, animated: true) - } - - // Section - let messagesSection = Section(Sections.messages.localized) { - $0.tag = Sections.messages.tag - } - - messagesSection.append(soundRow) - form.append(messagesSection) - - // MARK: Settings - // System Settings - let settingsRow = LabelRow { - $0.tag = Rows.systemSettings.tag - $0.title = Rows.systemSettings.localized - }.cellSetup { (cell, _) in - cell.selectionStyle = .gray - }.cellUpdate { (cell, _) in - cell.accessoryType = .disclosureIndicator - }.onCellSelection { _, row in - guard let url = URL(string: UIApplication.openSettingsURLString) else { - return - } - row.deselect() - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - - // Section - let settingsSection = Section(Sections.settings.localized) { - $0.tag = Sections.settings.tag - } - - settingsSection.append(settingsRow) - form.append(settingsSection) - - // MARK: ANS Description - // Description - let descriptionRow = TextAreaRow { - $0.textAreaHeight = .dynamic(initialTextViewHeight: 44) - $0.tag = Rows.description.tag - }.cellUpdate { [weak self] (cell, _) in - cell.textView.isSelectable = false - cell.textView.isEditable = false - if let parser = self?.markdownParser { - cell.textView.attributedText = parser.parse(Rows.description.localized) - } else { - cell.textView.text = Rows.description.localized - } - } - - // Github readme - let githubRow = LabelRow { - $0.tag = Rows.github.tag - $0.title = Rows.github.localized - $0.cell.imageView?.image = .asset(named: "row_github") - $0.cell.imageView?.tintColor = UIColor.adamant.tableRowIcons - }.cellSetup { (cell, _) in - cell.selectionStyle = .gray - }.cellUpdate { (cell, _) in - cell.accessoryType = .disclosureIndicator - }.onCellSelection { [weak self] (_, _) in - guard let url = URL(string: AdamantResources.ansReadmeUrl) else { - fatalError("Failed to build ANS URL") - } - - let safari = SFSafariViewController(url: url) - safari.preferredControlTintColor = UIColor.adamant.primary - safari.modalPresentationStyle = .overFullScreen - self?.present(safari, animated: true, completion: nil) - } - - let ansSection = Section(Sections.aboutNotificationTypes.localized) { - $0.tag = Sections.aboutNotificationTypes.tag - } - - ansSection.append(contentsOf: [descriptionRow, githubRow]) - form.append(ansSection) - - // MARK: Notifications - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantNotificationService.notificationsModeChanged, object: nil, queue: OperationQueue.main) { [weak self] notification in - guard let newMode = notification.userInfo?[AdamantUserInfoKey.NotificationsService.newNotificationsMode] as? NotificationsMode else { - return - } - - guard let row: ActionSheetRow = self?.form.rowBy(tag: Rows.notificationsMode.tag) else { - return - } - - row.value = newMode - row.updateCell() - } - - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantNotificationService.notificationsSoundChanged, object: nil, queue: OperationQueue.main) { [weak self] _ in - guard let row: LabelRow = self?.form.rowBy(tag: Rows.sound.tag) else { - return - } - - row.value = self?.notificationsService.notificationsSound.localized - row.updateCell() - } - setColors() - } - - // MARK: - Other - - func setColors() { - view.backgroundColor = UIColor.adamant.secondBackgroundColor - tableView.backgroundColor = .clear - } - - func setNotificationMode(_ mode: NotificationsMode, completion: @escaping () -> Void) { - guard mode != notificationsService.notificationsMode else { - return - } - - notificationsService.setNotificationsMode(mode) { [weak self] result in - DispatchQueue.onMainAsync { - defer { completion() } - - switch result { - case .success: - return - case .failure(let error): - switch error { - case .notEnoughMoney, .notStayedLoggedIn: - self?.dialogService.showRichError(error: error) - case .denied: - self?.presentNotificationsDeniedError() - } - } - } - } - } - - private func presentNotificationsDeniedError() { - let alert = UIAlertController( - title: nil, - message: NotificationStrings.notificationsDisabled, - preferredStyleSafe: .alert, - source: nil - ) - - alert.addAction(UIAlertAction(title: String.adamant.alert.settings, style: .default) { _ in - DispatchQueue.main.async { - if let settingsURL = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(settingsURL) - } - } - }) - - alert.addAction(UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil)) - alert.modalPresentationStyle = .overFullScreen - present(alert, animated: true, completion: nil) - } -} diff --git a/Adamant/Modules/Settings/SettingsFactory.swift b/Adamant/Modules/Settings/SettingsFactory.swift index 09b68d3f5..5662f747c 100644 --- a/Adamant/Modules/Settings/SettingsFactory.swift +++ b/Adamant/Modules/Settings/SettingsFactory.swift @@ -46,13 +46,6 @@ struct SettingsFactory { ) } - 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)!, diff --git a/Adamant/Modules/TestVibration/VibrationSelectionFactory.swift b/Adamant/Modules/TestVibration/VibrationSelectionFactory.swift index 3c3e87b05..d85712417 100644 --- a/Adamant/Modules/TestVibration/VibrationSelectionFactory.swift +++ b/Adamant/Modules/TestVibration/VibrationSelectionFactory.swift @@ -10,18 +10,18 @@ import Swinject import SwiftUI struct VibrationSelectionFactory { - private let assembler: Assembler + private let parent: Assembler + private let assemblies = [VibrationSelectionAssembly()] init(parent: Assembler) { - assembler = .init([VibrationSelectionAssembly()], parent: parent) + self.parent = parent } + @MainActor func makeViewController() -> UIViewController { - UIHostingController( - rootView: VibrationSelectionView( - viewModel: assembler.resolve(VibrationSelectionViewModel.self)! - ) - ) + let assembler = Assembler(assemblies, parent: parent) + let viewModel = { assembler.resolver.resolve(VibrationSelectionViewModel.self)! } + return UIHostingController(rootView: VibrationSelectionView(viewModel: viewModel)) } } @@ -31,6 +31,6 @@ private struct VibrationSelectionAssembly: Assembly { VibrationSelectionViewModel( vibroService: $0.resolve(VibroService.self)! ) - }.inObjectScope(.weak) + }.inObjectScope(.transient) } } diff --git a/Adamant/Modules/TestVibration/VibrationSelectionView.swift b/Adamant/Modules/TestVibration/VibrationSelectionView.swift index 11b065079..7452705f0 100644 --- a/Adamant/Modules/TestVibration/VibrationSelectionView.swift +++ b/Adamant/Modules/TestVibration/VibrationSelectionView.swift @@ -12,8 +12,8 @@ import CommonKit struct VibrationSelectionView: View { @StateObject var viewModel: VibrationSelectionViewModel - init(viewModel: VibrationSelectionViewModel) { - _viewModel = .init(wrappedValue: viewModel) + init(viewModel: @escaping () -> VibrationSelectionViewModel) { + _viewModel = .init(wrappedValue: viewModel()) } var body: some View { diff --git a/Adamant/Modules/Wallets/Adamant/AdmTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Adamant/AdmTransactionDetailsViewController.swift index 257a1e20c..2d642c0f6 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmTransactionDetailsViewController.swift @@ -48,7 +48,7 @@ final class AdmTransactionDetailsViewController: TransactionDetailsViewControlle transfersProvider: TransfersProvider, screensFactory: ScreensFactory, dialogService: DialogService, - currencyInfo: CurrencyInfoService, + currencyInfo: InfoServiceProtocol, addressBookService: AddressBookService, languageService: LanguageStorageProtocol ) { diff --git a/Adamant/Modules/Wallets/Adamant/AdmTransferViewController.swift b/Adamant/Modules/Wallets/Adamant/AdmTransferViewController.swift index fd1019f10..4748e14d8 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmTransferViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmTransferViewController.swift @@ -294,6 +294,11 @@ final class AdmTransferViewController: TransferViewControllerBase { return true } else if let admAddress = address.getLegacyAdamantAddress() { recipientAddress = admAddress.address + if let row: SafeDecimalRow = form.rowBy(tag: BaseRows.amount.tag) { + row.value = admAddress.amount + row.updateCell() + reloadFormData() + } return true } diff --git a/Adamant/Modules/Wallets/Adamant/AdmWallet.swift b/Adamant/Modules/Wallets/Adamant/AdmWallet.swift index 804a31084..980334f83 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWallet.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWallet.swift @@ -9,6 +9,7 @@ import Foundation final class AdmWallet: WalletAccount { + var unicId: String let address: String var balance: Decimal = 0 var notifications: Int = 0 @@ -16,7 +17,8 @@ final class AdmWallet: WalletAccount { var minAmount: Decimal = 0 var isBalanceInitialized: Bool = false - init(address: String) { + init(unicId: String, address: String) { + self.unicId = unicId self.address = address } } diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift index ec164fc28..71c147234 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift @@ -8,6 +8,7 @@ import Swinject import UIKit +import CommonKit struct AdmWalletFactory: WalletFactory { typealias Service = WalletService @@ -18,7 +19,7 @@ struct AdmWalletFactory: WalletFactory { func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { AdmWalletViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, accountService: assembler.resolve(AccountService.self)!, screensFactory: screensFactory, walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, @@ -47,12 +48,12 @@ struct AdmWalletFactory: WalletFactory { accountsProvider: assembler.resolve(AccountsProvider.self)!, dialogService: assembler.resolve(DialogService.self)!, screensFactory: screensFactory, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - nodesStorage: assembler.resolve(NodesStorageProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } @@ -90,7 +91,7 @@ private extension AdmWalletFactory { transfersProvider: assembler.resolve(TransfersProvider.self)!, screensFactory: screensFactory, dialogService: assembler.resolve(DialogService.self)!, - currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, languageService: assembler.resolve(LanguageStorageProtocol.self)! ) diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift index 9cbd3ec83..b687a5565 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift @@ -15,8 +15,8 @@ extension AdmWalletService { onScreenUpdateInterval: 10, threshold: 10, normalServiceUpdateInterval: 300, - crucialServiceUpdateInterval: 300, - onScreenServiceUpdateInterval: 300 + crucialServiceUpdateInterval: 30, + onScreenServiceUpdateInterval: 10 ) static var newPendingInterval: Int { @@ -67,27 +67,34 @@ extension AdmWalletService { 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")!, 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")!, 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")!), -Node(url: URL(string: "https://node1.adamant.business")!, altUrl: URL(string: "http://194.233.75.29:45555")), -Node(url: URL(string: "https://node2.blockchain2fa.io")!), -Node(url: URL(string: "https://phecda.adm.im")!, altUrl: URL(string: "http://46.250.234.248:36666")), -Node(url: URL(string: "https://tegmine.adm.im")!), -Node(url: URL(string: "https://tauri.adm.im")!, altUrl: URL(string: "http://154.26.159.245:36666")), -Node(url: URL(string: "https://dschubba.adm.im")!), + Node.makeDefaultNode(url: URL(string: "https://clown.adamant.im")!), +Node.makeDefaultNode(url: URL(string: "https://lake.adamant.im")!), +Node.makeDefaultNode(url: URL(string: "https://endless.adamant.im")!, altUrl: URL(string: "http://149.102.157.15:36666")), +Node.makeDefaultNode(url: URL(string: "https://bid.adamant.im")!), +Node.makeDefaultNode(url: URL(string: "https://unusual.adamant.im")!), +Node.makeDefaultNode(url: URL(string: "https://debate.adamant.im")!, altUrl: URL(string: "http://95.216.161.113:36666")), +Node.makeDefaultNode(url: URL(string: "http://78.47.205.206:36666")!), +Node.makeDefaultNode(url: URL(string: "http://5.161.53.74:36666")!), +Node.makeDefaultNode(url: URL(string: "http://184.94.215.92:45555")!), +Node.makeDefaultNode(url: URL(string: "https://node1.adamant.business")!, altUrl: URL(string: "http://194.233.75.29:45555")), +Node.makeDefaultNode(url: URL(string: "https://node2.blockchain2fa.io")!), +Node.makeDefaultNode(url: URL(string: "https://phecda.adm.im")!, altUrl: URL(string: "http://46.250.234.248:36666")), +Node.makeDefaultNode(url: URL(string: "https://tegmine.adm.im")!), +Node.makeDefaultNode(url: URL(string: "https://tauri.adm.im")!, altUrl: URL(string: "http://154.26.159.245:36666")), +Node.makeDefaultNode(url: URL(string: "https://dschubba.adm.im")!), ] } static var serviceNodes: [Node] { [ - Node(url: URL(string: "https://info.adamant.im")!), + Node.makeDefaultNode( + url: URL(string: "https://info.adamant.im")!, + altUrl: URL(string: "http://88.198.156.44:44099")! + ), + Node.makeDefaultNode( + url: URL(string: "https://info2.adm.im")!, + altUrl: URL(string: "http://207.180.210.95:33088")! + ) ] } } diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletService.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService.swift index 04784d370..3dbcbc005 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletService.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService.swift @@ -58,7 +58,7 @@ final class AdmWalletService: NSObject, WalletCoreProtocol { // MARK: - Dependencies weak var accountService: AccountService? - var apiService: ApiService! + var apiService: AdamantApiServiceProtocol! var transfersProvider: TransfersProvider! var coreDataStack: CoreDataStack! var vibroService: VibroService! @@ -90,6 +90,10 @@ final class AdmWalletService: NSObject, WalletCoreProtocol { $hasMoreOldTransactions.eraseToAnyPublisher() } + var hasActiveNode: Bool { + apiService.hasActiveNode + } + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( coinId: tokenUnicID, coreDataStack: coreDataStack, @@ -149,14 +153,14 @@ final class AdmWalletService: NSObject, WalletCoreProtocol { if wallet.balance != account.balance { wallet.balance = account.balance notify = true - } else if wallet.isBalanceInitialized { + } else if !wallet.isBalanceInitialized { notify = true } else { notify = false } wallet.isBalanceInitialized = true } else { - let wallet = AdmWallet(address: account.address) + let wallet = AdmWallet(unicId: tokenUnicID, address: account.address) wallet.isBalanceInitialized = true wallet.balance = account.balance @@ -184,7 +188,11 @@ final class AdmWalletService: NSObject, WalletCoreProtocol { } private func postUpdateNotification(with wallet: WalletAccount) { - NotificationCenter.default.post(name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet]) + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] + ) } func getWalletAddress(byAdamantAddress address: String) async throws -> String { @@ -230,7 +238,7 @@ extension AdmWalletService: SwinjectDependentService { @MainActor func injectDependencies(from container: Container) { accountService = container.resolve(AccountService.self) - apiService = container.resolve(ApiService.self) + apiService = container.resolve(AdamantApiServiceProtocol.self) transfersProvider = container.resolve(TransfersProvider.self) coreDataStack = container.resolve(CoreDataStack.self) vibroService = container.resolve(VibroService.self) diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletViewController.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletViewController.swift index 0c951c9fd..f9fa54d17 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletViewController.swift @@ -27,6 +27,15 @@ extension String.adamant.wallets { static var exchangeInChatAdmTokens: String { String.localized("AccountTab.Row.ExchangeADMInChat", comment: "Account tab: Exchange ADM in chat") } + + static var exchangesOnCoinMarketCap: String { + String.localized("AccountTab.Row.ExchangesOnCoinMarketCap", comment: "Account tab: Exchanges on CMC") + } + + static var exchangesOnCoinGecko: String { + String.localized("AccountTab.Row.ExchangesOnCoinGecko", comment: "Account tab: Exchanges on CoinGecko") + } + // URLs static func getFreeTokensUrl(for address: String) -> String { return String.localizedStringWithFormat(.localized("AccountTab.FreeTokens.UrlFormat", comment: "Account tab: A full 'Get free tokens' link, with %@ as address"), address) @@ -43,10 +52,11 @@ extension String.adamant.wallets { final class AdmWalletViewController: WalletViewControllerBase { // MARK: - Rows & Sections enum Rows { - case buyTokens, freeTokens + case stakeAdm, buyTokens, freeTokens var tag: String { switch self { + case .stakeAdm: return "stakeAdm" case .buyTokens: return "bTkns" case .freeTokens: return "frrTkns" } @@ -54,6 +64,7 @@ final class AdmWalletViewController: WalletViewControllerBase { var localized: String { switch self { + case .stakeAdm: return .localized("AccountTab.Row.StakeAdm", comment: "Stake ADM tokens' row") case .buyTokens: return .localized("AccountTab.Row.BuyTokens", comment: "Account tab: 'Buy tokens' button") case .freeTokens: return .localized("AccountTab.Row.FreeTokens", comment: "Account tab: 'Get free tokens' button") } @@ -61,6 +72,7 @@ final class AdmWalletViewController: WalletViewControllerBase { var image: UIImage? { switch self { + case .stakeAdm: return .asset(named: "row_stake") case .buyTokens: return .asset(named: "row_buy-coins") case .freeTokens: return .asset(named: "row_free-tokens") } @@ -87,6 +99,30 @@ final class AdmWalletViewController: WalletViewControllerBase { // MARK: Rows + let stakeAdmRow = LabelRow { + $0.tag = Rows.stakeAdm.tag + $0.title = Rows.stakeAdm.localized + $0.cell.imageView?.image = Rows.stakeAdm.image + $0.cell.imageView?.tintColor = UIColor.adamant.tableRowIcons + $0.cell.selectionStyle = .gray + $0.cell.backgroundColor = UIColor.adamant.cellColor + }.cellUpdate { (cell, row) in + cell.accessoryType = .disclosureIndicator + + row.title = Rows.stakeAdm.localized + }.onCellSelection { [weak self] (_, row) in + guard let self = self else { return } + let vc = screensFactory.makeDelegatesList() + row.deselect() + + if let split = splitViewController { + let details = UINavigationController(rootViewController:vc) + split.showDetailViewController(details, sender: self) + } else { + navigationController?.pushViewController(vc, animated: true) + } + } + let buyTokensRow = LabelRow { $0.tag = Rows.buyTokens.tag $0.title = Rows.buyTokens.localized @@ -147,6 +183,7 @@ final class AdmWalletViewController: WalletViewControllerBase { } } + section.append(stakeAdmRow) section.append(buyTokensRow) section.append(freeTokensRow) diff --git a/Adamant/Modules/Wallets/Adamant/BuyAndSellViewController.swift b/Adamant/Modules/Wallets/Adamant/BuyAndSellViewController.swift index c3f14868c..41ff0fbc4 100644 --- a/Adamant/Modules/Wallets/Adamant/BuyAndSellViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/BuyAndSellViewController.swift @@ -16,36 +16,24 @@ final class BuyAndSellViewController: FormViewController { enum Rows { case adamantMessage case adamantSite - case azbit - case stakecube - case coinstore - case fameEX - case xeggeX - case nonKYC - + case coinMarketCap + case coinGecko + var tag: String { switch self { case .adamantMessage: return "admChat" case .adamantSite: return "admSite" - case .azbit: return "cDeal" - case .stakecube: return "stakecube" - case .coinstore: return "coinstore" - case .fameEX: return "fameEX" - case .xeggeX: return "xeggeX" - case .nonKYC: return "nonKYC" + case .coinMarketCap: return "coinMarketCap" + case .coinGecko: return "coinGecko" } } var image: UIImage? { switch self { case .adamantMessage: return .asset(named: "row_logo") - case .adamantSite: return .asset(named: "row_logo") - case .azbit: return .asset(named: "azbit_logo") - case .stakecube: return .asset(named: "row_stakecube") - case .coinstore: return .asset(named: "row_coinstore") - case .fameEX: return .asset(named: "row_fameex") - case .xeggeX: return .asset(named: "row_xeggex") - case .nonKYC: return .asset(named: "row_nonkyc") + case .adamantSite: return .asset(named: "exch_anon") + case .coinMarketCap: return .asset(named: "row_coinmarket") + case .coinGecko: return .asset(named: "row_coingecko") } } @@ -53,12 +41,8 @@ final class BuyAndSellViewController: FormViewController { switch self { case .adamantMessage: return String.adamant.wallets.exchangeInChatAdmTokens case .adamantSite: return String.adamant.wallets.buyAdmTokens - case .azbit: return "Azbit" - case .stakecube: return "StakeCube" - case .coinstore: return "Coinstore" - case .fameEX: return "FameEX" - case .xeggeX: return "XeggeX" - case .nonKYC: return "NonKYC" + case .coinMarketCap: return String.adamant.wallets.exchangesOnCoinMarketCap + case .coinGecko: return String.adamant.wallets.exchangesOnCoinGecko } } @@ -66,12 +50,8 @@ final class BuyAndSellViewController: FormViewController { switch self { case .adamantMessage: return "" case .adamantSite: return "https://adamant.im/buy-tokens/" - case .azbit: return "https://azbit.com?referralCode=9YVWYAF" - case .stakecube: return "https://stakecube.net/app/exchange/adm_usdt?layout=pro&team=adm" - case .coinstore: return "https://h5.coinstore.com/h5/signup?invitCode=o951vZ" - case .fameEX: return "https://www.fameex.com/en-US/trade/adm-usdt/commissiondispense?code=MKKAWV" - case .xeggeX: return "https://xeggex.com/market/ADM_USDT?ref=656846d209bbed85b91aba4d" - case .nonKYC: return "https://nonkyc.io/market/ADM_USDT?ref=655b4df9eb13acde84677358" + case .coinMarketCap: return "https://coinmarketcap.com/currencies/adamant-messenger/#Markets" + case .coinGecko: return "https://www.coingecko.com/en/coins/adamant-messenger#markets" } } } @@ -109,29 +89,14 @@ final class BuyAndSellViewController: FormViewController { section.append(admRow) - // MARK: Azbit - let coinRow = buildUrlRow(for: .azbit) - section.append(coinRow) - - // MARK: StakeCube - let stakecubeCoinRow = buildUrlRow(for: .stakecube) - section.append(stakecubeCoinRow) - - // MARK: Coinstore - let coinstoreCoinRow = buildUrlRow(for: .coinstore) - section.append(coinstoreCoinRow) - - // MARK: FameEX - let fameEXCoinRow = buildUrlRow(for: .fameEX) - section.append(fameEXCoinRow) + // MARK: CoinMarketCap + let coinMarketCap = buildUrlRow(for: .coinMarketCap) + section.append(coinMarketCap) - // MARK: XeggeX - let xeggeXCoinRow = buildUrlRow(for: .xeggeX) - section.append(xeggeXCoinRow) + // MARK: CoinGecko + let coinGecko = buildUrlRow(for: .coinGecko) + section.append(coinGecko) - // MARK: NonKYC - let nonKYCCoinRow = buildUrlRow(for: .nonKYC) - section.append(nonKYCCoinRow) form.append(section) setColors() diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcApiService.swift b/Adamant/Modules/Wallets/Bitcoin/BtcApiService.swift index 6834da542..2a82c0431 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcApiService.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcApiService.swift @@ -17,17 +17,17 @@ final class BtcApiCore: BlockchainHealthCheckableService { } func request( - node: Node, - _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + origin: NodeOrigin, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult ) async -> WalletServiceResult { - await request(apiCore, node).mapError { $0.asWalletServiceError() } + await request(apiCore, origin).mapError { $0.asWalletServiceError() } } - func getStatusInfo(node: Node) async -> WalletServiceResult { + func getStatusInfo(origin: NodeOrigin) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 let response = await apiCore.sendRequestRPC( - node: node, + origin: origin, path: BtcApiCommands.getRPC(), requests: [ .init(method: BtcApiCommands.blockchainInfoMethod), @@ -59,16 +59,20 @@ final class BtcApiCore: BlockchainHealthCheckableService { height: blockchainInfo.blocks, wsEnabled: false, wsPort: nil, - version: String(networkInfo.version) + version: .init([networkInfo.version]) )) } } -final class BtcApiService: WalletApiService { +final class BtcApiService: ApiServiceProtocol { let api: BlockchainHealthCheckWrapper - var preferredNodeIds: [UUID] { - api.preferredNodeIds + var chosenFastestNodeId: UUID? { + api.chosenFastestNodeId + } + + var hasActiveNode: Bool { + !api.sortedAllowedNodes.isEmpty } init(api: BlockchainHealthCheckWrapper) { @@ -80,16 +84,16 @@ final class BtcApiService: WalletApiService { } func request( - _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult ) async -> WalletServiceResult { - await api.request { core, node in - await core.request(node: node, request) + await api.request { core, origin in + await core.request(origin: origin, request) } } func getStatusInfo() async -> WalletServiceResult { - await api.request { core, node in - await core.getStatusInfo(node: node) + await api.request { core, origin in + await core.getStatusInfo(origin: origin) } } } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWallet.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWallet.swift index 09314e21d..1dffa72fc 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWallet.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWallet.swift @@ -10,6 +10,7 @@ import Foundation import BitcoinKit final class BtcWallet: WalletAccount { + var unicId: String let addressEntity: Address let privateKey: PrivateKey let publicKey: PublicKey @@ -21,7 +22,12 @@ final class BtcWallet: WalletAccount { var address: String { addressEntity.stringValue } - init(privateKey: PrivateKey, addressConverter: AddressConverter) throws { + init( + unicId: String, + privateKey: PrivateKey, + addressConverter: AddressConverter + ) throws { + self.unicId = unicId self.privateKey = privateKey self.publicKey = privateKey.publicKey() self.addressEntity = try addressConverter.convert(publicKey: publicKey, type: .p2pkh) diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift index f4e82a85f..a575d21f9 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift @@ -19,7 +19,7 @@ struct BtcWalletFactory: WalletFactory { func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { BtcWalletViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, accountService: assembler.resolve(AccountService.self)!, screensFactory: screensFactory, walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, @@ -44,12 +44,12 @@ struct BtcWalletFactory: WalletFactory { accountsProvider: assembler.resolve(AccountsProvider.self)!, dialogService: assembler.resolve(DialogService.self)!, screensFactory: screensFactory, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - nodesStorage: assembler.resolve(NodesStorageProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } @@ -129,7 +129,7 @@ private extension BtcWalletFactory { func makeTransactionDetailsVC(service: Service) -> BtcTransactionDetailsViewController { BtcTransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, accountService: assembler.resolve(AccountService.self)!, walletService: service, diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift index 130bc3953..1c325deb1 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift @@ -14,7 +14,7 @@ extension BtcWalletService { crucialUpdateInterval: 30, onScreenUpdateInterval: 10, threshold: 2, - normalServiceUpdateInterval: 360, + normalServiceUpdateInterval: 330, crucialServiceUpdateInterval: 30, onScreenServiceUpdateInterval: 10 ) @@ -75,8 +75,8 @@ extension BtcWalletService { static var nodes: [Node] { [ - Node(url: URL(string: "https://btcnode1.adamant.im")!, altUrl: URL(string: "http://176.9.38.204:44099")), -Node(url: URL(string: "https://btcnode3.adamant.im")!, altUrl: URL(string: "http://195.201.242.108:44099")), + Node.makeDefaultNode(url: URL(string: "https://btcnode1.adamant.im/bitcoind")!, altUrl: URL(string: "http://176.9.38.204:44099/bitcoind")), +Node.makeDefaultNode(url: URL(string: "https://btcnode3.adamant.im/bitcoind")!, altUrl: URL(string: "http://195.201.242.108:44099/bitcoind")), ] } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+Send.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+Send.swift index 46f0f0f07..484f85583 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+Send.swift @@ -65,9 +65,9 @@ extension BtcWalletService: WalletServiceTwoStepSend { let txHex = transaction.serialized().hex // MARK: Sending request - let responseData = try await btcApiService.request { core, node in + let responseData = try await btcApiService.request { core, origin in await core.sendRequest( - node: node, + origin: origin, path: BtcApiCommands.sendTransaction(), method: .post, parameters: [String.empty: txHex], @@ -88,9 +88,9 @@ extension BtcWalletService: WalletServiceTwoStepSend { let address = wallet.address let parameters = ["noCache": "1"] - let responseData = try await btcApiService.request { core, node in + let responseData = try await btcApiService.request { core, origin in await core.sendRequest( - node: node, + origin: origin, path: BtcApiCommands.getUnspentTransactions(for: address), method: .get, parameters: parameters, @@ -128,5 +128,4 @@ extension BtcWalletService: WalletServiceTwoStepSend { return utxos } - } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift index 2792f8b5b..a6fa93238 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift @@ -118,7 +118,7 @@ final class BtcWalletService: WalletCoreProtocol { static let richMessageType = "btc_transaction" // MARK: - Dependencies - var apiService: ApiService! + var apiService: AdamantApiServiceProtocol! var btcApiService: BtcApiService! var accountService: AccountService! var dialogService: DialogService! @@ -175,6 +175,10 @@ final class BtcWalletService: WalletCoreProtocol { $hasMoreOldTransactions.eraseToAnyPublisher() } + var hasActiveNode: Bool { + apiService.hasActiveNode + } + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( coinId: tokenUnicID, coreDataStack: coreDataStack, @@ -429,7 +433,11 @@ extension BtcWalletService { let privateKeyData = passphrase.data(using: .utf8)!.sha256() let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) - let eWallet = try BtcWallet(privateKey: privateKey, addressConverter: addressConverter) + let eWallet = try BtcWallet( + unicId: tokenUnicID, + privateKey: privateKey, + addressConverter: addressConverter + ) self.btcWallet = eWallet NotificationCenter.default.post( @@ -493,7 +501,7 @@ extension BtcWalletService: SwinjectDependentService { @MainActor func injectDependencies(from container: Container) { accountService = container.resolve(AccountService.self) - apiService = container.resolve(ApiService.self) + apiService = container.resolve(AdamantApiServiceProtocol.self) dialogService = container.resolve(DialogService.self) increaseFeeService = container.resolve(IncreaseFeeService.self) addressConverter = container.resolve(AddressConverterFactory.self)?.make(network: network) @@ -516,16 +524,16 @@ extension BtcWalletService { } func getBalance(address: String) async throws -> Decimal { - let response: BtcBalanceResponse = try await btcApiService.request { api, node in - await api.sendRequestJsonResponse(node: node, path: BtcApiCommands.balance(for: address)) + let response: BtcBalanceResponse = try await btcApiService.request { api, origin in + await api.sendRequestJsonResponse(origin: origin, path: BtcApiCommands.balance(for: address)) }.get() return response.value / BtcWalletService.multiplier } func getFeeRate() async throws -> Decimal { - let response: [String: Decimal] = try await btcApiService.request { api, node in - await api.sendRequestJsonResponse(node: node, path: BtcApiCommands.getFeeRate()) + let response: [String: Decimal] = try await btcApiService.request { api, origin in + await api.sendRequestJsonResponse(origin: origin, path: BtcApiCommands.getFeeRate()) }.get() return response["2"] ?? 1 @@ -663,9 +671,9 @@ extension BtcWalletService { for address: String, fromTx: String? = nil ) async throws -> [RawBtcTransactionResponse] { - return try await btcApiService.request { api, node in + return try await btcApiService.request { api, origin in await api.sendRequestJsonResponse( - node: node, + origin: origin, path: BtcApiCommands.getTransactions( for: address, fromTx: fromTx @@ -679,9 +687,9 @@ extension BtcWalletService { throw WalletServiceError.notLogged } - let rawTransaction: RawBtcTransactionResponse = try await btcApiService.request { api, node in + let rawTransaction: RawBtcTransactionResponse = try await btcApiService.request { api, origin in await api.sendRequestJsonResponse( - node: node, + origin: origin, path: BtcApiCommands.getTransaction(by: hash) ) }.get() diff --git a/Adamant/Modules/Wallets/Dash/DashApiService.swift b/Adamant/Modules/Wallets/Dash/DashApiService.swift index 051441f0a..59ce6fd80 100644 --- a/Adamant/Modules/Wallets/Dash/DashApiService.swift +++ b/Adamant/Modules/Wallets/Dash/DashApiService.swift @@ -17,17 +17,17 @@ final class DashApiCore: BlockchainHealthCheckableService { } func request( - node: Node, - _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + origin: NodeOrigin, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult ) async -> WalletServiceResult { - await request(apiCore, node).mapError { $0.asWalletServiceError() } + await request(apiCore, origin).mapError { $0.asWalletServiceError() } } - func getStatusInfo(node: Node) async -> WalletServiceResult { + func getStatusInfo(origin: NodeOrigin) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 let response = await apiCore.sendRequestRPC( - node: node, + origin: origin, path: .empty, requests: [ .init(method: DashApiComand.networkInfoMethod), @@ -59,16 +59,20 @@ final class DashApiCore: BlockchainHealthCheckableService { height: blockchainInfo.blocks, wsEnabled: false, wsPort: nil, - version: networkInfo.buildversion + version: .init(networkInfo.buildversion) )) } } -final class DashApiService: WalletApiService { +final class DashApiService: ApiServiceProtocol { let api: BlockchainHealthCheckWrapper - var preferredNodeIds: [UUID] { - api.preferredNodeIds + var chosenFastestNodeId: UUID? { + api.chosenFastestNodeId + } + + var hasActiveNode: Bool { + !api.sortedAllowedNodes.isEmpty } init(api: BlockchainHealthCheckWrapper) { @@ -80,16 +84,16 @@ final class DashApiService: WalletApiService { } func request( - _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult ) async -> WalletServiceResult { - await api.request { core, node in - await core.request(node: node, request) + await api.request { core, origin in + await core.request(origin: origin, request) } } func getStatusInfo() async -> WalletServiceResult { - await api.request { core, node in - await core.getStatusInfo(node: node) + await api.request { core, origin in + await core.getStatusInfo(origin: origin) } } } diff --git a/Adamant/Modules/Wallets/Dash/DashWallet.swift b/Adamant/Modules/Wallets/Dash/DashWallet.swift index 77876b482..c442ad02a 100644 --- a/Adamant/Modules/Wallets/Dash/DashWallet.swift +++ b/Adamant/Modules/Wallets/Dash/DashWallet.swift @@ -10,6 +10,7 @@ import Foundation import BitcoinKit final class DashWallet: WalletAccount { + var unicId: String let addressEntity: Address let privateKey: PrivateKey let publicKey: PublicKey @@ -21,7 +22,12 @@ final class DashWallet: WalletAccount { var address: String { addressEntity.stringValue } - init(privateKey: PrivateKey, addressConverter: AddressConverter) throws { + init( + unicId: String, + privateKey: PrivateKey, + addressConverter: AddressConverter + ) throws { + self.unicId = unicId self.privateKey = privateKey self.publicKey = privateKey.publicKey() diff --git a/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift b/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift index e468dc05a..254644865 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift @@ -19,7 +19,7 @@ struct DashWalletFactory: WalletFactory { func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { DashWalletViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, accountService: assembler.resolve(AccountService.self)!, screensFactory: screensFactory, walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, @@ -43,12 +43,12 @@ struct DashWalletFactory: WalletFactory { accountsProvider: assembler.resolve(AccountsProvider.self)!, dialogService: assembler.resolve(DialogService.self)!, screensFactory: screensFactory, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - nodesStorage: assembler.resolve(NodesStorageProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } @@ -138,7 +138,7 @@ private extension DashWalletFactory { func makeTransactionDetailsVC(service: Service) -> DashTransactionDetailsViewController { DashTransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, accountService: assembler.resolve(AccountService.self)!, walletService: service, diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+DynamicConstants.swift index 0b6668985..ddd4c6316 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+DynamicConstants.swift @@ -75,8 +75,8 @@ extension DashWalletService { static var nodes: [Node] { [ - Node(url: URL(string: "https://dashnode1.adamant.im")!, altUrl: URL(string: "http://45.85.147.224:44099")), -Node(url: URL(string: "https://dashnode2.adamant.im")!, altUrl: URL(string: "http://207.180.210.95:44099")), + Node.makeDefaultNode(url: URL(string: "https://dashnode1.adamant.im")!, altUrl: URL(string: "http://45.85.147.224:44099")), +Node.makeDefaultNode(url: URL(string: "https://dashnode2.adamant.im")!, altUrl: URL(string: "http://207.180.210.95:44099")), ] } diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService+Send.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+Send.swift index e2aa6cf8a..cd5b1a860 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+Send.swift @@ -85,9 +85,9 @@ extension DashWalletService: WalletServiceTwoStepSend { func sendTransaction(_ transaction: BitcoinKit.Transaction) async throws { let txHex = transaction.serialized().hex - let response: BTCRPCServerResponce = try await dashApiService.request { core, node in + let response: BTCRPCServerResponce = try await dashApiService.request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: .empty, method: .post, parameters: DashSendRawTransactionDTO(txHex: txHex), diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService+Transactions.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+Transactions.swift index c465fe5f1..d5e178318 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletService+Transactions.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+Transactions.swift @@ -7,7 +7,7 @@ // import Foundation -import Alamofire +import CommonKit import BitcoinKit struct DashTransactionsPointer { @@ -36,9 +36,9 @@ extension DashWalletService { } func getTransaction(by hash: String) async throws -> BTCRawTransaction { - let result: BTCRawTransaction? = try await dashApiService.request { core, node in + let result: BTCRawTransaction? = try await dashApiService.request { core, origin in let response = await core.sendRequestRPC( - node: node, + origin: origin, path: .empty, request: .init( method: DashApiComand.rawTransactionMethod, @@ -73,9 +73,9 @@ extension DashWalletService { ) } - let result: [BTCRawTransaction] = try await dashApiService.request { core, node in + let result: [BTCRawTransaction] = try await dashApiService.request { core, origin in let response = await core.sendRequestRPC( - node: node, + origin: origin, path: .empty, requests: params ) @@ -102,9 +102,9 @@ extension DashWalletService { throw WalletServiceError.internalError(message: "Hash is empty", error: nil) } - let result: BTCRPCServerResponce = try await dashApiService.request { core, node in + let result: BTCRPCServerResponce = try await dashApiService.request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: .empty, method: .post, parameters: DashGetBlockDTO(hash: hash), @@ -125,9 +125,9 @@ extension DashWalletService { } let response: BTCRPCServerResponce<[DashUnspentTransaction]> = try await dashApiService.request { - core, node in + core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: .empty, method: .post, parameters: DashGetUnspentTransactionDTO(address: wallet.address), @@ -190,9 +190,9 @@ private extension DashWalletService { extension DashWalletService { func requestTransactionsIds(for address: String) async throws -> [String] { let response: BTCRPCServerResponce<[String]> = try await dashApiService.request { - core, node in + core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: .empty, method: .post, parameters: DashGetAddressTransactionIds(address: address), diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService.swift b/Adamant/Modules/Wallets/Dash/DashWalletService.swift index 695fa2c32..dadb5ad8f 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletService.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService.swift @@ -59,7 +59,7 @@ final class DashWalletService: WalletCoreProtocol { static let richMessageType = "dash_transaction" // MARK: - Dependencies - var apiService: ApiService! + var apiService: AdamantApiServiceProtocol! var dashApiService: DashApiService! var accountService: AccountService! var securedStore: SecuredStore! @@ -114,6 +114,10 @@ final class DashWalletService: WalletCoreProtocol { } } + var hasActiveNode: Bool { + apiService.hasActiveNode + } + // MARK: - Notifications let walletUpdatedNotification = Notification.Name("adamant.dashWallet.walletUpdated") let serviceEnabledChanged = Notification.Name("adamant.dashWallet.enabledChanged") @@ -308,6 +312,7 @@ extension DashWalletService { let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) let eWallet = try DashWallet( + unicId: tokenUnicID, privateKey: privateKey, addressConverter: addressConverter ) @@ -372,7 +377,7 @@ extension DashWalletService: SwinjectDependentService { @MainActor func injectDependencies(from container: Container) { accountService = container.resolve(AccountService.self) - apiService = container.resolve(ApiService.self) + apiService = container.resolve(AdamantApiServiceProtocol.self) securedStore = container.resolve(SecuredStore.self) dialogService = container.resolve(DialogService.self) addressConverter = container.resolve(AddressConverterFactory.self)? @@ -396,9 +401,9 @@ extension DashWalletService { } func getBalance(address: String) async throws -> Decimal { - let data: Data = try await dashApiService.request { core, node in + let data: Data = try await dashApiService.request { core, origin in await core.sendRequest( - node: node, + origin: origin, path: .empty, method: .post, parameters: DashGetAddressBalanceDTO(address: address), diff --git a/Adamant/Modules/Wallets/Doge/DogeApiService.swift b/Adamant/Modules/Wallets/Doge/DogeApiService.swift index 5874978e2..5ed75818c 100644 --- a/Adamant/Modules/Wallets/Doge/DogeApiService.swift +++ b/Adamant/Modules/Wallets/Doge/DogeApiService.swift @@ -17,18 +17,18 @@ final class DogeApiCore: BlockchainHealthCheckableService { } func request( - node: Node, - _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + origin: NodeOrigin, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult ) async -> WalletServiceResult { - await request(apiCore, node).mapError { $0.asWalletServiceError() } + await request(apiCore, origin).mapError { $0.asWalletServiceError() } } - func getStatusInfo(node: Node) async -> WalletServiceResult { + func getStatusInfo(origin: NodeOrigin) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 - let response: WalletServiceResult = await request(node: node) { core, node in + let response: WalletServiceResult = await request(origin: origin) { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: DogeApiCommands.getInfo() ) } @@ -39,17 +39,21 @@ final class DogeApiCore: BlockchainHealthCheckableService { height: data.info.blocks, wsEnabled: false, wsPort: nil, - version: "\(data.info.version)" + version: .init([data.info.version]) ) } } } -final class DogeApiService: WalletApiService { +final class DogeApiService: ApiServiceProtocol { let api: BlockchainHealthCheckWrapper - var preferredNodeIds: [UUID] { - api.preferredNodeIds + var chosenFastestNodeId: UUID? { + api.chosenFastestNodeId + } + + var hasActiveNode: Bool { + !api.sortedAllowedNodes.isEmpty } init(api: BlockchainHealthCheckWrapper) { @@ -61,16 +65,16 @@ final class DogeApiService: WalletApiService { } func request( - _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult ) async -> WalletServiceResult { - await api.request { core, node in - await core.request(node: node, request) + await api.request { core, origin in + await core.request(origin: origin, request) } } func getStatusInfo() async -> WalletServiceResult { - await api.request { core, node in - await core.getStatusInfo(node: node) + await api.request { core, origin in + await core.getStatusInfo(origin: origin) } } } diff --git a/Adamant/Modules/Wallets/Doge/DogeWallet.swift b/Adamant/Modules/Wallets/Doge/DogeWallet.swift index 20451d1f5..c70b4a1ff 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWallet.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWallet.swift @@ -10,6 +10,7 @@ import Foundation import BitcoinKit final class DogeWallet: WalletAccount { + var unicId: String let addressEntity: Address let privateKey: PrivateKey let publicKey: PublicKey @@ -21,18 +22,25 @@ final class DogeWallet: WalletAccount { var address: String { addressEntity.stringValue } - init(privateKey: PrivateKey, addressConverter: AddressConverter) throws { + init( + unicId: String, + privateKey: PrivateKey, + addressConverter: AddressConverter + ) throws { + self.unicId = unicId self.privateKey = privateKey self.publicKey = privateKey.publicKey() self.addressEntity = try addressConverter.convert(publicKey: publicKey, type: .p2pkh) } init( + unicId: String, privateKey: PrivateKey, balance: Decimal, notifications: Int, addressConverter: AddressConverter ) throws { + self.unicId = unicId self.privateKey = privateKey self.balance = balance self.notifications = notifications diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift b/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift index b2e700f46..0ece9a4e8 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift @@ -19,7 +19,7 @@ struct DogeWalletFactory: WalletFactory { func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { DogeWalletViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, accountService: assembler.resolve(AccountService.self)!, screensFactory: screensFactory, walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, @@ -43,12 +43,12 @@ struct DogeWalletFactory: WalletFactory { accountsProvider: assembler.resolve(AccountsProvider.self)!, dialogService: assembler.resolve(DialogService.self)!, screensFactory: screensFactory, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - nodesStorage: assembler.resolve(NodesStorageProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } @@ -128,7 +128,7 @@ private extension DogeWalletFactory { func makeTransactionDetailsVC(service: Service) -> DogeTransactionDetailsViewController { DogeTransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, accountService: assembler.resolve(AccountService.self)!, walletService: service, diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService+DynamicConstants.swift index 69c4c7900..d940f4428 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService+DynamicConstants.swift @@ -75,8 +75,9 @@ extension DogeWalletService { static var nodes: [Node] { [ - Node(url: URL(string: "https://dogenode1.adamant.im")!, altUrl: URL(string: "http://5.9.99.62:44099")), -Node(url: URL(string: "https://dogenode2.adamant.im")!, altUrl: URL(string: "http://176.9.32.126:44098")), + Node.makeDefaultNode(url: URL(string: "https://dogenode1.adamant.im")!, altUrl: URL(string: "http://5.9.99.62:44099")), +Node.makeDefaultNode(url: URL(string: "https://dogenode2.adamant.im")!, altUrl: URL(string: "http://176.9.32.126:44098")), +Node.makeDefaultNode(url: URL(string: "https://dogenode3.adm.im")!, altUrl: URL(string: "http://95.216.45.88:44098")), ] } diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift index 04c3d04ca..6805b6c61 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift @@ -69,13 +69,14 @@ extension DogeWalletService: WalletServiceTwoStepSend { func sendTransaction(_ transaction: BitcoinKit.Transaction) async throws { let txHex = transaction.serialized().hex - _ = try await dogeApiService.api.request { core, node in + _ = try await dogeApiService.api.request { core, origin in let response: APIResponseModel = await core.apiCore.sendRequestBasic( - node: node, + origin: origin, path: DogeApiCommands.sendTransaction(), method: .post, parameters: ["rawtx": txHex], encoding: .json, + timeout: .common, downloadProgress: { _ in } ) diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService.swift index 062706875..bd5eb957a 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService.swift @@ -54,7 +54,7 @@ final class DogeWalletService: WalletCoreProtocol { static let richMessageType = "doge_transaction" // MARK: - Dependencies - var apiService: ApiService! + var apiService: AdamantApiServiceProtocol! var dogeApiService: DogeApiService! var accountService: AccountService! var dialogService: DialogService! @@ -143,6 +143,10 @@ final class DogeWalletService: WalletCoreProtocol { $hasMoreOldTransactions.eraseToAnyPublisher() } + var hasActiveNode: Bool { + apiService.hasActiveNode + } + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( coinId: tokenUnicID, coreDataStack: coreDataStack, @@ -299,7 +303,11 @@ extension DogeWalletService { let privateKeyData = passphrase.data(using: .utf8)!.sha256() let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) - let eWallet = try DogeWallet(privateKey: privateKey, addressConverter: addressConverter) + let eWallet = try DogeWallet( + unicId: tokenUnicID, + privateKey: privateKey, + addressConverter: addressConverter + ) self.dogeWallet = eWallet NotificationCenter.default.post( @@ -359,7 +367,7 @@ extension DogeWalletService { extension DogeWalletService: SwinjectDependentService { func injectDependencies(from container: Container) { accountService = container.resolve(AccountService.self) - apiService = container.resolve(ApiService.self) + apiService = container.resolve(AdamantApiServiceProtocol.self) dialogService = container.resolve(DialogService.self) addressConverter = container.resolve(AddressConverterFactory.self)? .make(network: network) @@ -383,9 +391,9 @@ extension DogeWalletService { } func getBalance(address: String) async throws -> Decimal { - let data: Data = try await dogeApiService.request { core, node in + let data: Data = try await dogeApiService.request { core, origin in await core.sendRequest( - node: node, + origin: origin, path: DogeApiCommands.balance(for: address) ) }.get() @@ -525,9 +533,9 @@ extension DogeWalletService { "to": to ] - return try await dogeApiService.request { core, node in + return try await dogeApiService.request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: DogeApiCommands.getTransactions(for: address), method: .get, parameters: parameters, @@ -548,9 +556,9 @@ extension DogeWalletService { ] // MARK: Sending request - let data = try await dogeApiService.request { core, node in + let data = try await dogeApiService.request { core, origin in await core.sendRequest( - node: node, + origin: origin, path: DogeApiCommands.getUnspentTransactions(for: address), method: .get, parameters: parameters, @@ -597,17 +605,17 @@ extension DogeWalletService { } func getTransaction(by hash: String) async throws -> BTCRawTransaction { - try await dogeApiService.request { core, node in + try await dogeApiService.request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: DogeApiCommands.getTransaction(by: hash) ) }.get() } func getBlockId(by hash: String) async throws -> String { - let data = try await dogeApiService.request { core, node in - await core.sendRequest(node: node, path: DogeApiCommands.getBlock(by: hash)) + let data = try await dogeApiService.request { core, origin in + await core.sendRequest(origin: origin, path: DogeApiCommands.getBlock(by: hash)) }.get() let json = try? JSONSerialization.jsonObject( diff --git a/Adamant/Modules/Wallets/ERC20/ERC20Wallet.swift b/Adamant/Modules/Wallets/ERC20/ERC20Wallet.swift index d0eed70f5..8413a62f2 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20Wallet.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20Wallet.swift @@ -11,6 +11,7 @@ import web3swift import Web3Core final class ERC20Wallet: WalletAccount { + var unicId: String let address: String let ethAddress: EthereumAddress let keystore: BIP32Keystore @@ -21,7 +22,13 @@ final class ERC20Wallet: WalletAccount { var minAmount: Decimal = 0 var isBalanceInitialized: Bool = false - init(address: String, ethAddress: EthereumAddress, keystore: BIP32Keystore) { + init( + unicId: String, + address: String, + ethAddress: EthereumAddress, + keystore: BIP32Keystore + ) { + self.unicId = unicId self.address = address self.ethAddress = ethAddress self.keystore = keystore diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift index 7b0842dec..f77913123 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift @@ -19,7 +19,7 @@ struct ERC20WalletFactory: WalletFactory { func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { ERC20WalletViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, accountService: assembler.resolve(AccountService.self)!, screensFactory: screensFactory, walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, @@ -43,12 +43,12 @@ struct ERC20WalletFactory: WalletFactory { accountsProvider: assembler.resolve(AccountsProvider.self)!, dialogService: assembler.resolve(DialogService.self)!, screensFactory: screensFactory, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - nodesStorage: assembler.resolve(NodesStorageProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } @@ -130,7 +130,7 @@ private extension ERC20WalletFactory { func makeTransactionDetailsVC(service: Service) -> ERC20TransactionDetailsViewController { ERC20TransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, accountService: assembler.resolve(AccountService.self)!, walletService: service, diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift index 88e73bec2..22a4dc4c7 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift @@ -106,7 +106,7 @@ final class ERC20WalletService: WalletCoreProtocol { // MARK: - Dependencies weak var accountService: AccountService? - var apiService: ApiService! + var apiService: AdamantApiServiceProtocol! var erc20ApiService: ERC20ApiService! var dialogService: DialogService! var increaseFeeService: IncreaseFeeService! @@ -166,6 +166,10 @@ final class ERC20WalletService: WalletCoreProtocol { $hasMoreOldTransactions.eraseToAnyPublisher() } + var hasActiveNode: Bool { + apiService.hasActiveNode + } + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( coinId: tokenUnicID, coreDataStack: coreDataStack, @@ -375,7 +379,12 @@ extension ERC20WalletService { } // MARK: 3. Update - let eWallet = EthWallet(address: ethAddress.address, ethAddress: ethAddress, keystore: keystore) + let eWallet = EthWallet( + unicId: tokenUnicID, + address: ethAddress.address, + ethAddress: ethAddress, + keystore: keystore + ) ethWallet = eWallet if !enabled { @@ -407,7 +416,7 @@ extension ERC20WalletService: SwinjectDependentService { @MainActor func injectDependencies(from container: Container) { accountService = container.resolve(AccountService.self) - apiService = container.resolve(ApiService.self) + apiService = container.resolve(AdamantApiServiceProtocol.self) dialogService = container.resolve(DialogService.self) increaseFeeService = container.resolve(IncreaseFeeService.self) erc20ApiService = container.resolve(ERC20ApiService.self) @@ -548,9 +557,9 @@ extension ERC20WalletService { "order": "time.desc" ] - var transactions: [EthTransactionShort] = try await erc20ApiService.requestApiCore { core, node in + var transactions: [EthTransactionShort] = try await erc20ApiService.requestApiCore { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: EthWalletService.transactionsListApiSubpath, method: .get, parameters: txQueryParameters, diff --git a/Adamant/Modules/Wallets/Ethereum/EthApiCore.swift b/Adamant/Modules/Wallets/Ethereum/EthApiCore.swift index 833f3160a..30f444c79 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthApiCore.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthApiCore.swift @@ -17,10 +17,10 @@ actor EthApiCore { private var web3Cache: [URL: Web3] = .init() func performRequest( - node: Node, + origin: NodeOrigin, _ body: @escaping @Sendable (_ web3: Web3) async throws -> Success ) async -> WalletServiceResult { - switch await getWeb3(node: node) { + switch await getWeb3(origin: origin) { case let .success(web3): do { return .success(try await body(web3)) @@ -43,11 +43,11 @@ actor EthApiCore { } extension EthApiCore: BlockchainHealthCheckableService { - func getStatusInfo(node: Node) async -> WalletServiceResult { + func getStatusInfo(origin: NodeOrigin) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 let response = await apiCore.sendRequestRPC( - node: node, + origin: origin, path: .empty, requests: [ .init(method: EthApiComand.blockNumberMethod), @@ -85,14 +85,14 @@ extension EthApiCore: BlockchainHealthCheckableService { height: Int(height), wsEnabled: false, wsPort: nil, - version: extractVersion(from: clientVersion) + version: extractVersion(from: clientVersion).flatMap { .init($0) } )) } } private extension EthApiCore { - func getWeb3(node: Node) async -> WalletServiceResult { - guard let url = node.asURL() else { + func getWeb3(origin: NodeOrigin) async -> WalletServiceResult { + guard let url = origin.asURL() else { return .failure(.internalError(.endpointBuildFailed)) } diff --git a/Adamant/Modules/Wallets/Ethereum/EthApiService.swift b/Adamant/Modules/Wallets/Ethereum/EthApiService.swift index 701bad3d2..e30e81b8a 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthApiService.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthApiService.swift @@ -11,15 +11,19 @@ import Foundation import web3swift import Web3Core -class EthApiService: WalletApiService { +class EthApiService: ApiServiceProtocol { let api: BlockchainHealthCheckWrapper var keystoreManager: KeystoreManager? { get async { await api.service.keystoreManager } } - var preferredNodeIds: [UUID] { - api.preferredNodeIds + var chosenFastestNodeId: UUID? { + api.chosenFastestNodeId + } + + var hasActiveNode: Bool { + !api.sortedAllowedNodes.isEmpty } init(api: BlockchainHealthCheckWrapper) { @@ -33,22 +37,22 @@ class EthApiService: WalletApiService { func requestWeb3( _ request: @Sendable @escaping (Web3) async throws -> Output ) async -> WalletServiceResult { - await api.request { core, node in - await core.performRequest(node: node, request) + await api.request { core, origin in + await core.performRequest(origin: origin, request) } } func requestApiCore( - _ request: @Sendable @escaping (APICoreProtocol, Node) async -> ApiServiceResult + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult ) async -> WalletServiceResult { - await api.request { core, node in - await request(core.apiCore, node).mapError { $0.asWalletServiceError() } + await api.request { core, origin in + await request(core.apiCore, origin).mapError { $0.asWalletServiceError() } } } func getStatusInfo() async -> WalletServiceResult { - await api.request { core, node in - await core.getStatusInfo(node: node) + await api.request { core, origin in + await core.getStatusInfo(origin: origin) } } diff --git a/Adamant/Modules/Wallets/Ethereum/EthWallet.swift b/Adamant/Modules/Wallets/Ethereum/EthWallet.swift index d279d07fc..0f4ac11b9 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWallet.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWallet.swift @@ -11,6 +11,7 @@ import web3swift import Web3Core final class EthWallet: WalletAccount { + var unicId: String let address: String let ethAddress: EthereumAddress let keystore: BIP32Keystore @@ -21,7 +22,13 @@ final class EthWallet: WalletAccount { var minAmount: Decimal = 0 var isBalanceInitialized: Bool = false - init(address: String, ethAddress: EthereumAddress, keystore: BIP32Keystore) { + init( + unicId: String, + address: String, + ethAddress: EthereumAddress, + keystore: BIP32Keystore + ) { + self.unicId = unicId self.address = address self.ethAddress = ethAddress self.keystore = keystore diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift index 7bd4efbcf..51b00bc72 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift @@ -19,7 +19,7 @@ struct EthWalletFactory: WalletFactory { func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { EthWalletViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, accountService: assembler.resolve(AccountService.self)!, screensFactory: screensFactory, walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, @@ -43,12 +43,12 @@ struct EthWalletFactory: WalletFactory { accountsProvider: assembler.resolve(AccountsProvider.self)!, dialogService: assembler.resolve(DialogService.self)!, screensFactory: screensFactory, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - nodesStorage: assembler.resolve(NodesStorageProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } @@ -128,7 +128,7 @@ private extension EthWalletFactory { func makeTransactionDetailsVC(service: Service) -> EthTransactionDetailsViewController { EthTransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, accountService: assembler.resolve(AccountService.self)!, walletService: service, diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService+DynamicConstants.swift index 113a0f217..2e6db1660 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService+DynamicConstants.swift @@ -14,7 +14,7 @@ extension EthWalletService { crucialUpdateInterval: 30, onScreenUpdateInterval: 10, threshold: 5, - normalServiceUpdateInterval: 300, + normalServiceUpdateInterval: 330, crucialServiceUpdateInterval: 30, onScreenServiceUpdateInterval: 10 ) @@ -48,7 +48,7 @@ extension EthWalletService { } var defaultGasPriceGwei: BigUInt { - 30 + 10 } var defaultGasLimit: BigUInt { @@ -56,7 +56,7 @@ extension EthWalletService { } var warningGasPriceGwei: BigUInt { - 70 + 25 } var tokenName: String { @@ -95,8 +95,8 @@ extension EthWalletService { static var nodes: [Node] { [ - Node(url: URL(string: "https://ethnode2.adamant.im")!, altUrl: URL(string: "http://95.216.114.252:44099")), -Node(url: URL(string: "https://ethnode3.adamant.im")!, altUrl: URL(string: "http://46.4.37.157:44099")), + Node.makeDefaultNode(url: URL(string: "https://ethnode2.adamant.im")!, altUrl: URL(string: "http://95.216.114.252:44099")), +Node.makeDefaultNode(url: URL(string: "https://ethnode3.adamant.im")!, altUrl: URL(string: "http://46.4.37.157:44099")), ] } diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift index fb14484ae..658a9a13f 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift @@ -18,13 +18,19 @@ import CommonKit struct EthWalletStorage { let keystore: BIP32Keystore - + let unicId: String + func getWallet() -> EthWallet? { guard let ethAddress = keystore.addresses?.first else { return nil } - return EthWallet(address: ethAddress.address, ethAddress: ethAddress, keystore: keystore) + return EthWallet( + unicId: unicId, + address: ethAddress.address, + ethAddress: ethAddress, + keystore: keystore + ) } } @@ -123,7 +129,7 @@ final class EthWalletService: WalletCoreProtocol { // MARK: - Dependencies weak var accountService: AccountService? - var apiService: ApiService! + var apiService: AdamantApiServiceProtocol! var ethApiService: EthApiService! var dialogService: DialogService! var increaseFeeService: IncreaseFeeService! @@ -157,6 +163,10 @@ final class EthWalletService: WalletCoreProtocol { $hasMoreOldTransactions.eraseToAnyPublisher() } + var hasActiveNode: Bool { + apiService.hasActiveNode + } + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( coinId: tokenUnicID, coreDataStack: coreDataStack, @@ -387,7 +397,7 @@ extension EthWalletService { throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: nil) } - walletStorage = .init(keystore: store) + walletStorage = .init(keystore: store, unicId: tokenUnicID) await ethApiService.setKeystoreManager(.init([store])) } catch { throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: error) @@ -499,7 +509,7 @@ extension EthWalletService: SwinjectDependentService { @MainActor func injectDependencies(from container: Container) { accountService = container.resolve(AccountService.self) - apiService = container.resolve(ApiService.self) + apiService = container.resolve(AdamantApiServiceProtocol.self) dialogService = container.resolve(DialogService.self) increaseFeeService = container.resolve(IncreaseFeeService.self) ethApiService = container.resolve(EthApiService.self) @@ -701,9 +711,9 @@ extension EthWalletService { "contract_to": "eq." ] - let transactionsFrom: [EthTransactionShort] = try await ethApiService.requestApiCore { core, node in + let transactionsFrom: [EthTransactionShort] = try await ethApiService.requestApiCore { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: EthWalletService.transactionsListApiSubpath, method: .get, parameters: txFromQueryParameters, @@ -711,9 +721,9 @@ extension EthWalletService { ) }.get() - let transactionsTo: [EthTransactionShort] = try await ethApiService.requestApiCore { core, node in + let transactionsTo: [EthTransactionShort] = try await ethApiService.requestApiCore { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: EthWalletService.transactionsListApiSubpath, method: .get, parameters: txToQueryParameters, diff --git a/Adamant/Modules/Wallets/Klayr/KLYWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Klayr/KLYWalletService+DynamicConstants.swift index b58ec6f05..7186b1e6c 100644 --- a/Adamant/Modules/Wallets/Klayr/KLYWalletService+DynamicConstants.swift +++ b/Adamant/Modules/Wallets/Klayr/KLYWalletService+DynamicConstants.swift @@ -75,15 +75,21 @@ extension KlyWalletService { static var nodes: [Node] { [ - Node(url: URL(string: "https://klynode1.adamant.im")!, altUrl: URL(string: "http://195.26.255.137:44099")), -Node(url: URL(string: "https://klynode2.adamant.im")!, altUrl: URL(string: "http://109.176.199.130:44099")), + Node.makeDefaultNode(url: URL(string: "https://klynode2.adamant.im")!, altUrl: URL(string: "http://109.176.199.130:44099")), +Node.makeDefaultNode(url: URL(string: "https://klynode3.adm.im")!, altUrl: URL(string: "http://37.27.205.78:44099")), ] } static var serviceNodes: [Node] { [ - Node(url: URL(string: "https://klyservice1.adamant.im")!), -Node(url: URL(string: "https://klyservice2.adamant.im")!), + Node.makeDefaultNode( + url: URL(string: "https://klyservice2.adamant.im")!, + altUrl: URL(string: "http://109.176.199.130:44098")! + ), + Node.makeDefaultNode( + url: URL(string: "https://klyservice3.adm.im")!, + altUrl: URL(string: "http://37.27.205.78:44098")! + ), ] } } diff --git a/Adamant/Modules/Wallets/Klayr/KlyApiCore.swift b/Adamant/Modules/Wallets/Klayr/KlyApiCore.swift index 769cdc204..bdf69dc95 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyApiCore.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyApiCore.swift @@ -11,33 +11,33 @@ import Foundation import LiskKit class KlyApiCore: BlockchainHealthCheckableService { - func makeClient(node: CommonKit.Node) -> APIClient { + func makeClient(origin: NodeOrigin) -> APIClient { .init(options: .init( - nodes: [.init(origin: node.asString())], + nodes: [.init(origin: origin.asString())], nethash: .mainnet, randomNode: false )) } func request( - node: CommonKit.Node, + origin: NodeOrigin, body: @escaping @Sendable ( _ client: APIClient, _ completion: @escaping @Sendable (LiskKit.Result) -> Void ) -> Void ) async -> WalletServiceResult { await withCheckedContinuation { continuation in - body(makeClient(node: node)) { result in + body(makeClient(origin: origin)) { result in continuation.resume(returning: result.asWalletServiceResult()) } } } func request( - node: CommonKit.Node, + origin: NodeOrigin, _ body: @Sendable @escaping (APIClient) async throws -> Output ) async -> WalletServiceResult { - let client = makeClient(node: node) + let client = makeClient(origin: origin) do { return .success(try await body(client)) @@ -46,10 +46,10 @@ class KlyApiCore: BlockchainHealthCheckableService { } } - func getStatusInfo(node: CommonKit.Node) async -> WalletServiceResult { + func getStatusInfo(origin: NodeOrigin) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 - return await request(node: node) { client in + return await request(origin: origin) { client in try await LiskKit.Node(client: client).info() }.map { model in .init( @@ -57,7 +57,7 @@ class KlyApiCore: BlockchainHealthCheckableService { height: model.height ?? .zero, wsEnabled: false, wsPort: nil, - version: model.version + version: .init(model.version) ) } } diff --git a/Adamant/Modules/Wallets/Klayr/KlyNodeApiService.swift b/Adamant/Modules/Wallets/Klayr/KlyNodeApiService.swift index 885bbd5da..f8263c3b0 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyNodeApiService.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyNodeApiService.swift @@ -8,12 +8,17 @@ import LiskKit import Foundation +import CommonKit -final class KlyNodeApiService: WalletApiService { +final class KlyNodeApiService: ApiServiceProtocol { let api: BlockchainHealthCheckWrapper - var preferredNodeIds: [UUID] { - api.preferredNodeIds + var chosenFastestNodeId: UUID? { + api.chosenFastestNodeId + } + + var hasActiveNode: Bool { + !api.sortedAllowedNodes.isEmpty } init(api: BlockchainHealthCheckWrapper) { @@ -52,8 +57,8 @@ final class KlyNodeApiService: WalletApiService { } func getStatusInfo() async -> WalletServiceResult { - await api.request { core, node in - await core.getStatusInfo(node: node) + await api.request { core, origin in + await core.getStatusInfo(origin: origin) } } } @@ -65,16 +70,16 @@ private extension KlyNodeApiService { _ completion: @escaping @Sendable (LiskKit.Result) -> Void ) -> Void ) async -> WalletServiceResult { - await api.request { core, node in - await core.request(node: node, body: body) + await api.request { core, origin in + await core.request(origin: origin, body: body) } } func requestClient( _ body: @Sendable @escaping (APIClient) async throws -> Output ) async -> WalletServiceResult { - await api.request { core, node in - await core.request(node: node, body) + await api.request { core, origin in + await core.request(origin: origin, body) } } } diff --git a/Adamant/Modules/Wallets/Klayr/KlyServiceApiService.swift b/Adamant/Modules/Wallets/Klayr/KlyServiceApiService.swift index b34ded0eb..5d93aa1e1 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyServiceApiService.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyServiceApiService.swift @@ -12,11 +12,11 @@ import CommonKit final class KlyServiceApiCore: KlyApiCore { override func getStatusInfo( - node: CommonKit.Node + origin: NodeOrigin ) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 - return await request(node: node) { client in + return await request(origin: origin) { client in let service = LiskKit.Service(client: client) return try await (fee: service.fees(), info: service.info()) }.map { model in @@ -25,17 +25,21 @@ final class KlyServiceApiCore: KlyApiCore { height: .init(model.fee.meta.lastBlockHeight), wsEnabled: false, wsPort: nil, - version: model.info.version + version: .init(model.info.version) ) } } } -final class KlyServiceApiService: WalletApiService { +final class KlyServiceApiService: ApiServiceProtocol { let api: BlockchainHealthCheckWrapper - var preferredNodeIds: [UUID] { - api.preferredNodeIds + var chosenFastestNodeId: UUID? { + api.chosenFastestNodeId + } + + var hasActiveNode: Bool { + !api.sortedAllowedNodes.isEmpty } init(api: BlockchainHealthCheckWrapper) { @@ -73,16 +77,16 @@ private extension KlyServiceApiService { _ completion: @escaping @Sendable (LiskKit.Result) -> Void ) -> Void ) async -> WalletServiceResult { - await api.request { core, node in - await core.request(node: node, body: body) + await api.request { core, origin in + await core.request(origin: origin, body: body) } } func requestClient( _ body: @Sendable @escaping (APIClient) async throws -> Output ) async -> WalletServiceResult { - await api.request { core, node in - await core.request(node: node, body) + await api.request { core, origin in + await core.request(origin: origin, body) } } } diff --git a/Adamant/Modules/Wallets/Klayr/KlyTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Klayr/KlyTransactionDetailsViewController.swift index 872d8a640..4b44cc1ef 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyTransactionDetailsViewController.swift @@ -28,6 +28,9 @@ final class KlyTransactionDetailsViewController: TransactionDetailsViewControlle return control }() + override var showTxBlockchainComment: Bool { + true + } // MARK: - Lifecycle override func viewDidLoad() { @@ -76,6 +79,7 @@ final class KlyTransactionDetailsViewController: TransactionDetailsViewControlle trs.updateConfirmations(value: lastHeight) transaction = trs updateIncosinstentRowIfNeeded() + updateTxDataRow() tableView.reloadData() refreshControl.endRefreshing() } catch { diff --git a/Adamant/Modules/Wallets/Klayr/KlyTransactionsViewController.swift b/Adamant/Modules/Wallets/Klayr/KlyTransactionsViewController.swift index 5106ab376..eca2bf136 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyTransactionsViewController.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyTransactionsViewController.swift @@ -121,6 +121,10 @@ extension Transactions.TransactionModel: TransactionDetails { var sentDate: Date? { timestamp.map { Date(timeIntervalSince1970: TimeInterval($0)) } } + + var txBlockchainComment: String? { + txData + } } extension LocalTransaction: TransactionDetails { diff --git a/Adamant/Modules/Wallets/Klayr/KlyWallet.swift b/Adamant/Modules/Wallets/Klayr/KlyWallet.swift index 939e2255b..90b565938 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyWallet.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyWallet.swift @@ -11,6 +11,7 @@ import CommonKit import LiskKit final class KlyWallet: WalletAccount { + var unicId: String let legacyAddress: String let kly32Address: String let keyPair: KeyPair @@ -33,11 +34,13 @@ final class KlyWallet: WalletAccount { } init( + unicId: String, address: String, keyPair: KeyPair, nonce: UInt64, isNewApi: Bool ) { + self.unicId = unicId self.legacyAddress = address self.keyPair = keyPair self.isNewApi = isNewApi diff --git a/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift b/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift index cb5f85e33..f09d2701a 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift @@ -20,7 +20,7 @@ struct KlyWalletFactory: WalletFactory { func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { KlyWalletViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, accountService: assembler.resolve(AccountService.self)!, screensFactory: screensFactory, walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, @@ -44,12 +44,12 @@ struct KlyWalletFactory: WalletFactory { accountsProvider: assembler.resolve(AccountsProvider.self)!, dialogService: assembler.resolve(DialogService.self)!, screensFactory: screensFactory, - currencyInfoService: assembler.resolve(CurrencyInfoService.self)!, + currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, - nodesStorage: assembler.resolve(NodesStorageProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } @@ -129,7 +129,7 @@ private extension KlyWalletFactory { func makeTransactionDetailsVC(service: Service) -> KlyTransactionDetailsViewController { KlyTransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, - currencyInfo: assembler.resolve(CurrencyInfoService.self)!, + currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, accountService: assembler.resolve(AccountService.self)!, walletService: service, diff --git a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift index 82ccbcbca..4114041a0 100644 --- a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift +++ b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift @@ -18,7 +18,7 @@ final class KlyWalletService: WalletCoreProtocol { // MARK: Dependencies - var apiService: ApiService! + var apiService: AdamantApiServiceProtocol! var klyNodeApiService: KlyNodeApiService! var klyServiceApiService: KlyServiceApiService! var accountService: AccountService! @@ -32,6 +32,10 @@ final class KlyWalletService: WalletCoreProtocol { static let currencyLogo = UIImage.asset(named: "klayr_wallet") ?? .init() static let kvsAddress = "kly:address" static let defaultFee: BigUInt = 141000 + + var hasActiveNode: Bool { + apiService.hasActiveNode + } @Atomic var transactionFeeRaw: BigUInt = BigUInt(integerLiteral: 141000) @@ -153,7 +157,7 @@ extension KlyWalletService: SwinjectDependentService { @MainActor func injectDependencies(from container: Container) { accountService = container.resolve(AccountService.self) - apiService = container.resolve(ApiService.self) + apiService = container.resolve(AdamantApiServiceProtocol.self) dialogService = container.resolve(DialogService.self) klyServiceApiService = container.resolve(KlyServiceApiService.self) klyNodeApiService = container.resolve(KlyNodeApiService.self) @@ -395,6 +399,7 @@ private extension KlyWalletService { let address = LiskKit.Crypto.address(fromPublicKey: keyPair.publicKeyString) let wallet = KlyWallet( + unicId: tokenUnicID, address: address, keyPair: keyPair, nonce: .zero, diff --git a/Adamant/Modules/Wallets/Models/TransactionDetails.swift b/Adamant/Modules/Wallets/Models/TransactionDetails.swift index 3384f61bb..de0953454 100644 --- a/Adamant/Modules/Wallets/Models/TransactionDetails.swift +++ b/Adamant/Modules/Wallets/Models/TransactionDetails.swift @@ -47,6 +47,8 @@ protocol TransactionDetails { var nonceRaw: String? { get } + var txBlockchainComment: String? { get } + func summary( with url: String?, currentValue: String?, @@ -57,6 +59,8 @@ protocol TransactionDetails { extension TransactionDetails { var feeCurrencySymbol: String? { defaultCurrencySymbol } + var txBlockchainComment: String? { nil } + func summary( with url: String? = nil, currentValue: String? = nil, diff --git a/Adamant/Modules/Wallets/TransactionDetailsViewControllerBase.swift b/Adamant/Modules/Wallets/TransactionDetailsViewControllerBase.swift index 0bda84cb3..c403fcdf1 100644 --- a/Adamant/Modules/Wallets/TransactionDetailsViewControllerBase.swift +++ b/Adamant/Modules/Wallets/TransactionDetailsViewControllerBase.swift @@ -15,9 +15,9 @@ import CommonKit 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.good + case .failed: return .adamant.warning + case .notInitiated, .inconsistent, .noNetwork, .noNetworkFinal, .pending, .registered: return .adamant.attention + case .success: return .adamant.success } } @@ -71,6 +71,7 @@ class TransactionDetailsViewControllerBase: FormViewController { case historyFiat case currentFiat case inconsistentReason + case txBlockchainComment var tag: String { switch self { @@ -89,6 +90,7 @@ class TransactionDetailsViewControllerBase: FormViewController { case .historyFiat: return "hfiat" case .currentFiat: return "cfiat" case .inconsistentReason: return "incReason" + case .txBlockchainComment: return "data" } } @@ -110,6 +112,9 @@ class TransactionDetailsViewControllerBase: FormViewController { case .currentFiat: return .localized("TransactionDetailsScene.Row.CurrentFiat", comment: "Transaction details: current fiat value") case .inconsistentReason: return .localized("TransactionStatus.Inconsistent.Reason.Title", comment: "Transaction status: inconsistent reason title") + case .txBlockchainComment: + return + .localized("TransactionStatus.Inconsistent.RecordData.Title", comment: "Transaction details: Tx data record") } } @@ -152,7 +157,7 @@ class TransactionDetailsViewControllerBase: FormViewController { // MARK: - Dependencies let dialogService: DialogService - let currencyInfo: CurrencyInfoService + let currencyInfo: InfoServiceProtocol let addressBookService: AddressBookService let accountService: AccountService let walletService: WalletService? @@ -160,6 +165,10 @@ class TransactionDetailsViewControllerBase: FormViewController { // MARK: - Properties + var showTxBlockchainComment: Bool { + false + } + var transaction: TransactionDetails? { didSet { if !isFiatSet { @@ -244,7 +253,7 @@ class TransactionDetailsViewControllerBase: FormViewController { init( dialogService: DialogService, - currencyInfo: CurrencyInfoService, + currencyInfo: InfoServiceProtocol, addressBookService: AddressBookService, accountService: AccountService, walletService: WalletService?, @@ -647,6 +656,52 @@ class TransactionDetailsViewControllerBase: FormViewController { detailsSection.append(fiatRow) + // MARK: Tx blockchain comment + let txBlockchainComment = LabelRow { + $0.disabled = true + $0.tag = Rows.txBlockchainComment.tag + $0.title = Rows.txBlockchainComment.localized + + if let value = transaction?.txBlockchainComment { + $0.value = value + } else { + $0.value = TransactionDetailsViewControllerBase.awaitingValueString + } + + $0.cell.detailTextLabel?.textAlignment = .right + $0.cell.detailTextLabel?.lineBreakMode = .byTruncatingMiddle + + $0.hidden = Condition.function([], { [weak self] _ -> Bool in + guard let value = self?.transaction?.txBlockchainComment else { + return false + } + + if value.isEmpty { + return true + } + return false + }) + + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + cell.textLabel?.textColor = UIColor.adamant.textColor + }.onCellSelection { [weak self] (cell, row) in + if let text = row.value { + self?.shareValue(text, from: cell) + } + }.cellUpdate { [weak self] (cell, row) in + cell.textLabel?.textColor = UIColor.adamant.textColor + if let value = self?.transaction?.txBlockchainComment { + row.value = value + } else { + row.value = TransactionDetailsViewControllerBase.awaitingValueString + } + } + + if showTxBlockchainComment { + detailsSection.append(txBlockchainComment) + } + // MARK: Comments section if let comment = comment { @@ -772,14 +827,17 @@ class TransactionDetailsViewControllerBase: FormViewController { Task { var tickers = try await currencyInfo.getHistory( for: currencySymbol, - timestamp: date - ) + date: date + ).get() isFiatSet = true - guard var ticker = tickers["\(currencySymbol)/\(currentFiat)"] else { - return - } + guard + var ticker = tickers[.init( + crypto: currencySymbol, + fiat: currentFiat + )] + else { return } let totalFiat = amount * ticker valueAtTimeTxn = fiatFormatter.string(from: totalFiat) @@ -790,12 +848,15 @@ class TransactionDetailsViewControllerBase: FormViewController { feeCurrencySymbol != currencySymbol { tickers = try await currencyInfo.getHistory( for: feeCurrencySymbol, - timestamp: date - ) + date: date + ).get() - guard let feeTicker = tickers["\(feeCurrencySymbol)/\(currentFiat)"] else { - return - } + guard + let feeTicker = tickers[.init( + crypto: feeCurrencySymbol, + fiat: currentFiat + )] + else { return } ticker = feeTicker } @@ -815,6 +876,11 @@ class TransactionDetailsViewControllerBase: FormViewController { checkAddressesIfNeeded() } + func updateTxDataRow() { + let row = form.rowBy(tag: Rows.txBlockchainComment.tag) + row?.evaluateHidden() + } + // MARK: - Other private func setColors() { diff --git a/Adamant/Modules/Wallets/TransactionTableViewCell.swift b/Adamant/Modules/Wallets/TransactionTableViewCell.swift index 619b4ca6b..3238d4be7 100644 --- a/Adamant/Modules/Wallets/TransactionTableViewCell.swift +++ b/Adamant/Modules/Wallets/TransactionTableViewCell.swift @@ -236,9 +236,9 @@ private extension TransactionStatus { var color: UIColor { switch self { case .failed: - return .adamant.danger + return .adamant.warning case .pending, .registered: - return .adamant.alert + return .adamant.attention default: return .adamant.secondary } diff --git a/Adamant/Modules/Wallets/TransactionsListViewControllerBase.swift b/Adamant/Modules/Wallets/TransactionsListViewControllerBase.swift index 861f2f52e..a61cec1c8 100644 --- a/Adamant/Modules/Wallets/TransactionsListViewControllerBase.swift +++ b/Adamant/Modules/Wallets/TransactionsListViewControllerBase.swift @@ -375,9 +375,9 @@ extension TransactionsListViewControllerBase: UITableViewDelegate { private extension TransactionStatus { var color: UIColor { switch self { - case .failed: return .adamant.danger + case .failed: return .adamant.warning case .notInitiated, .inconsistent, .noNetwork, .noNetworkFinal, .pending, .registered: - return .adamant.alert + return .adamant.attention case .success: return .adamant.secondary } } diff --git a/Adamant/Modules/Wallets/TransferViewControllerBase.swift b/Adamant/Modules/Wallets/TransferViewControllerBase.swift index 6c5065f5b..7d69b9c5f 100644 --- a/Adamant/Modules/Wallets/TransferViewControllerBase.swift +++ b/Adamant/Modules/Wallets/TransferViewControllerBase.swift @@ -180,14 +180,14 @@ class TransferViewControllerBase: FormViewController { let accountsProvider: AccountsProvider let dialogService: DialogService let screensFactory: ScreensFactory - let currencyInfoService: CurrencyInfoService + let currencyInfoService: InfoServiceProtocol var increaseFeeService: IncreaseFeeService var chatsProvider: ChatsProvider let vibroService: VibroService let walletService: WalletService let walletCore: WalletCoreProtocol let reachabilityMonitor: ReachabilityMonitor - let nodesStorage: NodesStorageProtocol + let apiServiceCompose: ApiServiceComposeProtocol // MARK: - Properties @@ -314,12 +314,12 @@ class TransferViewControllerBase: FormViewController { accountsProvider: AccountsProvider, dialogService: DialogService, screensFactory: ScreensFactory, - currencyInfoService: CurrencyInfoService, + currencyInfoService: InfoServiceProtocol, increaseFeeService: IncreaseFeeService, vibroService: VibroService, walletService: WalletService, reachabilityMonitor: ReachabilityMonitor, - nodesStorage: NodesStorageProtocol + apiServiceCompose: ApiServiceComposeProtocol ) { self.accountService = accountService self.accountsProvider = accountsProvider @@ -332,8 +332,7 @@ class TransferViewControllerBase: FormViewController { self.walletService = walletService self.walletCore = walletService.core self.reachabilityMonitor = reachabilityMonitor - self.nodesStorage = nodesStorage - + self.apiServiceCompose = apiServiceCompose super.init(nibName: nil, bundle: nil) } @@ -682,7 +681,7 @@ class TransferViewControllerBase: FormViewController { func markRow(_ row: BaseRowType, valid: Bool) { row.baseCell.textLabel?.textColor = valid ? getBaseColor(for: row.tag) - : UIColor.adamant.alert + : UIColor.adamant.attention } func getBaseColor(for tag: String?) -> UIColor { @@ -703,13 +702,13 @@ class TransferViewControllerBase: FormViewController { } if let label = recipientSection.header?.viewForSection(recipientSection, type: .header), let label = label as? UILabel { - label.textColor = isValid ? UIColor.adamant.primary : UIColor.adamant.alert + label.textColor = isValid ? UIColor.adamant.primary : UIColor.adamant.attention } - recipientRow.cell.textField.textColor = isValid ? UIColor.adamant.primary : UIColor.adamant.alert + recipientRow.cell.textField.textColor = isValid ? UIColor.adamant.primary : UIColor.adamant.attention recipientRow.cell.textField.leftView?.subviews.forEach { view in guard let label = view as? UILabel else { return } - label.textColor = isValid ? UIColor.adamant.primary : UIColor.adamant.alert + label.textColor = isValid ? UIColor.adamant.primary : UIColor.adamant.attention } } @@ -800,24 +799,21 @@ class TransferViewControllerBase: FormViewController { return } - if admReportRecipient != nil, - !nodesStorage.haveActiveNode(in: .adm) { + guard + apiServiceCompose.hasActiveNode(group: .adm) || admReportRecipient == nil + else { dialogService.showWarning( withMessage: ApiServiceError.noEndpointsAvailable( - coin: NodeGroup.adm.name + nodeGroupName: NodeGroup.adm.name ).localizedDescription ) return } - let groupsWithoutActiveNode = walletCore.nodeGroups.filter { - !nodesStorage.haveActiveNode(in: $0) - } - - if let group = groupsWithoutActiveNode.first { + guard walletCore.hasActiveNode else { dialogService.showWarning( withMessage: ApiServiceError.noEndpointsAvailable( - coin: group.name + nodeGroupName: walletCore.tokenName ).localizedDescription ) return @@ -1169,7 +1165,7 @@ extension TransferViewControllerBase { $0.disabled = true $0.title = BaseRows.fee.localized + estimateSymbol $0.cell.titleLabel.textColor = .adamant.active - $0.cell.secondDetailsLabel.textColor = .adamant.alert + $0.cell.secondDetailsLabel.textColor = .adamant.attention $0.value = self?.getCellFeeValue() } diff --git a/Adamant/Modules/Wallets/WalletAccount.swift b/Adamant/Modules/Wallets/WalletAccount.swift index 923537513..4c1de3708 100644 --- a/Adamant/Modules/Wallets/WalletAccount.swift +++ b/Adamant/Modules/Wallets/WalletAccount.swift @@ -11,6 +11,7 @@ import Foundation // MARK: - Wallet Account protocol WalletAccount { // MARK: Account + var unicId: String { get } var address: String { get } var balance: Decimal { get } var isBalanceInitialized: Bool { get } diff --git a/Adamant/Modules/Wallets/WalletViewControllerBase.swift b/Adamant/Modules/Wallets/WalletViewControllerBase.swift index 3209d6fec..3b254b0a9 100644 --- a/Adamant/Modules/Wallets/WalletViewControllerBase.swift +++ b/Adamant/Modules/Wallets/WalletViewControllerBase.swift @@ -48,7 +48,7 @@ class WalletViewControllerBase: FormViewController, WalletViewController { // MARK: - Dependencies - private let currencyInfoService: CurrencyInfoService + private let currencyInfoService: InfoServiceProtocol private let accountService: AccountService private let walletServiceCompose: WalletServiceCompose @@ -84,7 +84,7 @@ class WalletViewControllerBase: FormViewController, WalletViewController { init( dialogService: DialogService, - currencyInfoService: CurrencyInfoService, + currencyInfoService: InfoServiceProtocol, accountService: AccountService, screensFactory: ScreensFactory, walletServiceCompose: WalletServiceCompose, diff --git a/Adamant/Modules/Wallets/WalletsService/WalletCoreProtocol.swift b/Adamant/Modules/Wallets/WalletsService/WalletCoreProtocol.swift index a204a8342..253d65558 100644 --- a/Adamant/Modules/Wallets/WalletsService/WalletCoreProtocol.swift +++ b/Adamant/Modules/Wallets/WalletsService/WalletCoreProtocol.swift @@ -124,17 +124,8 @@ extension WalletServiceError: HealthCheckableError { } } - var isRequestCancelledError: Bool { - switch self { - case .requestCancelled: - return true - default: - return false - } - } - - static func noEndpointsError(coin: String) -> WalletServiceError { - .apiError(.noEndpointsError(coin: coin)) + static func noEndpointsError(nodeGroupName: String) -> WalletServiceError { + .apiError(.noEndpointsError(nodeGroupName: nodeGroupName)) } } @@ -290,6 +281,7 @@ protocol WalletCoreProtocol: AnyObject { var enabled: Bool { get } // MARK: Logic + var hasActiveNode: Bool { get } func update() // MARK: Tools diff --git a/Adamant/Release.entitlements b/Adamant/Release.entitlements index 854da1316..85b260349 100644 --- a/Adamant/Release.entitlements +++ b/Adamant/Release.entitlements @@ -20,6 +20,10 @@ $(TeamIdentifierPrefix)$(CFBundleIdentifier) com.apple.security.app-sandbox + com.apple.security.application-groups + + group.adamant.adamant-messenger + com.apple.security.device.camera com.apple.security.network.client diff --git a/Adamant/ServiceProtocols/ApiServiceComposeProtocol.swift b/Adamant/ServiceProtocols/ApiServiceComposeProtocol.swift new file mode 100644 index 000000000..16e0a5b17 --- /dev/null +++ b/Adamant/ServiceProtocols/ApiServiceComposeProtocol.swift @@ -0,0 +1,16 @@ +// +// ApiServiceComposeProtocol.swift +// Adamant +// +// Created by Andrew G on 21.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +protocol ApiServiceComposeProtocol { + func chosenFastestNodeId(group: NodeGroup) -> UUID? + func hasActiveNode(group: NodeGroup) -> Bool + func healthCheck(group: NodeGroup) +} diff --git a/Adamant/ServiceProtocols/FileApiServiceProtocol.swift b/Adamant/ServiceProtocols/FileApiServiceProtocol.swift index 8a1d30942..08ef445d2 100644 --- a/Adamant/ServiceProtocols/FileApiServiceProtocol.swift +++ b/Adamant/ServiceProtocols/FileApiServiceProtocol.swift @@ -7,8 +7,9 @@ // import Foundation +import CommonKit -protocol FileApiServiceProtocol: WalletApiService { +protocol FileApiServiceProtocol: ApiServiceProtocol { func uploadFile( data: Data, uploadProgress: @escaping ((Progress) -> Void) diff --git a/Adamant/ServiceProtocols/NodesStorageProtocol.swift b/Adamant/ServiceProtocols/NodesStorageProtocol.swift deleted file mode 100644 index be9077df9..000000000 --- a/Adamant/ServiceProtocols/NodesStorageProtocol.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// 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 haveActiveNode(in group: NodeGroup) -> Bool - - 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/NotificationsService.swift b/Adamant/ServiceProtocols/NotificationsService.swift index ca35842e3..528df9f97 100644 --- a/Adamant/ServiceProtocols/NotificationsService.swift +++ b/Adamant/ServiceProtocols/NotificationsService.swift @@ -35,6 +35,19 @@ enum NotificationSound: String { case proud case relax case success + case note + case antic + case cheers + case chord + case droplet + case handoff + case milestone + case passage + case portal + case rattle + case rebound + case slide + case welcome var tag: String { switch self { @@ -44,6 +57,19 @@ enum NotificationSound: String { case .proud: return "pr" case .relax: return "rl" case .success: return "sh" + case .note: return "note" + case .antic: return "antic" + case .cheers: return "chrs" + case .chord: return "chord" + case .droplet: return "droplet" + case .handoff: return "hnoff" + case .milestone: return "mlst" + case .passage: return "psg" + case .portal: return "portal" + case .rattle: return "rattle" + case .rebound: return "rbnd" + case .slide: return "slide" + case .welcome: return "welcome" } } @@ -55,6 +81,19 @@ enum NotificationSound: String { case .proud: return "so-proud-notification.mp3" case .relax: return "relax-message-tone.mp3" case .success: return "short-success.mp3" + case .note: return "note.mp3" + case .antic: return "antic.mp3" + case .cheers: return "cheers.mp3" + case .chord: return "chord.mp3" + case .droplet: return "droplet.mp3" + case .handoff: return "handoff.mp3" + case .milestone: return "milestone.mp3" + case .passage: return "passage.mp3" + case .portal: return "portal.mp3" + case .rattle: return "rattle.mp3" + case .rebound: return "rebound.mp3" + case .slide: return "slide.mp3" + case .welcome: return "welcome.mp3" } } @@ -66,6 +105,19 @@ enum NotificationSound: String { case .proud: return "Proud" case .relax: return "Relax" case .success: return "Success" + case .note: return "Note" + case .antic: return "Antic" + case .cheers: return "Cheers" + case .chord: return "Chord" + case .droplet: return "Droplet" + case .handoff: return "Handoff" + case .milestone: return "Milestone" + case .passage: return "Passage" + case .portal: return "Portal" + case .rattle: return "Rattle" + case .rebound: return "Rebound" + case .slide: return "Slide" + case .welcome: return "Welcome" } } @@ -76,6 +128,19 @@ enum NotificationSound: String { case "so-proud-notification.mp3": self = .proud case "relax-message-tone.mp3": self = .relax case "short-success.mp3": self = .success + case "note.mp3": self = .note + case "antic.mp3": self = .antic + case "cheers.mp3": self = .cheers + case "chord.mp3": self = .chord + case "droplet.mp3": self = .droplet + case "handoff.mp3": self = .handoff + case "milestone.mp3": self = .milestone + case "passage.mp3": self = .passage + case "portal.mp3": self = .portal + case "rattle.mp3": self = .rattle + case "rebound.mp3": self = .rebound + case "slide.mp3": self = .slide + case "welcome.mp3": self = .welcome case "": self = .none default: self = .inputDefault } @@ -176,8 +241,19 @@ extension NotificationsServiceError: RichError { protocol NotificationsService: AnyObject { var notificationsMode: NotificationsMode { get } var notificationsSound: NotificationSound { get } + var notificationsReactionSound: NotificationSound { get } + var inAppSound: Bool { get } + var inAppVibrate: Bool { get } + var inAppToasts: Bool { get } - func setNotificationSound(_ sound: NotificationSound) + func setInAppSound(_ value: Bool) + func setInAppVibrate(_ value: Bool) + func setInAppToasts(_ value: Bool) + + func setNotificationSound( + _ sound: NotificationSound, + for target: NotificationTarget + ) func setNotificationsMode(_ mode: NotificationsMode, completion: ((NotificationsServiceResult) -> Void)?) func showNotification(title: String, body: String, type: AdamantNotificationType) diff --git a/Adamant/ServiceProtocols/ReachabilityMonitor.swift b/Adamant/ServiceProtocols/ReachabilityMonitor.swift index a58880744..0becf2da0 100644 --- a/Adamant/ServiceProtocols/ReachabilityMonitor.swift +++ b/Adamant/ServiceProtocols/ReachabilityMonitor.swift @@ -7,6 +7,7 @@ // import Foundation +import CommonKit extension Notification.Name { struct AdamantReachabilityMonitor { @@ -26,6 +27,7 @@ extension AdamantUserInfoKey { } protocol ReachabilityMonitor { + var connectionPublisher: AnyObservable { get } var connection: Bool { get } func start() diff --git a/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift b/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift index 399298992..94c5ce365 100644 --- a/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift +++ b/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift @@ -7,6 +7,7 @@ // import Foundation +import CommonKit struct TransactionStatusInfo { let sentDate: Date? diff --git a/Adamant/Services/AdamantAccountService.swift b/Adamant/Services/AdamantAccountService.swift index 94e427c25..ec16b222a 100644 --- a/Adamant/Services/AdamantAccountService.swift +++ b/Adamant/Services/AdamantAccountService.swift @@ -15,14 +15,14 @@ final class AdamantAccountService: AccountService { // MARK: Dependencies - private let apiService: ApiService + private let apiService: AdamantApiServiceProtocol private let adamantCore: AdamantCore private let dialogService: DialogService private let securedStore: SecuredStore private let walletServiceCompose: WalletServiceCompose + private let currencyInfoService: InfoServiceProtocol weak var notificationsService: NotificationsService? - weak var currencyInfoService: CurrencyInfoService? weak var pushNotificationsTokenService: PushNotificationsTokenService? weak var visibleWalletService: VisibleWalletsService? @@ -38,17 +38,19 @@ final class AdamantAccountService: AccountService { @Atomic private var subscriptions = Set() init( - apiService: ApiService, + apiService: AdamantApiServiceProtocol, adamantCore: AdamantCore, dialogService: DialogService, securedStore: SecuredStore, - walletServiceCompose: WalletServiceCompose + walletServiceCompose: WalletServiceCompose, + currencyInfoService: InfoServiceProtocol ) { self.apiService = apiService self.adamantCore = adamantCore self.dialogService = dialogService self.securedStore = securedStore self.walletServiceCompose = walletServiceCompose + self.currencyInfoService = currencyInfoService NotificationCenter.default.addObserver(forName: .AdamantAccountService.forceUpdateBalance, object: nil, queue: OperationQueue.main) { [weak self] _ in self?.update() diff --git a/Adamant/Services/AdamantAddressBookService.swift b/Adamant/Services/AdamantAddressBookService.swift index 8d6993bbc..37bd1dbe8 100644 --- a/Adamant/Services/AdamantAddressBookService.swift +++ b/Adamant/Services/AdamantAddressBookService.swift @@ -18,7 +18,7 @@ final class AdamantAddressBookService: AddressBookService { // MARK: - Dependencies - private let apiService: ApiService + private let apiService: AdamantApiServiceProtocol private let adamantCore: AdamantCore private let accountService: AccountService private let dialogService: DialogService @@ -38,7 +38,7 @@ final class AdamantAddressBookService: AddressBookService { // MARK: - Lifecycle nonisolated init( - apiService: ApiService, + apiService: AdamantApiServiceProtocol, adamantCore: AdamantCore, accountService: AccountService, dialogService: DialogService diff --git a/Adamant/Services/AdamantCurrencyInfoService.swift b/Adamant/Services/AdamantCurrencyInfoService.swift deleted file mode 100644 index 87a32fdb2..000000000 --- a/Adamant/Services/AdamantCurrencyInfoService.swift +++ /dev/null @@ -1,283 +0,0 @@ -// -// AdamantCurrencyInfoService.swift -// Adamant -// -// Created by Anton Boyarkin on 23/03/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import Foundation -import Alamofire -import UIKit -import CommonKit - -extension StoreKey { - struct CoinInfo { - static let selectedCurrency = "coinInfo.selectedCurrency" - } -} - -// MARK: - Service -final class AdamantCurrencyInfoService: CurrencyInfoService { - // MARK: - API - private lazy var infoServiceUrl: URL = { - return URL(string: AdamantResources.coinsInfoSrvice)! - }() - - private enum InfoServiceApiCommands: String { - case get = "/get" - case history = "/getHistory" - } - - private func url(for command: InfoServiceApiCommands, with queryItems: [URLQueryItem]? = nil) -> URL? { - guard var components = URLComponents(url: infoServiceUrl, resolvingAgainstBaseURL: false) else { - fatalError("Failed to build InfoService url") - } - - components.path = command.rawValue - components.queryItems = queryItems - - return try? components.asURL() - } - - // MARK: - Properties - private static let historyThreshold = Double(exactly: 60*60*24)! - - private var rateCoins: [String]? - private var rates = [String: Decimal]() - - private let defaultResponseDispatchQueue = DispatchQueue(label: "com.adamant.info-coins-response-queue", qos: .utility, attributes: [.concurrent]) - - public var currentCurrency: Currency = Currency.default { - didSet { - securedStore.set(currentCurrency.rawValue, for: StoreKey.CoinInfo.selectedCurrency) - NotificationCenter.default.post(name: Notification.Name.AdamantCurrencyInfoService.currencyRatesUpdated, object: nil) - } - } - - private var observerActive: NSObjectProtocol? - - // MARK: - Dependencies - - private let securedStore: SecuredStore - private let walletServiceCompose: WalletServiceCompose - - // MARK: - Init - - init(securedStore: SecuredStore, walletServiceCompose: WalletServiceCompose) { - self.securedStore = securedStore - self.walletServiceCompose = walletServiceCompose - rateCoins = walletServiceCompose.getWallets().map { $0.core.tokenSymbol } - addObservers() - setupSecuredStore() - } - - deinit { - removeObservers() - } - - // MARK: - Observers - func addObservers() { - observerActive = NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in - self?.update() - } - } - - func removeObservers() { - if let observerActive = observerActive { - NotificationCenter.default.removeObserver(observerActive) - } - } - - // MARK: - Info service - func update() { - guard let coins = rateCoins else { - return - } - - loadRates(for: coins) { [weak self] result in - switch result { - case .success(let rates): - self?.rates = rates - NotificationCenter.default.post(name: Notification.Name.AdamantCurrencyInfoService.currencyRatesUpdated, object: nil) - - case .failure(let error): - print("Fail to load rates from server with error: \(error.localizedDescription)") - } - } - } - - func getRate(for coin: String) -> Decimal? { - let currency = currentCurrency.rawValue - let pair = "\(coin)/\(currency)" - - return rates[pair] - } - - private func loadRates(for coins: [String], completion: @escaping (ApiServiceResult<[String: Decimal]>) -> Void) { - guard let url = url(for: .get, with: [URLQueryItem(name: "coin", value: coins.joined(separator: ","))]) else { - completion(.failure(.internalError(message: "Failed to build URL", error: nil))) - return - } - - let headers: HTTPHeaders = [ - "Content-Type": "application/json" - ] - - AF.request(url, method: .get, headers: headers).responseData(queue: defaultResponseDispatchQueue) { response in - switch response.result { - case .success(let data): - do { - let model: CoinInfoServiceResponseGet = try JSONDecoder().decode(CoinInfoServiceResponseGet.self, from: data) - if let result = model.result { - let nonOptionalResult = result.compactMapValues { $0 } - completion(.success(nonOptionalResult)) - } else { - completion(.failure(.serverError(error: "Coin info API result: Parsing error"))) - } - } catch { - completion(.failure(.serverError(error: "Coin info API result: Parsing error"))) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - } - } - - func getHistory(for coin: String, timestamp: Date, completion: @escaping (ApiServiceResult<[String:Decimal]?>) -> Void) { - guard let url = url(for: .history, with: [URLQueryItem(name: "timestamp", value: String(format: "%.0f", timestamp.timeIntervalSince1970)), URLQueryItem(name: "coin", value: coin)]) else { - completion(.failure(.internalError(message: "Failed to build URL", error: nil))) - return - } - - let headers: HTTPHeaders = [ - "Content-Type": "application/json" - ] - - AF.request(url, method: .get, headers: headers).responseData(queue: defaultResponseDispatchQueue) { response in - switch response.result { - case .success(let data): - do { - let model: CoinInfoServiceResponseHistory = try JSONDecoder().decode(CoinInfoServiceResponseHistory.self, from: data) - guard let result = model.result?.first, abs(timestamp.timeIntervalSince(result.date)) < AdamantCurrencyInfoService.historyThreshold else { // Разница в датах не должна превышать суток - completion(.success(nil)) - return - } - - completion(.success(result.tickers)) - } catch { - completion(.failure(.serverError(error: "Coin info API result: Parsing error"))) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - } - } - - func getHistory( - for coin: String, - timestamp: Date - ) async throws -> [String: Decimal] { - try await withUnsafeThrowingContinuation { continuation in - getHistory( - for: coin, - timestamp: timestamp) { completion in - switch completion { - case .success(let result): - continuation.resume(returning: result ?? [:]) - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } - - private func setupSecuredStore() { - if - let id: String = securedStore.get(StoreKey.CoinInfo.selectedCurrency), - let currency = Currency(rawValue: id) - { - currentCurrency = currency - } else { - currentCurrency = Currency.default - } - } -} - -// MARK: - Server responses -struct CoinInfoServiceResponseGet: Decodable { - enum CodingKeys: String, CodingKey { - case success - case date - case result - } - - let success: Bool - let date: Date - let result: [String: Decimal?]? - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.success = try container.decode(Bool.self, forKey: .success) - self.result = try? container.decode([String: Decimal?].self, forKey: .result) - - if let timeInterval = try? container.decode(TimeInterval.self, forKey: .date) { - self.date = Date(timeIntervalSince1970: timeInterval) - } else { - self.date = Date() - } - } -} - -struct CoinInfoServiceResponseHistory: Decodable { - enum CodingKeys: String, CodingKey { - case success - case date - case result - } - - let success: Bool - let date: Date - let result: [CoinInfoServiceHistoryResult]? - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.success = try container.decode(Bool.self, forKey: .success) - self.result = try? container.decode([CoinInfoServiceHistoryResult].self, forKey: .result) - - if let timeInterval = try? container.decode(TimeInterval.self, forKey: .date) { - self.date = Date(timeIntervalSince1970: timeInterval) - } else { - self.date = Date() - } - } -} - -struct CoinInfoServiceHistoryResult: Decodable { - enum CodingKeys: String, CodingKey { - case id = "_id" - case date - case tickers - } - - let id: String - let tickers: [String: Decimal]? - let date: Date - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.id = try container.decode(String.self, forKey: .id) - self.tickers = try? container.decode([String: Decimal].self, forKey: .tickers) - - if let timeInterval = try? container.decode(TimeInterval.self, forKey: .date) { - self.date = Date(timeIntervalSince1970: timeInterval / 1000) // ms, just because - } else { - self.date = Date() - } - } -} diff --git a/Adamant/Services/AdamantDialogService.swift b/Adamant/Services/AdamantDialogService.swift index 57ddad131..577954048 100644 --- a/Adamant/Services/AdamantDialogService.swift +++ b/Adamant/Services/AdamantDialogService.swift @@ -11,19 +11,25 @@ import MessageUI import PopupKit import SafariServices import CommonKit +import AVFoundation @MainActor final class AdamantDialogService: DialogService { // MARK: Dependencies + private let notificationsService: NotificationsService private let vibroService: VibroService private let popupManager = PopupManager() private let mailDelegate = MailDelegate() private weak var window: UIWindow? - nonisolated init(vibroService: VibroService) { + nonisolated init( + vibroService: VibroService, + notificationsService: NotificationsService + ) { self.vibroService = vibroService + self.notificationsService = notificationsService } func setup(window: UIWindow) { @@ -207,6 +213,8 @@ extension AdamantDialogService { // MARK: - Notifications extension AdamantDialogService { func showNotification(title: String?, message: String?, image: UIImage?, tapHandler: (() -> Void)?) { + guard notificationsService.inAppToasts else { return } + popupManager.showNotification( icon: image, title: title, diff --git a/Adamant/Services/AdamantNotificationService.swift b/Adamant/Services/AdamantNotificationService.swift index 2f8095326..030963239 100644 --- a/Adamant/Services/AdamantNotificationService.swift +++ b/Adamant/Services/AdamantNotificationService.swift @@ -11,6 +11,8 @@ import UIKit import UserNotifications import CommonKit import Combine +import CoreData +import AVFoundation extension NotificationsMode { func toRaw() -> String { @@ -26,25 +28,60 @@ extension NotificationsMode { } } +enum NotificationTarget: CaseIterable { + case baseMessage + case reaction + + var storeId: String { + switch self { + case .baseMessage: + return StoreKey.notificationsService.notificationsSound + case .reaction: + return StoreKey.notificationsService.notificationsReactionSound + } + } +} + @MainActor -final class AdamantNotificationsService: NotificationsService { +final class AdamantNotificationsService: NSObject, NotificationsService { // MARK: Dependencies private let securedStore: SecuredStore + private let vibroService: VibroService weak var accountService: AccountService? + weak var chatsProvider: ChatsProvider? // MARK: Properties + private let defaultNotificationsSound: NotificationSound = .inputDefault + private let defaultNotificationsReactionSound: NotificationSound = .none + private let defaultInAppSound: Bool = false + private let defaultInAppVibrate: Bool = true + private let defaultInAppToasts: Bool = true + private(set) var notificationsMode: NotificationsMode = .disabled private(set) var customBadgeNumber = 0 private(set) var notificationsSound: NotificationSound = .inputDefault + private(set) var notificationsReactionSound: NotificationSound = .none + private(set) var inAppSound: Bool = false + private(set) var inAppVibrate: Bool = true + private(set) var inAppToasts: Bool = true + private var isBackgroundSession = false private var backgroundNotifications = 0 private var subscriptions = Set() private var preservedBadgeNumber: Int? + private var audioPlayer: AVAudioPlayer? + private var unreadController: NSFetchedResultsController? // MARK: Lifecycle - nonisolated init(securedStore: SecuredStore) { + nonisolated init( + securedStore: SecuredStore, + vibroService: VibroService + ) { self.securedStore = securedStore + self.vibroService = vibroService + + super.init() Task { @MainActor in NotificationCenter.default @@ -68,53 +105,45 @@ final class AdamantNotificationsService: NotificationsService { } } - private func onUserLoggedIn() { - UNUserNotificationCenter.current().removeAllDeliveredNotifications() - UIApplication.shared.applicationIconBadgeNumber = 0 - - if let raw: String = securedStore.get(StoreKey.notificationsService.notificationsMode), - let mode = NotificationsMode(string: raw) { - setNotificationsMode(mode, completion: nil) - } else { - setNotificationsMode(.disabled, completion: nil) - } - - if let raw: String = securedStore.get(StoreKey.notificationsService.notificationsSound), - let sound = NotificationSound(fileName: raw) { - setNotificationSound(sound) - } else { - setNotificationsMode(.disabled, completion: nil) - } - - preservedBadgeNumber = nil + func setInAppSound(_ value: Bool) { + setValue(for: StoreKey.notificationsService.inAppSounds, value: value) + inAppSound = value } - private func onUserLoggedOut() { - setNotificationsMode(.disabled, completion: nil) - setNotificationSound(.inputDefault) - securedStore.remove(StoreKey.notificationsService.notificationsMode) - securedStore.remove(StoreKey.notificationsService.notificationsSound) - preservedBadgeNumber = nil + func setInAppVibrate(_ value: Bool) { + setValue(for: StoreKey.notificationsService.inAppVibrate, value: value) + inAppVibrate = value } - private func onStayInChanged(_ stayIn: Bool) { - if stayIn { - setBadge(number: preservedBadgeNumber, force: false) - } else { - preservedBadgeNumber = nil - setBadge(number: nil, force: true) - } + func setInAppToasts(_ value: Bool) { + setValue(for: StoreKey.notificationsService.inAppToasts, value: value) + inAppToasts = value } } // MARK: - Notifications Sound { extension AdamantNotificationsService { - func setNotificationSound(_ sound: NotificationSound) { - notificationsSound = sound - securedStore.set(sound.fileName, for: StoreKey.notificationsService.notificationsSound) - NotificationCenter.default.post(name: Notification.Name.AdamantNotificationService.notificationsSoundChanged, - object: self, - userInfo: nil) + func setNotificationSound( + _ sound: NotificationSound, + for target: NotificationTarget + ) { + switch target { + case .baseMessage: + notificationsSound = sound + case .reaction: + notificationsReactionSound = sound + } + + securedStore.set( + sound.fileName, + for: target.storeId + ) + + NotificationCenter.default.post( + name: .AdamantNotificationService.notificationsSoundChanged, + object: self, + userInfo: nil + ) } } @@ -283,6 +312,10 @@ extension AdamantNotificationsService { UNUserNotificationCenter.current().removeAllDeliveredNotifications() UIApplication.shared.applicationIconBadgeNumber = customBadgeNumber } + + func setValue(for key: String, value: Bool) { + securedStore.set(value, for: key) + } } // MARK: - Background batch notifications @@ -297,3 +330,108 @@ extension AdamantNotificationsService { backgroundNotifications = 0 } } + +private extension AdamantNotificationsService { + func onUserLoggedIn() { + UNUserNotificationCenter.current().removeAllDeliveredNotifications() + UIApplication.shared.applicationIconBadgeNumber = 0 + + if let raw: String = securedStore.get(StoreKey.notificationsService.notificationsMode), + let mode = NotificationsMode(string: raw) { + setNotificationsMode(mode, completion: nil) + } else { + setNotificationsMode(.disabled, completion: nil) + } + + NotificationTarget.allCases.forEach { target in + if let raw: String = securedStore.get(target.storeId), + let sound = NotificationSound(fileName: raw) { + setNotificationSound(sound, for: target) + } + } + + inAppSound = securedStore.get(StoreKey.notificationsService.inAppSounds) ?? defaultInAppSound + inAppVibrate = securedStore.get(StoreKey.notificationsService.inAppVibrate) ?? defaultInAppVibrate + inAppToasts = securedStore.get(StoreKey.notificationsService.inAppToasts) ?? defaultInAppToasts + + preservedBadgeNumber = nil + + Task { + await setupUnreadController() + } + } + + func onUserLoggedOut() { + setNotificationsMode(.disabled, completion: nil) + setNotificationSound(defaultNotificationsSound, for: .baseMessage) + setNotificationSound(defaultNotificationsReactionSound, for: .reaction) + securedStore.remove(StoreKey.notificationsService.notificationsMode) + securedStore.remove(StoreKey.notificationsService.notificationsSound) + securedStore.remove(StoreKey.notificationsService.notificationsReactionSound) + securedStore.remove(StoreKey.notificationsService.inAppSounds) + securedStore.remove(StoreKey.notificationsService.inAppVibrate) + securedStore.remove(StoreKey.notificationsService.inAppToasts) + preservedBadgeNumber = nil + + resetUnreadController() + } + + func onStayInChanged(_ stayIn: Bool) { + if stayIn { + setBadge(number: preservedBadgeNumber, force: false) + } else { + preservedBadgeNumber = nil + setBadge(number: nil, force: true) + } + } + + func setupUnreadController() async { + unreadController = await chatsProvider?.getUnreadMessagesController() + unreadController?.delegate = self + try? unreadController?.performFetch() + } + + func resetUnreadController() { + unreadController = nil + unreadController?.delegate = nil + } + + func playSound(by fileName: String) { + guard let url = Bundle.main.url(forResource: fileName.replacingOccurrences(of: ".mp3", with: ""), withExtension: "mp3") else { + return + } + + try? AVAudioSession.sharedInstance().setCategory(.soloAmbient) + try? AVAudioSession.sharedInstance().setActive(true) + audioPlayer = try? AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.mp3.rawValue) + audioPlayer?.volume = 1.0 + audioPlayer?.play() + } +} + +extension AdamantNotificationsService: NSFetchedResultsControllerDelegate { + func controller( + _ controller: NSFetchedResultsController, + didChange anObject: Any, + at indexPath: IndexPath?, + for type: NSFetchedResultsChangeType, + newIndexPath: IndexPath? + ) { + guard let transaction = anObject as? ChatTransaction, + type == .insert + else { return } + + if inAppVibrate { + vibroService.applyVibration(.medium) + } + + if inAppSound { + switch transaction { + case let tx as RichMessageTransaction where tx.additionalType == .reaction: + playSound(by: notificationsReactionSound.fileName) + default: + playSound(by: notificationsSound.fileName) + } + } + } +} diff --git a/Adamant/Services/AdamantPushNotificationsTokenService.swift b/Adamant/Services/AdamantPushNotificationsTokenService.swift index 00131f3aa..241da8baa 100644 --- a/Adamant/Services/AdamantPushNotificationsTokenService.swift +++ b/Adamant/Services/AdamantPushNotificationsTokenService.swift @@ -11,7 +11,7 @@ import CommonKit final class AdamantPushNotificationsTokenService: PushNotificationsTokenService { private let securedStore: SecuredStore - private let apiService: ApiService + private let apiService: AdamantApiServiceProtocol private let adamantCore: AdamantCore private let accountService: AccountService @@ -21,7 +21,7 @@ final class AdamantPushNotificationsTokenService: PushNotificationsTokenService init( securedStore: SecuredStore, - apiService: ApiService, + apiService: AdamantApiServiceProtocol, adamantCore: AdamantCore, accountService: AccountService ) { diff --git a/Adamant/Services/AdamantReachability.swift b/Adamant/Services/AdamantReachability.swift index aa85a0eef..02079b275 100644 --- a/Adamant/Services/AdamantReachability.swift +++ b/Adamant/Services/AdamantReachability.swift @@ -13,9 +13,13 @@ import CommonKit // MARK: - AdamantReachability wrapper final class AdamantReachability: ReachabilityMonitor { + @ObservableValue private(set) var connection = true + private let monitor = NWPathMonitor() - @Atomic private(set) var connection = true + var connectionPublisher: AnyObservable { + $connection.eraseToAnyPublisher() + } func start() { monitor.pathUpdateHandler = { [weak self] _ in diff --git a/Adamant/Services/AdamantVisibleWalletsService.swift b/Adamant/Services/AdamantVisibleWalletsService.swift index 26020be18..566b825c4 100644 --- a/Adamant/Services/AdamantVisibleWalletsService.swift +++ b/Adamant/Services/AdamantVisibleWalletsService.swift @@ -222,7 +222,12 @@ final class AdamantVisibleWalletsService: VisibleWalletsService { } let wallet = availableServices.remove(at: index) - availableServices.insert(wallet, at: newIndex) + + if availableServices.indices.contains(newIndex) { + availableServices.insert(wallet, at: newIndex) + } else { + availableServices.append(wallet) + } } return availableServices diff --git a/Adamant/Services/ApiService/AdamantApiService.swift b/Adamant/Services/ApiService/AdamantApiService.swift deleted file mode 100644 index 035a0e3cb..000000000 --- a/Adamant/Services/ApiService/AdamantApiService.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// AdamantApiService.swift -// Adamant -// -// Created by Anokhov Pavel on 06.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import CommonKit -import Foundation - -final class AdamantApiService { - let adamantCore: AdamantCore - let service: BlockchainHealthCheckWrapper - - init( - healthCheckWrapper: BlockchainHealthCheckWrapper, - adamantCore: AdamantCore - ) { - service = healthCheckWrapper - self.adamantCore = adamantCore - } - - func request( - _ request: @Sendable (APICoreProtocol, Node) async -> ApiServiceResult - ) async -> ApiServiceResult { - await service.request { admApiCore, node in - await request(admApiCore.apiCore, node) - } - } -} - -extension AdamantApiService: ApiService { - var preferredNodeIds: [UUID] { - service.preferredNodeIds - } - - func healthCheck() { - service.healthCheck() - } -} diff --git a/Adamant/Services/ApiServiceCompose.swift b/Adamant/Services/ApiServiceCompose.swift new file mode 100644 index 000000000..7bde7470f --- /dev/null +++ b/Adamant/Services/ApiServiceCompose.swift @@ -0,0 +1,59 @@ +// +// ApiServiceCompose.swift +// Adamant +// +// Created by Andrew G on 21.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation +import CommonKit + +struct ApiServiceCompose: ApiServiceComposeProtocol { + let btc: ApiServiceProtocol + let eth: ApiServiceProtocol + let klyNode: ApiServiceProtocol + let klyService: ApiServiceProtocol + let doge: ApiServiceProtocol + let dash: ApiServiceProtocol + let adm: ApiServiceProtocol + let ipfs: ApiServiceProtocol + let infoService: ApiServiceProtocol + + func chosenFastestNodeId(group: NodeGroup) -> UUID? { + getApiService(group: group).chosenFastestNodeId + } + + func hasActiveNode(group: NodeGroup) -> Bool { + getApiService(group: group).hasActiveNode + } + + func healthCheck(group: NodeGroup) { + getApiService(group: group).healthCheck() + } +} + +private extension ApiServiceCompose { + func getApiService(group: NodeGroup) -> ApiServiceProtocol { + switch group { + case .btc: + return btc + case .eth: + return eth + case .klyNode: + return klyNode + case .klyService: + return klyService + case .doge: + return doge + case .dash: + return dash + case .adm: + return adm + case .ipfs: + return ipfs + case .infoService: + return infoService + } + } +} diff --git a/Adamant/Services/BlockchainHealthCheckWrapper.swift b/Adamant/Services/BlockchainHealthCheckWrapper.swift deleted file mode 100644 index 105eea8d5..000000000 --- a/Adamant/Services/BlockchainHealthCheckWrapper.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// 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(let info): - if let versionNumber = Node.stringToDouble(info.version), - versionNumber < nodeGroup.minNodeVersion { - nodesStorage.updateNodeParams( - id: node.id, - connectionStatus: .notAllowed(.outdatedApiVersion) - ) - } - - 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 - var status: Node.ConnectionStatus? - let actualNodeVersion = Node.stringToDouble(node.version) - - if let actualNodeVersion = actualNodeVersion, - actualNodeVersion < nodeGroup.minNodeVersion { - status = Node.ConnectionStatus.notAllowed(.outdatedApiVersion) - } else { - status = 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, .notAllowed: - 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 04babdba3..61c5a7a31 100644 --- a/Adamant/Services/DataProviders/AdamantAccountsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantAccountsProvider.swift @@ -42,7 +42,7 @@ final class AdamantAccountsProvider: AccountsProvider { // MARK: Dependencies @MainActor private let stack: CoreDataStack - private let apiService: ApiService + private let apiService: AdamantApiServiceProtocol private let addressBookService: AddressBookService // MARK: Properties @@ -52,7 +52,7 @@ final class AdamantAccountsProvider: AccountsProvider { // MARK: Lifecycle nonisolated init( stack: CoreDataStack, - apiService: ApiService, + apiService: AdamantApiServiceProtocol, addressBookService: AddressBookService ) { self.stack = stack diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index 346873b93..fee22aaf9 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -24,7 +24,7 @@ actor AdamantChatsProvider: ChatsProvider { let accountService: AccountService let accountsProvider: AccountsProvider let securedStore: SecuredStore - let apiService: ApiService + let apiService: AdamantApiServiceProtocol let stack: CoreDataStack // MARK: Properties @@ -73,7 +73,7 @@ actor AdamantChatsProvider: ChatsProvider { // MARK: Lifecycle init( accountService: AccountService, - apiService: ApiService, + apiService: AdamantApiServiceProtocol, socketService: SocketService, stack: CoreDataStack, adamantCore: AdamantCore, @@ -359,7 +359,11 @@ extension AdamantChatsProvider { // MARK: 3. Get transactions - let chatrooms = try? await apiGetChatrooms(address: address, offset: offset) + let chatrooms = try? await apiService.getChatRooms( + address: address, + offset: offset, + waitsForConnectivity: true + ).get() guard let chatrooms = chatrooms else { if !isInitiallySynced { @@ -410,20 +414,6 @@ extension AdamantChatsProvider { } } - func apiGetChatrooms(address: String, offset: Int?) async throws -> ChatRooms? { - do { - let chatrooms = try await apiService.getChatRooms(address: address, offset: offset).get() - return chatrooms - } catch let error as ApiServiceError { - guard case .networkError = error else { - return nil - } - - await Task.sleep(interval: requestRepeatDelay) - return try await apiGetChatrooms(address: address, offset: offset) - } - } - func getChatMessages(with addressRecipient: String, offset: Int?) async { await getChatMessages(with: addressRecipient, offset: offset, loadedCount: .zero) } @@ -1327,6 +1317,10 @@ extension AdamantChatsProvider { // MARK: 3. Send do { + let locallyID = signedTransaction.generateId() ?? UUID().uuidString + transaction.transactionId = locallyID + transaction.chatMessageId = locallyID + let id = try await apiService.sendMessageTransaction(transaction: signedTransaction).get() // Update ID with recieved, add to unconfirmed transactions. @@ -1334,38 +1328,66 @@ extension AdamantChatsProvider { transaction.chatMessageId = String(id) transaction.statusEnum = .pending - if let index = unconfirmedTransactionsBySignature.firstIndex( - of: signedTransaction.signature - ) { - unconfirmedTransactionsBySignature.remove(at: index) - } - - unconfirmedTransactions[id] = transaction.objectID + removeTxFromUnconfirmed( + signature: signedTransaction.signature, + transaction: transaction + ) return transaction } catch { - transaction.statusEnum = .failed - - switch error as? ApiServiceError { - case .networkError: - throw ChatsProviderError.networkError - case .accountNotFound: - throw ChatsProviderError.accountNotFound(recipientId) - case .notLogged: - 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: - throw ChatsProviderError.requestCancelled - case .none: - throw ChatsProviderError.serverError(error) + guard case let(apiError) = (error as? ApiServiceError), + case let(.serverError(text)) = apiError, + text.contains("Transaction is already confirmed") + || text.contains("Transaction is already processed") + else { + transaction.statusEnum = .failed + throw handleTransactionError(error, recipientId: recipientId) } + + transaction.statusEnum = .pending + + removeTxFromUnconfirmed( + signature: signedTransaction.signature, + transaction: transaction + ) + + return transaction + } + } + + func removeTxFromUnconfirmed( + signature: String, + transaction: ChatTransaction + ) { + if let index = unconfirmedTransactionsBySignature.firstIndex( + of: signature + ) { + unconfirmedTransactionsBySignature.remove(at: index) + } + + unconfirmedTransactions[UInt64(transaction.transactionId) ?? .zero] = transaction.objectID + } + + func handleTransactionError(_ error: Error, recipientId: String) -> Error { + switch error as? ApiServiceError { + case .networkError: + return ChatsProviderError.networkError + case .accountNotFound: + return ChatsProviderError.accountNotFound(recipientId) + case .notLogged: + return ChatsProviderError.notLogged + case .serverError(let e), .commonError(let e): + return ChatsProviderError.serverError(AdamantError(message: e)) + case .noEndpointsAvailable: + return ChatsProviderError.serverError(AdamantError( + message: error.localizedDescription + )) + case .internalError(let message, _): + return ChatsProviderError.internalError(AdamantError(message: message)) + case .requestCancelled: + return ChatsProviderError.requestCancelled + case .none: + return ChatsProviderError.serverError(error) } } } @@ -1706,7 +1728,6 @@ extension AdamantChatsProvider { transactionInProgress.append(trs.transaction.id) if let objectId = unconfirmedTransactions[trs.transaction.id], let unconfirmed = context.object(with: objectId) as? ChatTransaction { - print("confirmTransaction tr=\(trs.transaction.id)") confirmTransaction( unconfirmed, id: trs.transaction.id, @@ -1724,7 +1745,6 @@ extension AdamantChatsProvider { // if transaction in pending status then ignore it if unconfirmedTransactionsBySignature.contains(trs.transaction.signature) { - print("ignore tr=\(trs.transaction.id)") continue } @@ -1923,17 +1943,18 @@ extension AdamantChatsProvider { transaction.blockId = blockId transaction.confirmations = confirmations - if blockId.isEmpty { - transaction.statusEnum = .delivered - } else { + if !blockId.isEmpty { self.unconfirmedTransactions.removeValue(forKey: id) } if let lastHeight = receivedLastHeight, lastHeight < height { self.receivedLastHeight = height - transaction.statusEnum = .delivered + } + + if height != .zero { transaction.isConfirmed = true } + transaction.statusEnum = .delivered } func blockChat(with address: String) { diff --git a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift index d2a923b68..7763b73a5 100644 --- a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift +++ b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift @@ -16,7 +16,7 @@ actor AdamantTransfersProvider: TransfersProvider { static let transferFee: Decimal = Decimal(sign: .plus, exponent: -1, significand: 5) // MARK: Dependencies - let apiService: ApiService + let apiService: AdamantApiServiceProtocol private let stack: CoreDataStack private let adamantCore: AdamantCore private let accountService: AccountService @@ -61,7 +61,7 @@ actor AdamantTransfersProvider: TransfersProvider { // MARK: Lifecycle init( - apiService: ApiService, + apiService: AdamantApiServiceProtocol, stack: CoreDataStack, adamantCore: AdamantCore, accountService: AccountService, diff --git a/Adamant/Services/DataProviders/DefaultNodesProvider.swift b/Adamant/Services/DataProviders/DefaultNodesProvider.swift new file mode 100644 index 000000000..a202d2de3 --- /dev/null +++ b/Adamant/Services/DataProviders/DefaultNodesProvider.swift @@ -0,0 +1,42 @@ +// +// DefaultNodesProvider.swift +// Adamant +// +// Created by Andrew G on 08.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import CommonKit + +struct DefaultNodesProvider: Sendable { + func get(_ groups: Set) -> [NodeGroup: [Node]] { + .init(uniqueKeysWithValues: groups.map { + ($0, defaultItems(group: $0)) + }) + } +} + +private extension DefaultNodesProvider { + func defaultItems(group: NodeGroup) -> [Node] { + switch group { + case .btc: + return BtcWalletService.nodes + case .eth: + return EthWalletService.nodes + case .klyNode: + return KlyWalletService.nodes + case .klyService: + return KlyWalletService.serviceNodes + case .doge: + return DogeWalletService.nodes + case .dash: + return DashWalletService.nodes + case .adm: + return AdmWalletService.nodes + case .ipfs: + return IPFSApiService.nodes + case .infoService: + return AdmWalletService.serviceNodes + } + } +} diff --git a/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift b/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift index 44ef0bb4c..2b199ec11 100644 --- a/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift +++ b/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift @@ -8,6 +8,7 @@ import Foundation import CoreData +import CommonKit final class InMemoryCoreDataStack: CoreDataStack { let container: NSPersistentContainer diff --git a/Adamant/Services/FilesNetworkManager/IPFS+Constants.swift b/Adamant/Services/FilesNetworkManager/IPFS+Constants.swift index bda6da969..99552bb27 100644 --- a/Adamant/Services/FilesNetworkManager/IPFS+Constants.swift +++ b/Adamant/Services/FilesNetworkManager/IPFS+Constants.swift @@ -10,8 +10,12 @@ import Foundation import CommonKit extension IPFSApiService { - var preferredNodeIds: [UUID] { - service.preferredNodeIds + var chosenFastestNodeId: UUID? { + service.chosenFastestNodeId + } + + var hasActiveNode: Bool { + !service.sortedAllowedNodes.isEmpty } func healthCheck() { @@ -24,28 +28,27 @@ extension IPFSApiService { static var nodes: [Node] { [ - Node( + Node.makeDefaultNode( url: URL(string: "https://ipfs4.adm.im")!, altUrl: URL(string: "http://95.216.45.88:44099")! ), - Node( + Node.makeDefaultNode( url: URL(string: "https://ipfs5.adamant.im")!, altUrl: URL(string: "http://62.72.43.99:44099")! ), - Node( + Node.makeDefaultNode( url: URL(string: "https://ipfs6.adamant.business")!, altUrl: URL(string: "http://75.119.138.235:44099")! ) ] } - static let healthCheckParameters = CoinHealthCheckParameters( - normalUpdateInterval: 210, + static let healthCheckParameters = BlockchainHealthCheckParams( + group: .ipfs, + name: symbol, + normalUpdateInterval: 300, crucialUpdateInterval: 30, - onScreenUpdateInterval: 10, - threshold: 3, - normalServiceUpdateInterval: 210, - crucialServiceUpdateInterval: 30, - onScreenServiceUpdateInterval: 10 + minNodeVersion: nil, + nodeHeightEpsilon: 1 ) } diff --git a/Adamant/Services/FilesNetworkManager/IPFSApiCore.swift b/Adamant/Services/FilesNetworkManager/IPFSApiCore.swift index f75a75918..0d681764a 100644 --- a/Adamant/Services/FilesNetworkManager/IPFSApiCore.swift +++ b/Adamant/Services/FilesNetworkManager/IPFSApiCore.swift @@ -20,18 +20,18 @@ final class IPFSApiCore { self.apiCore = apiCore } - func getNodeStatus(node: Node) async -> ApiServiceResult { + func getNodeStatus(origin: NodeOrigin) async -> ApiServiceResult { await apiCore.sendRequestJsonResponse( - node: node, + origin: origin, path: IPFSApiCommands.status ) } } extension IPFSApiCore: BlockchainHealthCheckableService { - func getStatusInfo(node: Node) async -> ApiServiceResult { + func getStatusInfo(origin: NodeOrigin) async -> ApiServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 - let statusResponse = await getNodeStatus(node: node) + let statusResponse = await getNodeStatus(origin: origin) let ping = Date.now.timeIntervalSince1970 - startTimestamp return statusResponse.map { _ in diff --git a/Adamant/Services/FilesNetworkManager/IPFSApiService.swift b/Adamant/Services/FilesNetworkManager/IPFSApiService.swift index ece6b084b..75f1df1ef 100644 --- a/Adamant/Services/FilesNetworkManager/IPFSApiService.swift +++ b/Adamant/Services/FilesNetworkManager/IPFSApiService.swift @@ -27,7 +27,7 @@ final class IPFSApiService: FileApiServiceProtocol { } func request( - _ request: @Sendable (APICoreProtocol, Node) async -> ApiServiceResult + _ request: @Sendable (APICoreProtocol, NodeOrigin) async -> ApiServiceResult ) async -> ApiServiceResult { await service.request { admApiCore, node in await request(admApiCore.apiCore, node) @@ -44,11 +44,12 @@ final class IPFSApiService: FileApiServiceProtocol { data: data ) - let result: Result = await request { core, node in + let result: Result = await request { core, origin in await core.sendRequestMultipartFormDataJsonResponse( - node: node, + origin: origin, path: IPFSApiCommands.file.upload, models: [model], + timeout: .extended, uploadProgress: uploadProgress ) } @@ -68,10 +69,11 @@ final class IPFSApiService: FileApiServiceProtocol { id: String, downloadProgress: @escaping ((Progress) -> Void) ) async -> FileApiServiceResult { - let result: Result = await request { core, node in + let result: Result = await request { core, origin in let result: APIResponseModel = await core.sendRequest( - node: node, + origin: origin, path: "\(IPFSApiCommands.file.download)\(id)", + timeout: .extended, downloadProgress: downloadProgress ) diff --git a/Adamant/Services/FilesNetworkManager/Models/FileApiServiceResult.swift b/Adamant/Services/FilesNetworkManager/Models/FileApiServiceResult.swift new file mode 100644 index 000000000..98393ab84 --- /dev/null +++ b/Adamant/Services/FilesNetworkManager/Models/FileApiServiceResult.swift @@ -0,0 +1,9 @@ +// +// FileApiServiceResult.swift +// Adamant +// +// Created by Andrew G on 21.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +typealias FileApiServiceResult = Result diff --git a/Adamant/Services/FilesNetworkManager/Models/FileManagerError.swift b/Adamant/Services/FilesNetworkManager/Models/FileManagerError.swift index e36259dc5..d34d39af1 100644 --- a/Adamant/Services/FilesNetworkManager/Models/FileManagerError.swift +++ b/Adamant/Services/FilesNetworkManager/Models/FileManagerError.swift @@ -7,6 +7,7 @@ // import Foundation +import CommonKit enum NetworkFileProtocolType: String { case ipfs diff --git a/Adamant/Services/HealthCheckWrapper.swift b/Adamant/Services/HealthCheckWrapper.swift deleted file mode 100644 index e1afb94c5..000000000 --- a/Adamant/Services/HealthCheckWrapper.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// HealthCheckWrapper.swift -// Adamant -// -// Created by Andrew G on 22.10.2023. -// Copyright © 2023 Adamant. All rights reserved. -// - -import CommonKit -import Foundation -import Combine -import UIKit - -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() - - @Atomic private var previousAppState: UIApplication.State? - @Atomic private var lastUpdateTime = Date() - - @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) - - NotificationCenter.default - .publisher(for: UIApplication.didBecomeActiveNotification, object: nil) - .sink { [weak self] _ in self?.didBecomeActiveAction() } - .store(in: &subscriptions) - - NotificationCenter.default - .publisher(for: UIApplication.willResignActiveNotification, object: nil) - .sink { [weak self] _ in self?.previousAppState = .background } - .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 = 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() - self?.lastUpdateTime = Date() - } - } - - func didBecomeActiveAction() { - defer { previousAppState = .active } - - guard previousAppState == .background, - Date() > lastUpdateTime.addingTimeInterval(normalUpdateInterval / 3) - else { return } - - healthCheck() - lastUpdateTime = Date() - } -} - -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/NativeCore+AdamantCore.swift b/Adamant/Services/NativeCore+AdamantCore.swift deleted file mode 100644 index 1d34430ca..000000000 --- a/Adamant/Services/NativeCore+AdamantCore.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// NativeCore+AdamantCore.swift -// Adamant -// -// Created by Anokhov Pavel on 25/05/2019. -// Copyright © 2019 Adamant. All rights reserved. -// - -import Foundation -import CryptoSwift -import CommonKit - -extension NativeAdamantCore: AdamantCore { - // MARK: - Passphrases - - func generateNewPassphrase() -> String { - if let passphrase = try? Mnemonic.generate().joined(separator: " ") { - return passphrase - } - return "" - } - - // MARK: - Signing transactions - - func sign(transaction: SignableTransaction, senderId: String, keypair: Keypair) -> String? { - let privateKey = keypair.privateKey.hexBytes() - let hash = transaction.bytes.sha256() - - guard let signature = Crypto.sign.signature(message: hash, secretKey: privateKey) else { - print("FAIL to sign of transaction") - return nil - } - - return signature.hexString() - } -} - -// MARK: - Bytes -fileprivate extension SignableTransaction { - - var bytes: [UInt8] { - return - typeBytes + - timestampBytes + - senderPublicKeyBytes + - requesterPublicKeyBytes + - recipientIdBytes + - amountBytes + - assetBytes + - signatureBytes + - signSignatureBytes - } - - var typeBytes: [UInt8] { - return [UInt8(type.rawValue)] - } - - var timestampBytes: [UInt8] { - return ByteBackpacker.pack(UInt32(timestamp), byteOrder: .littleEndian) - } - - var senderPublicKeyBytes: [UInt8] { - return senderPublicKey.hexBytes() - } - - var requesterPublicKeyBytes: [UInt8] { - return 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] { - return [] - } - - var signSignatureBytes: [UInt8] { - return [] - } - - 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/Services/NodesStorage.swift b/Adamant/Services/NodesStorage.swift deleted file mode 100644 index dae6bda6b..000000000 --- a/Adamant/Services/NodesStorage.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// 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) - } - } - - func haveActiveNode(in group: CommonKit.NodeGroup) -> Bool { - let nodes = items.wrappedValue.filter { $0.group == group }.map { $0.node } - let node = nodes.first(where: { $0.connectionStatus == .allowed && $0.isEnabled }) - return node != nil - } - - init(securedStore: SecuredStore) { - self.securedStore = securedStore - - var nodes = securedStore.get(StoreKey.NodesStorage.nodes) ?? Self.defaultItems - let nodesToAdd = Self.defaultItems.filter { defaultNode in - !nodes.contains { $0.node.host == defaultNode.node.host } - } - nodes.append(contentsOf: nodesToAdd) - - _items = .init(wrappedValue: .init( - wrappedValue: nodes - )) - - 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 .klyNode: - return KlyWalletService.nodes.map { .init(group: .klyNode, node: $0) } - case .klyService: - return KlyWalletService.serviceNodes.map { .init(group: .klyService, 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) } - case .ipfs: - return IPFSApiService.nodes.map { .init(group: .ipfs, 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/RichTransactionReactService/AdamantRichTransactionReactService.swift b/Adamant/Services/RichTransactionReactService/AdamantRichTransactionReactService.swift index a26d0ee53..fcbaf64e8 100644 --- a/Adamant/Services/RichTransactionReactService/AdamantRichTransactionReactService.swift +++ b/Adamant/Services/RichTransactionReactService/AdamantRichTransactionReactService.swift @@ -12,7 +12,7 @@ import CommonKit actor AdamantRichTransactionReactService: NSObject, RichTransactionReactService { private let coreDataStack: CoreDataStack - private let apiService: ApiService + private let apiService: AdamantApiServiceProtocol private let adamantCore: AdamantCore private let accountService: AccountService @@ -25,7 +25,7 @@ actor AdamantRichTransactionReactService: NSObject, RichTransactionReactService init( coreDataStack: CoreDataStack, - apiService: ApiService, + apiService: AdamantApiServiceProtocol, adamantCore: AdamantCore, accountService: AccountService ) { diff --git a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift index 6e8ce8798..0769b02be 100644 --- a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift +++ b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift @@ -12,7 +12,7 @@ import CommonKit actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService { private let coreDataStack: CoreDataStack - private let apiService: ApiService + private let apiService: AdamantApiServiceProtocol private let adamantCore: AdamantCore private let accountService: AccountService private let walletServiceCompose: WalletServiceCompose @@ -23,7 +23,7 @@ actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService init( coreDataStack: CoreDataStack, - apiService: ApiService, + apiService: AdamantApiServiceProtocol, adamantCore: AdamantCore, accountService: AccountService, walletServiceCompose: WalletServiceCompose diff --git a/Adamant/Services/RichTransactionStatusService/AdamantTransactionStatusService.swift b/Adamant/Services/RichTransactionStatusService/AdamantTransactionStatusService.swift index 58e8aba73..0e3a04af9 100644 --- a/Adamant/Services/RichTransactionStatusService/AdamantTransactionStatusService.swift +++ b/Adamant/Services/RichTransactionStatusService/AdamantTransactionStatusService.swift @@ -86,8 +86,8 @@ private extension AdamantTransactionStatusService { func makeNodesAvailabilitySubscription() -> some Observable<[UUID]> { nodesStorage - .nodesWithGroupsPublisher - .map { $0.compactMap { $0.node.isEnabled ? $0.node.id : nil } } + .nodesPublisher + .map { $0.values.flatMap { $0.compactMap { $0.isEnabled ? $0.id : nil } } } .removeDuplicates() } diff --git a/Adamant/SharedViews/ReplyView.swift b/Adamant/SharedViews/ReplyView.swift index 57b6b1bc7..89d0adf74 100644 --- a/Adamant/SharedViews/ReplyView.swift +++ b/Adamant/SharedViews/ReplyView.swift @@ -52,7 +52,7 @@ final class ReplyView: UIView { private lazy var closeBtn: UIButton = { let btn = UIButton() btn.setImage( - UIImage(systemName: "xmark")?.withTintColor(.adamant.alert), + UIImage(systemName: "xmark")?.withTintColor(.adamant.attention), for: .normal ) btn.addTarget(self, action: #selector(didTapCloseBtn), for: .touchUpInside) diff --git a/Adamant/SharedViews/VersionFooter.xib b/Adamant/SharedViews/VersionFooter.xib deleted file mode 100644 index a01667958..000000000 --- a/Adamant/SharedViews/VersionFooter.xib +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - Exo2-Regular - - - - - - - - - - - - - - - - - - - - - - diff --git a/Adamant/SharedViews/VersionFooterView.swift b/Adamant/SharedViews/VersionFooterView.swift new file mode 100644 index 000000000..1cfbfeac7 --- /dev/null +++ b/Adamant/SharedViews/VersionFooterView.swift @@ -0,0 +1,96 @@ +// +// VersionFooterView.swift +// Adamant +// +// Created by Andrew G on 19.09.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import UIKit +import SnapKit + +final class VersionFooterView: UIView { + struct Model { + let version: String + let commit: String? + + static let `default` = Self(version: .empty, commit: nil) + } + + var model: Model = .default { + didSet { update() } + } + + private let versionLabel = UILabel( + font: .adamantPrimary(ofSize: fontSize), + textColor: .adamant.primary + ) + + private let commitLabel = UILabel( + font: .adamantPrimary(ofSize: fontSize), + textColor: .adamant.primary + ) + + private lazy var labelsStack: UIStackView = { + let stack = UIStackView(arrangedSubviews: [versionLabel]) + stack.alignment = .center + stack.axis = .vertical + stack.spacing = labelsGap + return stack + }() + + private lazy var containerView: UIView = { + let view = UIView() + view.addSubview(labelsStack) + labelsStack.snp.makeConstraints { + $0.directionalHorizontalEdges.top.equalToSuperview() + $0.bottom.equalToSuperview().inset(bottomInset) + } + + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + .init( + width: size.width, + height: containerView.systemLayoutSizeFitting(size).height + ) + } +} + +private extension VersionFooterView { + func configure() { + addSubview(containerView) + containerView.snp.makeConstraints { + $0.directionalEdges.equalToSuperview() + } + } + + func update() { + versionLabel.text = model.version + commitLabel.text = model.commit + + switch (model.commit, commitLabel.superview) { + case (.some, .none): + labelsStack.addArrangedSubview(commitLabel) + case (.none, .some): + labelsStack.removeArrangedSubview(commitLabel) + case (.none, .none), (.some, .some): + break + } + } +} + +private let fontSize: CGFloat = 17 +private let labelsGap: CGFloat = 6 +private let bottomInset: CGFloat = 15 diff --git a/Adamant/Utilities/AdamantCoinTools.swift b/Adamant/Utilities/AdamantCoinTools.swift index 62b5c43da..3084f1d97 100644 --- a/Adamant/Utilities/AdamantCoinTools.swift +++ b/Adamant/Utilities/AdamantCoinTools.swift @@ -38,18 +38,27 @@ final class AdamantCoinTools { let url = URLComponents(string: uri) guard !uri.isEmpty, - let url = url, - let raw = url.string + let url = url else { return nil } - guard let prefix = uri.split(separator: ":").first, - prefix.caseInsensitiveCompare(qqPrefix) == .orderedSame + let array = uri.split(separator: ":") + + guard array.count > 1, + let prefix = array.first else { - return QQAddressInformation(address: raw, params: nil) + return parseAdress(url: url) + } + + guard prefix.caseInsensitiveCompare(qqPrefix) == .orderedSame else { + return nil } + return parseAdress(url: url) + } + + private class func parseAdress(url: URLComponents) -> QQAddressInformation { let addressRaw = url.path let params = url.queryItems?.compactMap { diff --git a/Adamant/Utilities/AdamantUriTools.swift b/Adamant/Utilities/AdamantUriTools.swift index 0d3e995da..3208d785a 100644 --- a/Adamant/Utilities/AdamantUriTools.swift +++ b/Adamant/Utilities/AdamantUriTools.swift @@ -19,6 +19,7 @@ enum AdamantAddressParam { case address(String) case label(String) case message(String) + case amount(Double) init?(raw: String) { let keyValue = raw.split(separator: "=") @@ -33,7 +34,9 @@ enum AdamantAddressParam { self = AdamantAddressParam.label(keyValue[1].replacingOccurrences(of: "+", with: " ").replacingOccurrences(of: "%20", with: " ")) case "message": self = AdamantAddressParam.message(keyValue[1].replacingOccurrences(of: "+", with: " ").replacingOccurrences(of: "%20", with: " ")) - + case "amount": + guard let amount = Double(keyValue[1]) else { return nil } + self = AdamantAddressParam.amount(amount) default: return nil } @@ -47,6 +50,8 @@ enum AdamantAddressParam { return "label=\(value.replacingOccurrences(of: " ", with: "+"))" case .message(let value): return "message=\(value.replacingOccurrences(of: " ", with: "+"))" + case .amount(let value): + return "amount=\(String(value).replacingOccurrences(of: " ", with: "+"))" } } } @@ -73,6 +78,8 @@ final class AdamantUriTools { components.queryItems?.append(.init(name: "label", value: value)) case .message(let value): components.queryItems?.append(.init(name: "message", value: value)) + case .amount(let value): + components.queryItems?.append(.init(name: "amount", value: String(value))) } } @@ -93,6 +100,8 @@ final class AdamantUriTools { components.queryItems?.append(.init(name: "label", value: value)) case .message(let value): components.queryItems?.append(.init(name: "message", value: value)) + case .amount(let value): + components.queryItems?.append(.init(name: "amount", value: String(value))) } } diff --git a/AdamantTests/AdamantHealthCheckServiceTests.swift b/AdamantTests/AdamantHealthCheckServiceTests.swift index 6549ca478..cd21bcd65 100644 --- a/AdamantTests/AdamantHealthCheckServiceTests.swift +++ b/AdamantTests/AdamantHealthCheckServiceTests.swift @@ -113,7 +113,7 @@ class AdamantHealthCheckServiceTests: XCTestCase { // MARK: - Helpers - private func makeTestNode(connectionStatus: Node.ConnectionStatus = .synchronizing) -> Node { + private func makeTestNode(connectionStatus: NodeConnectionStatus = .synchronizing) -> Node { let node = Node(scheme: .default, host: "", port: nil) node.connectionStatus = connectionStatus return node diff --git a/AdamantTests/NodesAllowanceTests.swift b/AdamantTests/NodesAllowanceTests.swift index 39d983c73..86c83d21a 100644 --- a/AdamantTests/NodesAllowanceTests.swift +++ b/AdamantTests/NodesAllowanceTests.swift @@ -104,7 +104,7 @@ class NodesAllowanceTests: XCTest { nodes.getAllowedNodes(sortedBySpeedDescending: true, needWS: ws) } - private func makeTestNode(connectionStatus: Node.ConnectionStatus = .synchronizing) -> Node { + private func makeTestNode(connectionStatus: NodeConnectionStatus = .synchronizing) -> Node { let node = Node(scheme: .default, host: "", port: nil) node.connectionStatus = connectionStatus return node diff --git a/CommonKit/Package.swift b/CommonKit/Package.swift index 54fb7ce47..a74078126 100644 --- a/CommonKit/Package.swift +++ b/CommonKit/Package.swift @@ -15,7 +15,7 @@ let package = Package( .library( name: "CommonKit", targets: ["CommonKit"] - ), + ) ], dependencies: [ .package( @@ -45,7 +45,12 @@ let package = Package( .package( url: "https://github.com/RNCryptor/RNCryptor.git", .upToNextMinor(from: "5.1.0") - ) + ), + .package( + url: "https://github.com/Alamofire/Alamofire.git", + .upToNextMinor(from: "5.7.1") + ), + .package(path: "../BitcoinKit") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -59,12 +64,17 @@ let package = Package( "SnapKit", "MarkdownKit", "KeychainAccess", - "RNCryptor" + "RNCryptor", + "Alamofire", + "BitcoinKit" + ], + resources: [ + .process("./Assets/GitData.plist") ] ), .testTarget( name: "CommonKitTests", dependencies: ["CommonKit"] - ), + ) ] ) diff --git a/CommonKit/Scripts/CoinsScript.rb b/CommonKit/Scripts/CoinsScript.rb index d8c4b4539..27f2caec8 100755 --- a/CommonKit/Scripts/CoinsScript.rb +++ b/CommonKit/Scripts/CoinsScript.rb @@ -90,9 +90,9 @@ def writeToSwiftFile(name, json) url = node["url"] altUrl = node["alt_ip"] if altUrl == nil - nodes += "Node(url: URL(string: \"#{url}\")!),\n" + nodes += "Node.makeDefaultNode(url: URL(string: \"#{url}\")!),\n" else - nodes += "Node(url: URL(string: \"#{url}\")!, altUrl: URL(string: \"#{altUrl}\")),\n" + nodes += "Node.makeDefaultNode(url: URL(string: \"#{url}\")!, altUrl: URL(string: \"#{altUrl}\")),\n" end end end @@ -106,7 +106,7 @@ def writeToSwiftFile(name, json) if serviceNodesArray != nil serviceNodesArray.each do |node| url = node["url"] - serviceNodes += "Node(url: URL(string: \"#{url}\")!),\n" + serviceNodes += "Node.makeDefaultNode(url: URL(string: \"#{url}\")!),\n" end end end diff --git a/CommonKit/Scripts/GitDataScript.sh b/CommonKit/Scripts/GitDataScript.sh new file mode 100755 index 000000000..e2ce85461 --- /dev/null +++ b/CommonKit/Scripts/GitDataScript.sh @@ -0,0 +1,10 @@ +ROOT="$PWD" + +echo """ + + + + CommitHash + $(git rev-parse HEAD) + +""" > $ROOT/CommonKit/Sources/CommonKit/Assets/GitData.plist diff --git a/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift b/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift index 5b46f2aa1..6bcdac826 100644 --- a/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift +++ b/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift @@ -4,21 +4,21 @@ public extension AdamantResources { // MARK: Nodes 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")!, 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")!, 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")!), -Node(url: URL(string: "https://node1.adamant.business")!, altUrl: URL(string: "http://194.233.75.29:45555")), -Node(url: URL(string: "https://node2.blockchain2fa.io")!), -Node(url: URL(string: "https://phecda.adm.im")!, altUrl: URL(string: "http://46.250.234.248:36666")), -Node(url: URL(string: "https://tegmine.adm.im")!), -Node(url: URL(string: "https://tauri.adm.im")!, altUrl: URL(string: "http://154.26.159.245:36666")), -Node(url: URL(string: "https://dschubba.adm.im")!), + Node.makeDefaultNode(url: URL(string: "https://clown.adamant.im")!), +Node.makeDefaultNode(url: URL(string: "https://lake.adamant.im")!), +Node.makeDefaultNode(url: URL(string: "https://endless.adamant.im")!, altUrl: URL(string: "http://149.102.157.15:36666")), +Node.makeDefaultNode(url: URL(string: "https://bid.adamant.im")!), +Node.makeDefaultNode(url: URL(string: "https://unusual.adamant.im")!), +Node.makeDefaultNode(url: URL(string: "https://debate.adamant.im")!, altUrl: URL(string: "http://95.216.161.113:36666")), +Node.makeDefaultNode(url: URL(string: "http://78.47.205.206:36666")!), +Node.makeDefaultNode(url: URL(string: "http://5.161.53.74:36666")!), +Node.makeDefaultNode(url: URL(string: "http://184.94.215.92:45555")!), +Node.makeDefaultNode(url: URL(string: "https://node1.adamant.business")!, altUrl: URL(string: "http://194.233.75.29:45555")), +Node.makeDefaultNode(url: URL(string: "https://node2.blockchain2fa.io")!), +Node.makeDefaultNode(url: URL(string: "https://phecda.adm.im")!, altUrl: URL(string: "http://46.250.234.248:36666")), +Node.makeDefaultNode(url: URL(string: "https://tegmine.adm.im")!), +Node.makeDefaultNode(url: URL(string: "https://tauri.adm.im")!, altUrl: URL(string: "http://154.26.159.245:36666")), +Node.makeDefaultNode(url: URL(string: "https://dschubba.adm.im")!), ] } diff --git a/CommonKit/Sources/CommonKit/AdamantResources.swift b/CommonKit/Sources/CommonKit/AdamantResources.swift index 45b87f7d1..749572179 100644 --- a/CommonKit/Sources/CommonKit/AdamantResources.swift +++ b/CommonKit/Sources/CommonKit/AdamantResources.swift @@ -8,9 +8,7 @@ import Foundation -public enum AdamantResources { - public static let coinsInfoSrvice = "https://info.adamant.im" - +public enum AdamantResources { // MARK: ADAMANT Addresses public static let supportEmail = "business@adamant.im" public static let ansReadmeUrl = "https://github.com/Adamant-im/adamant-notificationService" diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings index 034264e2f..55b8c1786 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings @@ -22,6 +22,9 @@ /* About scene: 'Contact us' section title. */ "About.Section.ContactUs" = "Kontaktieren Sie uns"; +/* Commit Hash */ +"About.Version.Commit" = "Commit: %@"; + /* About scene: Website row */ "About.Row.Website" = "Webseite"; @@ -167,7 +170,10 @@ "AccountTab.Row.Balance" = "Kontostand"; /* Account tab: 'Buy tokens' button */ -"AccountTab.Row.BuyTokens" = "Token tauschen"; +"AccountTab.Row.BuyTokens" = "Coin tauschen"; + +/* Account tab: 'Stake ADM' button */ +"AccountTab.Row.StakeAdm" = "Setzen und verdienen"; /* Account tab: 'Logout' button */ "AccountTab.Row.Logout" = "Ausloggen"; @@ -205,6 +211,12 @@ /* Account tab: Exchange ADM tokens in chat */ "AccountTab.Row.ExchangeADMInChat" = "Austauscher im Chat"; +/* Account tab: Exchanges on CMC */ +"AccountTab.Row.ExchangesOnCoinMarketCap" = "Börsen auf CoinMarketCap"; + +/* Account tab: Exchanges on CoinGecko */ +"AccountTab.Row.ExchangesOnCoinGecko" = "Börsen auf CoinGecko"; + /* Account tab: 'Address' row */ "AccountTab.Row.Address" = "Addresse"; @@ -463,6 +475,9 @@ /* Delegates page: scene title */ "Delegates.Title" = "Delegierte"; +/* Delegates page: header text */ +"Delegates.HeaderText" = "Wenn Sie ADM einsetzen und Belohnungen erhalten möchten, stimmen Sie für einen aktiven Schmiedepool Ihres Vertrauens. Lesen Sie mehr im [ADAMANT Blog](https://news.adamant.im/hodl-list-of-adamant-pools-join-in-and-get-rewards-491a98610f4b)."; + /* Visible Wallets page: scene title */ "VisibleWallets.Title" = "Wallet-Liste"; @@ -649,6 +664,12 @@ /* NodesList: Button label */ "NodesList.NodesList" = "ADM Node-Liste"; +/* Info Service: Info Service */ +"InfoService.InfoService" = "Info Service"; + +/* Info Service: inconsistent data */ +"InfoService.InconsistentData" = "Inconsistent data"; + /* NodesList: scene title */ "NodesList.Title" = "Liste der benutzten ADM Nodes"; @@ -803,7 +824,7 @@ "Notifications.Mode.NotificationsDisabled" = "Deaktiviert"; /* Notifications: Use Background fetch notifications */ -"Notifications.Mode.BackgroundFetch" = "Hintergrundaktualisierung"; +"Notifications.Mode.BackgroundFetch" = "Hintergrund Fetch"; /* Notifications: Use Apple Push notifications */ "Notifications.Mode.ApplePush" = "Push"; @@ -820,6 +841,22 @@ /* Notifications: Select Sounds */ "Notifications.Sounds.Name" = "Geräusche"; +/* Notifications: Vibrate title */ +"Notifications.Vibrate.Title" = "Vibrieren Sie"; + +/* Notifications: Toasts title */ +"Notifications.Toasts.Title" = "Toasts"; + +/* Notifications: Reactions header */ +"Notifications.Reactions.Header" = "Reaktionen"; + +/* Notifications: In-App notifications header */ +"Notifications.InAppNotifications.Header" = "In-App-Benachrichtigungen"; + + +/* Notifications: Select Alert Tones */ +"Notifications.Alert.Tones" = "ALERT-TÖNE"; + /* Notifications: Select Alert Save */ "Notifications.Alert.Save" = "Speichern"; @@ -1036,6 +1073,9 @@ /* Transaction status: inconsistent reason title */ "TransactionStatus.Inconsistent.Reason.Title" = "Inkonsistenter Grund"; +/* Transaction status: inconsistent data title */ +"TransactionStatus.Inconsistent.RecordData.Title" = "Tx-Datensatz"; + /* Transaction details: scene title */ "TransactionDetailsScene.Title" = "Details"; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings index 24d1f315d..89e3429c4 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings @@ -22,6 +22,9 @@ /* About scene: 'Contact us' section title. */ "About.Section.ContactUs" = "Contact us"; +/* Commit Hash */ +"About.Version.Commit" = "Commit: %@"; + /* Contribute scene: 'Crashlytics' section title. */ "Contribute.Section.Crashlytics" = "Crashlytics"; @@ -164,7 +167,10 @@ "AccountTab.Row.Balance" = "Balance"; /* Account tab: 'Buy tokens' button */ -"AccountTab.Row.BuyTokens" = "Exchange tokens"; +"AccountTab.Row.BuyTokens" = "Exchange coins"; + +/* Account tab: 'Stake ADM' button */ +"AccountTab.Row.StakeAdm" = "Stake and earn"; /* Account tab: 'Logout' button */ "AccountTab.Row.Logout" = "Logout"; @@ -202,6 +208,12 @@ /* Account tab: Exchange ADM tokens in chat */ "AccountTab.Row.ExchangeADMInChat" = "In-chat Exchanger"; +/* Account tab: Exchanges on CMC */ +"AccountTab.Row.ExchangesOnCoinMarketCap" = "Exchanges on CoinMarketCap"; + +/* Account tab: Exchanges on CoinGecko */ +"AccountTab.Row.ExchangesOnCoinGecko" = "Exchanges on CoinGecko"; + /* Account tab: 'Address' row */ "AccountTab.Row.Address" = "Address"; @@ -454,6 +466,9 @@ /* Delegates page: scene title */ "Delegates.Title" = "Delegates"; +/* Delegates page: header text */ +"Delegates.HeaderText" = "To stake ADM and get rewards, vote for an active forging pool you trust. Read more in the [ADAMANT blog](https://news.adamant.im/hodl-list-of-adamant-pools-join-in-and-get-rewards-491a98610f4b)."; + /* Visible Wallets page: scene title */ "VisibleWallets.Title" = "Wallet list"; @@ -637,6 +652,12 @@ /* New Chat: 'What does it mean?', a help button for info about uninitialized accounts. */ "NewChatScene.NotInitialized.HelpButton" = "What does it mean?"; +/* Info Service: Info Service */ +"InfoService.InfoService" = "Info Service"; + +/* Info Service: inconsistent data */ +"InfoService.InconsistentData" = "Inconsistent data"; + /* NodesList: Button label */ "NodesList.NodesList" = "ADM node list"; @@ -805,6 +826,18 @@ /* Notifications: Select Sounds */ "Notifications.Sounds.Name" = "Sounds"; +/* Notifications: Vibrate title */ +"Notifications.Vibrate.Title" = "Vibrate"; + +/* Notifications: Toasts title */ +"Notifications.Toasts.Title" = "Toasts"; + +/* Notifications: Reactions header */ +"Notifications.Reactions.Header" = "Reactions"; + +/* Notifications: In-App notifications header */ +"Notifications.InAppNotifications.Header" = "In-App Notifications"; + /* Notifications: Select Alert Tones */ "Notifications.Alert.Tones" = "ALERT TONES"; @@ -1021,6 +1054,9 @@ /* Transaction status: inconsistent reason title */ "TransactionStatus.Inconsistent.Reason.Title" = "Inconsistent reason"; +/* Transaction status: inconsistent data title */ +"TransactionStatus.Inconsistent.RecordData.Title" = "Tx data record"; + /* Transaction details: scene title */ "TransactionDetailsScene.Title" = "Details"; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings index 154ed882b..b03173776 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings @@ -22,6 +22,9 @@ /* About scene: 'Contact us' section title. */ "About.Section.ContactUs" = "Пишите нам"; +/* Commit Hash */ +"About.Version.Commit" = "Коммит: %@"; + /* About scene: Website row */ "About.Row.Website" = "Вебсайт"; @@ -155,7 +158,10 @@ "AccountTab.Row.Balance" = "Баланс"; /* Account tab: 'Buy tokens' button */ -"AccountTab.Row.BuyTokens" = "Обменять токены"; +"AccountTab.Row.BuyTokens" = "Купить или продать монеты"; + +/* Account tab: 'Stake ADM' button */ +"AccountTab.Row.StakeAdm" = "Застейкать и заработать"; /* Account tab: 'Logout' button */ "AccountTab.Row.Logout" = "Выход"; @@ -193,6 +199,12 @@ /* Account tab: Exchange ADM tokens in chat */ "AccountTab.Row.ExchangeADMInChat" = "Обменник в чате"; +/* Account tab: Exchanges on CMC */ +"AccountTab.Row.ExchangesOnCoinMarketCap" = "Биржи на CoinMarketCap"; + +/* Account tab: Exchanges on CoinGecko */ +"AccountTab.Row.ExchangesOnCoinGecko" = "Биржи на CoinGecko"; + /* Account tab: 'Address' row */ "AccountTab.Row.Address" = "Адрес"; @@ -454,6 +466,9 @@ /* Delegates page: scene title */ "Delegates.Title" = "Делегаты"; +/* Delegates page: header text */ +"Delegates.HeaderText" = "Чтобы застейкать ADM и получать вознаграждения, проголосуйте за активный форжинг-пул, которому вы доверяете. Подробнее читайте в [блоге АДАМАНТа](https://news.adamant.im/hodl-list-of-adamant-pools-join-in-and-get-rewards-491a98610f4b)."; + /* Visible Wallets page: scene title */ "VisibleWallets.Title" = "Кошельки"; @@ -640,6 +655,12 @@ /* NodesList: Button label */ "NodesList.NodesList" = "Список нод ADM"; +/* Info Service: Info Service */ +"InfoService.InfoService" = "Инфо Сервис"; + +/* Info Service: inconsistent data */ +"InfoService.InconsistentData" = "Несоответствие в данных"; + /* NodesList: scene title */ "NodesList.Title" = "Список нод ADM"; @@ -805,6 +826,21 @@ /* Notifications: Select Sounds */ "Notifications.Sounds.Name" = "Звуки"; +/* Notifications: Vibrate title */ +"Notifications.Vibrate.Title" = "Вибрация"; + +/* Notifications: Toasts title */ +"Notifications.Toasts.Title" = "Превью"; + +/* Notifications: Reactions header */ +"Notifications.Reactions.Header" = "Реакции"; + +/* Notifications: In-App notifications header */ +"Notifications.InAppNotifications.Header" = "В приложении"; + +/* Notifications: Select Alert Tones */ +"Notifications.Alert.Tones" = "ЗВУКИ УВЕДОМЛЕНИЙ"; + /* Notifications: Select Alert Save */ "Notifications.Alert.Save" = "Сохранить"; @@ -1015,6 +1051,9 @@ /* Transaction status: inconsistent reason title */ "TransactionStatus.Inconsistent.Reason.Title" = "Противоречивая причина"; +/* Transaction status: inconsistent data title */ +"TransactionStatus.Inconsistent.RecordData.Title" = "Данные транзакции"; + /* Transaction details: scene title */ "TransactionDetailsScene.Title" = "Подробности"; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings index 005f1d6fc..fa57aef0d 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings @@ -22,6 +22,9 @@ /* About scene: 'Contact us' section title. */ "About.Section.ContactUs" = "联系我们"; +/* Commit Hash */ +"About.Version.Commit" = "Commit: %@"; + /* Contribute scene: 'Crashlytics' section title. */ "Contribute.Section.Crashlytics" = "Crashlytics"; @@ -166,6 +169,9 @@ /* Account tab: 'Buy tokens' button */ "AccountTab.Row.BuyTokens" = "交换代币"; +/* Account tab: 'Stake ADM' button */ +"AccountTab.Row.StakeAdm" = "赌注和收入"; + /* Account tab: 'Logout' button */ "AccountTab.Row.Logout" = "注销"; @@ -202,6 +208,12 @@ /* Account tab: Exchange ADM tokens in chat */ "AccountTab.Row.ExchangeADMInChat" = "聊天交流"; +/* Account tab: Exchanges on CMC */ +"AccountTab.Row.ExchangesOnCoinMarketCap" = "CoinMarketCap 上的交易所"; + +/* Account tab: Exchanges on CoinGecko */ +"AccountTab.Row.ExchangesOnCoinGecko" = "CoinGecko 上的交易所"; + /* Account tab: 'Address' row */ "AccountTab.Row.Address" = "地址"; @@ -454,6 +466,9 @@ /* Delegates page: scene title */ "Delegates.Title" = "与会代表"; +/* Delegates page: header text */ +"Delegates.HeaderText" = "要入股 ADM 并获得奖励,请为您信任的活跃锻造池投票。在 [ADAMANT 博客](https://news.adamant.im/hodl-list-of-adamant-pools-join-in-and-get-rewards-491a98610f4b) 阅读更多内容."; + /* Visible Wallets page: scene title */ "VisibleWallets.Title" = "钱包列表"; @@ -637,6 +652,12 @@ /* New Chat: 'What does it mean?', a help button for info about uninitialized accounts. */ "NewChatScene.NotInitialized.HelpButton" = "这是什么意思?"; +/* Info Service: Info Service */ +"InfoService.InfoService" = "Info Service"; + +/* Info Service: inconsistent data */ +"InfoService.InconsistentData" = "Inconsistent data"; + /* NodesList: Button label */ "NodesList.NodesList" = "ADM节点列表"; @@ -805,6 +826,18 @@ /* Notifications: Select Sounds */ "Notifications.Sounds.Name" = "声音"; +/* Notifications: Vibrate title */ +"Notifications.Vibrate.Title" = "振动"; + +/* Notifications: Toasts title */ +"Notifications.Toasts.Title" = "祝酒词"; + +/* Notifications: Reactions header */ +"Notifications.Reactions.Header" = "反应"; + +/* Notifications: In-App notifications header */ +"Notifications.InAppNotifications.Header" = "应用程序内通知"; + /* Notifications: Select Alert Tones */ "Notifications.Alert.Tones" = "警报音"; @@ -1018,6 +1051,9 @@ /* Transaction status: inconsistent reason title */ "TransactionStatus.Inconsistent.Reason.Title" = "不一致的原因"; +/* Transaction status: inconsistent data title */ +"TransactionStatus.Inconsistent.RecordData.Title" = "Tx数据记录"; + /* Transaction details: scene title */ "TransactionDetailsScene.Title" = "详细信息"; diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/Contents.json new file mode 100644 index 000000000..2de93eb52 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "exch_anon@3x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "exch_anon@3x-2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "exch_anon@3x-3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/exch_anon@3x-2.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/exch_anon@3x-2.png new file mode 100644 index 000000000..1787e7909 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/exch_anon@3x-2.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/exch_anon@3x-3.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/exch_anon@3x-3.png new file mode 100644 index 000000000..ab36ba5fc Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/exch_anon@3x-3.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/exch_anon@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/exch_anon@3x.png new file mode 100644 index 000000000..86ebf749d Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/exch_anon.imageset/exch_anon@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/Contents.json new file mode 100644 index 000000000..a8925078b --- /dev/null +++ b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "coingecko@3x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "coingecko@3x-2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "coingecko@3x-3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/coingecko@3x-2.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/coingecko@3x-2.png new file mode 100644 index 000000000..4bbe64048 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/coingecko@3x-2.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/coingecko@3x-3.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/coingecko@3x-3.png new file mode 100644 index 000000000..08ab05304 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/coingecko@3x-3.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/coingecko@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/coingecko@3x.png new file mode 100644 index 000000000..f4293f22f Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coingecko.imageset/coingecko@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/Contents.json new file mode 100644 index 000000000..188ed7836 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "coinmarket@3x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "coinmarket@3x-2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "coinmarket@3x-3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/coinmarket@3x-2.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/coinmarket@3x-2.png new file mode 100644 index 000000000..90c5d02a5 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/coinmarket@3x-2.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/coinmarket@3x-3.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/coinmarket@3x-3.png new file mode 100644 index 000000000..7a6ce7ea7 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/coinmarket@3x-3.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/coinmarket@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/coinmarket@3x.png new file mode 100644 index 000000000..b2dde2d6a Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_coinmarket.imageset/coinmarket@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/Contents.json index 84ca8d215..6ec0c171b 100644 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/Contents.json +++ b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "row_faceid.png", + "filename" : "face-id@3x.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "row_faceid@2x.png", + "filename" : "face-id@3x-2.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "row_faceid@3x.png", + "filename" : "face-id@3x-3.png", "idiom" : "universal", "scale" : "3x" } diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/face-id@3x-2.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/face-id@3x-2.png new file mode 100644 index 000000000..fd2024d8b Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/face-id@3x-2.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/face-id@3x-3.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/face-id@3x-3.png new file mode 100644 index 000000000..80b9067b2 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/face-id@3x-3.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/face-id@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/face-id@3x.png new file mode 100644 index 000000000..4b8a7faf6 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/face-id@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/row_faceid.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/row_faceid.png deleted file mode 100644 index fa075c07a..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/row_faceid.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/row_faceid@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/row_faceid@2x.png deleted file mode 100644 index 441ff22af..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/row_faceid@2x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/row_faceid@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/row_faceid@3x.png deleted file mode 100644 index 4d99bc8b7..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_faceid.imageset/row_faceid@3x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Contents.json new file mode 100644 index 000000000..822af7cc6 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "Stake@3x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Stake@3x-2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Stake@3x-3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Stake@3x-2.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Stake@3x-2.png new file mode 100644 index 000000000..3394c50ed Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Stake@3x-2.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Stake@3x-3.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Stake@3x-3.png new file mode 100644 index 000000000..13430cb15 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Stake@3x-3.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Stake@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Stake@3x.png new file mode 100644 index 000000000..889ee89a0 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Row/row_stake.imageset/Stake@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/Contents.json index 88cbb81ed..78e8130df 100644 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/Contents.json +++ b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/Contents.json @@ -6,12 +6,12 @@ "scale" : "1x" }, { - "filename" : "no-token@2x.png", + "filename" : "no-token@2x_white.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "no-token@3x.png", + "filename" : "no-token@3x_white.png", "idiom" : "universal", "scale" : "3x" } diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token.png index ee0a0b29a..a3f02a85d 100644 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token.png and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@2x.png deleted file mode 100644 index 72863cedc..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@2x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@2x_white.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@2x_white.png new file mode 100644 index 000000000..23a0ca130 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@2x_white.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@3x.png deleted file mode 100644 index 014f9a68c..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@3x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@3x_white.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@3x_white.png new file mode 100644 index 000000000..fffe1319c Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@3x_white.png differ diff --git a/CommonKit/Sources/CommonKit/Core/NativeAdamantCore.swift b/CommonKit/Sources/CommonKit/Core/NativeAdamantCore.swift index a3e75721c..32e93519c 100644 --- a/CommonKit/Sources/CommonKit/Core/NativeAdamantCore.swift +++ b/CommonKit/Sources/CommonKit/Core/NativeAdamantCore.swift @@ -14,7 +14,7 @@ import CryptoSwift * Decoding and Encoding for messages and values */ -public final class NativeAdamantCore { +public final class NativeAdamantCore: AdamantCore { // MARK: - Messages public func encodeMessage(_ message: String, recipientPublicKey publicKey: String, privateKey privateKeyHex: String) -> (message: String, nonce: String)? { @@ -67,6 +67,18 @@ public final class NativeAdamantCore { return decrepted.utf8String } + public func sign(transaction: SignableTransaction, senderId: String, keypair: Keypair) -> String? { + let privateKey = keypair.privateKey.hexBytes() + let hash = transaction.bytes.sha256() + + guard let signature = Crypto.sign.signature(message: hash, secretKey: privateKey) else { + print("FAIL to sign of transaction") + return nil + } + + return signature.hexString() + } + public func encodeData( _ data: Data, recipientPublicKey publicKey: String, @@ -228,3 +240,86 @@ public extension String { }) } } + +// MARK: - Bytes +private extension SignableTransaction { + + var bytes: [UInt8] { + return + typeBytes + + timestampBytes + + senderPublicKeyBytes + + requesterPublicKeyBytes + + recipientIdBytes + + amountBytes + + assetBytes + + signatureBytes + + signSignatureBytes + } + + var typeBytes: [UInt8] { + return [UInt8(type.rawValue)] + } + + var timestampBytes: [UInt8] { + return ByteBackpacker.pack(UInt32(timestamp), byteOrder: .littleEndian) + } + + var senderPublicKeyBytes: [UInt8] { + return senderPublicKey.hexBytes() + } + + var requesterPublicKeyBytes: [UInt8] { + return 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] { + return [] + } + + var signSignatureBytes: [UInt8] { + return [] + } + + 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/CommonKit/Sources/CommonKit/Core/SecuredStore.swift b/CommonKit/Sources/CommonKit/Core/SecuredStore.swift index 7d40ff833..0dfab93d1 100644 --- a/CommonKit/Sources/CommonKit/Core/SecuredStore.swift +++ b/CommonKit/Sources/CommonKit/Core/SecuredStore.swift @@ -25,6 +25,10 @@ public extension StoreKey { public static let notificationsMode = "notifications.mode" public static let customBadgeNumber = "notifications.number" public static let notificationsSound = "notifications.sound" + public static let notificationsReactionSound = "notifications.reaction.sound" + public static let inAppSounds = "notifications.inAppSounds" + public static let inAppVibrate = "notifications.inAppVibrate" + public static let inAppToasts = "notifications.inAppToasts" } enum visibleWallets { @@ -69,7 +73,4 @@ public protocol SecuredStore: AnyObject { func set(_ value: T, for key: String) func remove(_ key: String) - - /// Remove everything - func purgeStore() } diff --git a/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApi.swift b/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApi.swift index 5d8080212..afbea385c 100644 --- a/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApi.swift +++ b/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApi.swift @@ -11,97 +11,19 @@ import Foundation public final class ExtensionsApi { // MARK: Properties private let addressBookKey = "contact_list" - private let nodesStoreKey = "nodesSource.nodes" - public let keychainStore: KeychainStore - - public private(set) lazy var nodes: [Node] = { - let nodes = keychainStore.get(nodesStoreKey) ?? AdamantResources.nodes - return nodes.filter { $0.isEnabled }.shuffled() - }() - - private var currentNode: Node? - - private func selectNewNode() { - currentNode = nodes.popLast() - } + private let apiService: AdamantApiServiceProtocol // MARK: Cotr - public init(keychainStore: KeychainStore) { - self.keychainStore = keychainStore + public init(apiService: AdamantApiServiceProtocol) { + self.apiService = apiService } // MARK: - API // MARK: Transactions public func getTransaction(by id: UInt64) -> Transaction? { - // MARK: 1. Getting Transaction - var response: ServerModelResponse? - var nodeUrl: URL! = nil - if currentNode == nil { - selectNewNode() - } - - repeat { - guard let node = currentNode, let url = node.asURL() else { - selectNewNode() - continue - } - nodeUrl = url - - do { - guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - selectNewNode() - continue - } - - components.path = "/api/transactions/get" - components.queryItems = [URLQueryItem(name: "id", value: "\(id)"), - URLQueryItem(name: "returnAsset", value: "1")] - - if let url = components.url { - let data = try Data(contentsOf: url) - response = try JSONDecoder().decode(ServerModelResponse.self, from: data) - } else { - selectNewNode() - continue - } - } catch { - selectNewNode() - continue - } - } while response == nil && nodes.count > 0 // Try until we have a transaction, or we run out of nodes - - guard let transaction = response?.model else { - return nil - } - - // MARK: 2. Working on transaction - - // For old nodes - if /api/transaction/get doesn't return chat asset - get it from /api/chats/ - if transaction.type == .chatMessage, transaction.asset.chat == nil { - do { - guard var components = URLComponents(url: nodeUrl, resolvingAgainstBaseURL: false) else { - return nil - } - - components.path = "/api/chats/get" - components.queryItems = [URLQueryItem(name: "recipientId", value: transaction.recipientId), - URLQueryItem(name: "orderBy", value: "timestamp:asc"), - URLQueryItem(name: "fromHeight", value: "\(transaction.height - 1)") - ] - - if let url = components.url { - let data = try Data(contentsOf: url) - let collection = try JSONDecoder().decode(ServerCollectionResponse.self, from: data) - return collection.collection?.first { $0.id == id } - } else { - return nil - } - } catch { - return nil - } - } else { - return transaction + Task.sync { [apiService] in + try? await apiService.getTransaction(id: id, withAsset: true).get() } } @@ -112,46 +34,18 @@ public final class ExtensionsApi { core: NativeAdamantCore, keypair: Keypair ) -> [String:ContactDescription]? { - var response: ServerCollectionResponse? - - // Getting transaction - repeat { - guard let node = currentNode, let url = node.asURL() else { - selectNewNode() - continue - } - - do { - guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - selectNewNode() - continue - } - - components.path = "/api/states/get" - components.queryItems = [URLQueryItem(name: "senderId", value: address), - URLQueryItem(name: "orderBy", value: "timestamp:desc"), - URLQueryItem(name: "key", value: addressBookKey)] - - if let url = components.url { - let data = try Data(contentsOf: url) - response = try JSONDecoder().decode(ServerCollectionResponse.self, from: data) - } else { - selectNewNode() - continue - } - } catch { - selectNewNode() - continue - } - } while response == nil && nodes.count > 0 // Try until we have a transaction, or we run out of nodes + let addressBookString = Task.sync { [apiService, addressBookKey] in + try? await apiService.get(key: addressBookKey, sender: address).get() + } // Working with transaction - guard let collection = response?.collection, - let object = collection.first?.asset.state?.value.toDictionary(), + guard + let object = addressBookString?.toDictionary(), let message = object["message"] as? String, - let nonce = object["nonce"] as? String else { - return nil + let nonce = object["nonce"] as? String + else { + return nil } // Decoding diff --git a/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApiFactory.swift b/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApiFactory.swift new file mode 100644 index 000000000..0eb644d1e --- /dev/null +++ b/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApiFactory.swift @@ -0,0 +1,45 @@ +// +// ExtensionsApiFactory.swift +// +// +// Created by Andrew G on 08.08.2024. +// + +import Combine + +public struct ExtensionsApiFactory { + public let core: AdamantCore + public let securedStore: SecuredStore + + public init(core: AdamantCore, securedStore: SecuredStore) { + self.core = core + self.securedStore = securedStore + } + + public func make() -> ExtensionsApi { + .init(apiService: AdamantApiService( + healthCheckWrapper: .init( + service: AdamantApiCore(apiCore: APICore()), + nodesStorage: NodesStorage( + securedStore: securedStore, + nodesMergingService: NodesMergingService(), + defaultNodes: { _ in .init() } + ), + nodesAdditionalParamsStorage: NodesAdditionalParamsStorage( + securedStore: securedStore + ), + isActive: false, + params: .init( + group: .adm, + name: "ADM", + normalUpdateInterval: .infinity, + crucialUpdateInterval: .infinity, + minNodeVersion: nil, + nodeHeightEpsilon: .zero + ), + connection: Just(true).eraseToAnyPublisher() + ), + adamantCore: core + )) + } +} diff --git a/Adamant/Helpers/ADM+JsonDecode.swift b/CommonKit/Sources/CommonKit/Helpers/ADM+JsonDecode.swift similarity index 93% rename from Adamant/Helpers/ADM+JsonDecode.swift rename to CommonKit/Sources/CommonKit/Helpers/ADM+JsonDecode.swift index 778fe1667..cb85f5854 100644 --- a/Adamant/Helpers/ADM+JsonDecode.swift +++ b/CommonKit/Sources/CommonKit/Helpers/ADM+JsonDecode.swift @@ -8,22 +8,22 @@ import Foundation -struct JSONCodingKeys: CodingKey { - var stringValue: String +public struct JSONCodingKeys: CodingKey { + public var stringValue: String - init?(stringValue: String) { + public init?(stringValue: String) { self.stringValue = stringValue } - var intValue: Int? + public var intValue: Int? - init?(intValue: Int) { + public init?(intValue: Int) { self.init(stringValue: "\(intValue)") self.intValue = intValue } } -extension KeyedDecodingContainer { +public extension KeyedDecodingContainer { func decode(forKey key: K) throws -> Data { if let stringValue = try? decode(String.self, forKey: key) { return Data(stringValue.utf8) @@ -82,7 +82,7 @@ extension KeyedDecodingContainer { } } -extension UnkeyedDecodingContainer { +public extension UnkeyedDecodingContainer { mutating func decode(_ type: Array.Type) throws -> [Any] { var array: [Any] = [] while isAtEnd == false { diff --git a/Adamant/ServiceProtocols/AdamantCore/AdamantCore+Extensions.swift b/CommonKit/Sources/CommonKit/Helpers/AdamantCore+Extensions.swift similarity index 98% rename from Adamant/ServiceProtocols/AdamantCore/AdamantCore+Extensions.swift rename to CommonKit/Sources/CommonKit/Helpers/AdamantCore+Extensions.swift index 2cebbc0be..006a642c4 100644 --- a/Adamant/ServiceProtocols/AdamantCore/AdamantCore+Extensions.swift +++ b/CommonKit/Sources/CommonKit/Helpers/AdamantCore+Extensions.swift @@ -7,10 +7,9 @@ // import Foundation -import CommonKit import BigInt -extension AdamantCore { +public extension AdamantCore { func makeSignedTransaction( transaction: SignableTransaction, senderId: String, @@ -97,7 +96,7 @@ extension AdamantCore { // MARK: - Bytes -extension UnregisteredTransaction { +public extension UnregisteredTransaction { func generateId() -> String? { let hash = bytes.sha256() diff --git a/CommonKit/Sources/CommonKit/Helpers/AdamantSecureStorage.swift b/CommonKit/Sources/CommonKit/Helpers/AdamantSecureStorage.swift new file mode 100644 index 000000000..c622f3a7c --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/AdamantSecureStorage.swift @@ -0,0 +1,80 @@ +// +// AdamantSecureStorage.swift +// +// +// Created by Stanislav Jelezoglo on 02.08.2024. +// + +import Foundation + +public struct AdamantSecureStorage: SecureStorageProtocol { + private let tag = "com.adamant.keys.id".data(using: .utf8)! + + public init() { } + + public func getPrivateKey() -> SecKey? { + loadPrivateKey() ?? createAndStorePrivateKey() + } + + public func getPublicKey(privateKey: SecKey) -> SecKey? { + SecKeyCopyPublicKey(privateKey) + } + + public func encrypt(data: Data, publicKey: SecKey) -> Data? { + SecKeyCreateEncryptedData( + publicKey, + .eciesEncryptionCofactorX963SHA256AESGCM, + data as CFData, + nil + ).map { $0 as Data } + } + + public func decrypt(data: Data, privateKey: SecKey) -> Data? { + SecKeyCreateDecryptedData( + privateKey, + .eciesEncryptionCofactorX963SHA256AESGCM, + data as CFData, + nil + ).map { $0 as Data } + } +} + +private extension AdamantSecureStorage { + func loadPrivateKey() -> SecKey? { + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: tag, + kSecAttrKeyType as String: kSecAttrKeyTypeEC, + kSecReturnRef as String: true + ] + + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + + return status == errSecSuccess + ? (item as! SecKey) + : nil + } + + func createAndStorePrivateKey() -> SecKey? { + guard let access = SecAccessControlCreateWithFlags( + kCFAllocatorDefault, + kSecAttrAccessibleAfterFirstUnlock, + .privateKeyUsage, + nil + ) else { return nil } + + let attributes: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeEC, + kSecAttrKeySizeInBits as String: 256, + kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, + kSecPrivateKeyAttrs as String: [ + kSecAttrIsPermanent as String: true, + kSecAttrApplicationTag as String: tag, + kSecAttrAccessControl as String: access + ] + ] + + return SecKeyCreateRandomKey(attributes as CFDictionary, nil) + } +} diff --git a/CommonKit/Sources/CommonKit/Helpers/AdamantUtilities.swift b/CommonKit/Sources/CommonKit/Helpers/AdamantUtilities.swift index 10f91c1e9..c86213044 100644 --- a/CommonKit/Sources/CommonKit/Helpers/AdamantUtilities.swift +++ b/CommonKit/Sources/CommonKit/Helpers/AdamantUtilities.swift @@ -10,6 +10,8 @@ import Foundation import os public enum AdamantUtilities { + public enum Git {} + public static let admCurrencyExponent: Int = -8 // MARK: - Dates @@ -58,3 +60,10 @@ public enum AdamantUtilities { os_log("adamant-console-log %{public}@", message) } } + +public extension AdamantUtilities.Git { + static let commitHash = Bundle.module.url( + forResource: "GitData", + withExtension: "plist" + ).flatMap { NSDictionary(contentsOf: $0)?.value(forKey: "CommitHash") as? String } +} diff --git a/CommonKit/Sources/CommonKit/Helpers/ApiServiceError+AFError.swift b/CommonKit/Sources/CommonKit/Helpers/ApiServiceError+AFError.swift new file mode 100644 index 000000000..8d6c1a6ac --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/ApiServiceError+AFError.swift @@ -0,0 +1,21 @@ +// +// ApiServiceError+AFError.swift +// +// +// Created by Andrew G on 08.08.2024. +// + +import Alamofire + +public extension ApiServiceError { + init(error: Error) { + let afError = error as? AFError + + switch afError { + case .explicitlyCancelled: + self = .requestCancelled + default: + self = .networkError(error: error) + } + } +} diff --git a/CommonKit/Sources/CommonKit/Helpers/Atomic.swift b/CommonKit/Sources/CommonKit/Helpers/Atomic.swift index 2ebcfc977..4b83208df 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Atomic.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Atomic.swift @@ -2,7 +2,7 @@ // Atomic.swift // // -// Created by Andrey Golubenko on 21.08.2023. +// Created by Andrew on 21.08.2023. // import Foundation @@ -16,42 +16,45 @@ import Foundation /// results or crashes. /// In order to ensure you've acquired the lock for a certain amount of time use the `mutate` method. @propertyWrapper -public final class Atomic { - private var value: Value +public final class Atomic: @unchecked Sendable { + private var _value: Value private let lock = NSLock() public var projectedValue: Atomic { self } public var wrappedValue: Value { + get { value } + set { value = newValue } + } + + public var value: Value { get { lock.lock() defer { lock.unlock() } - return value + return _value } set { lock.lock() defer { lock.unlock() } - value = newValue + _value = newValue } } - - public init(wrappedValue: Value) { - value = wrappedValue + + public init(_ value: Value) { + _value = value } - - /// Synchronises mutation to ensure the value doesn't get changed by another thread during this mutation. - public func mutate(_ mutation: (inout Value) -> Void) { - lock.lock() - defer { lock.unlock() } - mutation(&value) + + public convenience init(wrappedValue: Value) { + self.init(wrappedValue) } /// Synchronises mutation to ensure the value doesn't get changed by another thread during this mutation. /// This method returns a value specified in the `mutation` closure. + @discardableResult public func mutate(_ mutation: (inout Value) -> T) -> T { lock.lock() defer { lock.unlock() } - return mutation(&value) + return mutation(&_value) } } diff --git a/Adamant/Utilities/ByteBackpacker.swift b/CommonKit/Sources/CommonKit/Helpers/ByteBackpacker.swift similarity index 100% rename from Adamant/Utilities/ByteBackpacker.swift rename to CommonKit/Sources/CommonKit/Helpers/ByteBackpacker.swift diff --git a/CommonKit/Sources/CommonKit/Helpers/Date+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/Date+adamant.swift index 5e1e9e7b8..69fba5488 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Date+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Date+adamant.swift @@ -55,11 +55,17 @@ public extension Date { } /// Returns readable day string. "Today, Yesterday, etc" - func humanizedDay() -> String { + func humanizedDay(useTimeFormat: Bool) -> String { let dateString: String - if isToday { // Today - dateString = String.localized("Chats.Date.Today") + if useTimeFormat { + let formatter = defaultFormatter + formatter.dateStyle = .none + formatter.timeStyle = .short + dateString = formatter.string(from: self) + } else { + dateString = String.localized("Chats.Date.Today") + } } else if daysAgo < 2 { // Yesterday dateString = elapsedTime(from: self) } else if weeksAgo < 1 { // This week, show weekday, month and date diff --git a/CommonKit/Sources/CommonKit/Helpers/Node+NodeKeychainDTO.swift b/CommonKit/Sources/CommonKit/Helpers/Node+NodeKeychainDTO.swift new file mode 100644 index 000000000..d9196c714 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/Node+NodeKeychainDTO.swift @@ -0,0 +1,40 @@ +// +// Node+NodeDTO.swift +// +// +// Created by Andrew G on 28.07.2024. +// + +extension Node { + func mapToDto() -> NodeKeychainDTO { + .init( + mainOrigin: mainOrigin, + altOrigin: altOrigin, + wsEnabled: wsEnabled, + isEnabled: isEnabled, + version: version?.string, + height: height, + ping: ping, + connectionStatus: connectionStatus, + type: type + ) + } +} + +extension NodeKeychainDTO { + func mapToModel() -> Node { + .init( + id: .init(), + isEnabled: isEnabled, + wsEnabled: wsEnabled, + mainOrigin: mainOrigin, + altOrigin: altOrigin, + version: version.flatMap { .init($0) }, + height: height, + ping: ping, + connectionStatus: connectionStatus, + preferMainOrigin: nil, + type: type + ) + } +} diff --git a/CommonKit/Sources/CommonKit/Models/NodeGroup+Constants.swift b/CommonKit/Sources/CommonKit/Helpers/NodeGroup+Constants.swift similarity index 94% rename from CommonKit/Sources/CommonKit/Models/NodeGroup+Constants.swift rename to CommonKit/Sources/CommonKit/Helpers/NodeGroup+Constants.swift index 739e258fb..d4d9bdb73 100644 --- a/CommonKit/Sources/CommonKit/Models/NodeGroup+Constants.swift +++ b/CommonKit/Sources/CommonKit/Helpers/NodeGroup+Constants.swift @@ -12,7 +12,7 @@ public extension NodeGroup { switch self { case .adm: return false - case .eth, .doge, .dash, .btc, .klyNode, .klyService, .ipfs: + case .eth, .doge, .dash, .btc, .klyNode, .klyService, .ipfs, .infoService: return true } } diff --git a/CommonKit/Sources/CommonKit/Helpers/SafeDecodingArray.swift b/CommonKit/Sources/CommonKit/Helpers/SafeDecodingArray.swift new file mode 100644 index 000000000..f3898e692 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/SafeDecodingArray.swift @@ -0,0 +1,49 @@ +// +// SafeDecodingArray.swift +// +// +// Created by Andrew G on 30.07.2024. +// + +public struct SafeDecodingArray { + public let values: [T] + + init(_ values: [T]) { + self.values = values + } +} + +extension SafeDecodingArray: Sequence { + public typealias Element = T + public typealias Iterator = IndexingIterator<[Element]> + + public func makeIterator() -> Iterator { + values.makeIterator() + } +} + +extension SafeDecodingArray: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(values) + } +} + +extension SafeDecodingArray: Decodable { + struct Item { + let value: Value? + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let items = try container.decode([Item].self) + values = items.compactMap { $0.value } + } +} + +extension SafeDecodingArray.Item: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + value = try? container.decode(Value.self) + } +} diff --git a/CommonKit/Sources/CommonKit/Helpers/SafeDecodingDictionary.swift b/CommonKit/Sources/CommonKit/Helpers/SafeDecodingDictionary.swift new file mode 100644 index 000000000..4b7dcf028 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/SafeDecodingDictionary.swift @@ -0,0 +1,61 @@ +// +// SafeDecodingDictionary.swift +// +// +// Created by Andrew G on 02.08.2024. +// + +public struct SafeDecodingDictionary { + public let values: [Key: Value] + + init(_ values: [Key: Value]) { + self.values = values + } +} + +extension SafeDecodingDictionary: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(values) + } +} + +extension SafeDecodingDictionary: Decodable { + struct KeyItem: Hashable { + let value: T? + } + + struct ValueItem { + let value: T? + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let items = try container.decode([KeyItem: ValueItem].self) + + let keysAndValues: [(Key, Value)] = items.compactMap { keyItem, valueItem in + guard + let key = keyItem.value, + let value = valueItem.value + else { return nil } + + return (key, value) + } + + values = .init(uniqueKeysWithValues: keysAndValues) + } +} + +extension SafeDecodingDictionary.KeyItem: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + value = try? container.decode(T.self) + } +} + +extension SafeDecodingDictionary.ValueItem: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + value = try? container.decode(T.self) + } +} diff --git a/CommonKit/Sources/CommonKit/Helpers/SecureStorageProtocol.swift b/CommonKit/Sources/CommonKit/Helpers/SecureStorageProtocol.swift new file mode 100644 index 000000000..d71eb3d9f --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/SecureStorageProtocol.swift @@ -0,0 +1,15 @@ +// +// SecureStorageProtocol.swift +// +// +// Created by Stanislav Jelezoglo on 05.08.2024. +// + +import Foundation + +public protocol SecureStorageProtocol { + func getPrivateKey() -> SecKey? + func getPublicKey(privateKey: SecKey) -> SecKey? + func encrypt(data: Data, publicKey: SecKey) -> Data? + func decrypt(data: Data, privateKey: SecKey) -> Data? +} diff --git a/Adamant/Helpers/ServerResponse+Resolver.swift b/CommonKit/Sources/CommonKit/Helpers/ServerResponse+Resolver.swift similarity index 89% rename from Adamant/Helpers/ServerResponse+Resolver.swift rename to CommonKit/Sources/CommonKit/Helpers/ServerResponse+Resolver.swift index fef3485a3..c59a2d339 100644 --- a/Adamant/Helpers/ServerResponse+Resolver.swift +++ b/CommonKit/Sources/CommonKit/Helpers/ServerResponse+Resolver.swift @@ -6,9 +6,7 @@ // Copyright © 2023 Adamant. All rights reserved. // -import CommonKit - -extension ServerModelResponse { +public extension ServerModelResponse { func resolved() -> ApiServiceResult { if let model = model { return .success(model) @@ -18,7 +16,7 @@ extension ServerModelResponse { } } -extension ServerCollectionResponse { +public extension ServerCollectionResponse { func resolved() -> ApiServiceResult<[T]> { if let collection = collection { return .success(collection) @@ -28,7 +26,7 @@ extension ServerCollectionResponse { } } -extension TransactionIdResponse { +public extension TransactionIdResponse { func resolved() -> ApiServiceResult { if let ransactionId = transactionId { return .success(ransactionId) @@ -38,7 +36,7 @@ extension TransactionIdResponse { } } -extension GetPublicKeyResponse { +public extension GetPublicKeyResponse { func resolved() -> ApiServiceResult { if let publicKey = publicKey { return .success(publicKey) diff --git a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/View+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/View+Extension.swift index 8ce6b6d67..c183ac767 100644 --- a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/View+Extension.swift +++ b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/View+Extension.swift @@ -39,6 +39,7 @@ public extension View { return resultView } + // TODO: Remove this function (or fix) func fullScreen() -> some View { return frame(width: .infinity, height: .infinity) .ignoresSafeArea() diff --git a/CommonKit/Sources/CommonKit/Helpers/Task+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/Task+Extension.swift index ccf203117..c73276b4a 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Task+Extension.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Task+Extension.swift @@ -12,4 +12,24 @@ public extension Task where Success == Never, Failure == Never { static func sleep(interval: TimeInterval) async { try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) } + + /// Avoid using it. It lowers performance due to changing threads. + @discardableResult + static func sync(_ action: @Sendable @escaping () async -> T) -> T { + _sync(action) + } +} + +@discardableResult +private func _sync(_ action: @Sendable @escaping () async -> T) -> T { + let result = Atomic(wrappedValue: nil) + let semaphore = DispatchSemaphore(value: .zero) + + Task { + result.value = await action() + semaphore.signal() + } + + semaphore.wait() + return result.value! } diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift index 640ae44d5..837dee852 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift @@ -21,18 +21,15 @@ extension UIColor { } // MARK: Colors from Adamant Guideline - public static let first = UIColor(hex: "#474a5f") - public static let fourth = UIColor(hex: "#eeeeee") - - public static let active = UIColor(red: 0.0901961, green: 0.611765, blue: 0.92549, alpha: 1) - public static let alert = UIColor(hex: "#faa05a") - public static let good = UIColor(hex: "#32d296") - public static let danger = UIColor(hex: "#f0506e") - public static let inactive = UIColor(hex: "#6d6f72") + public static let active = #colorLiteral(red: 0.09019607843, green: 0.6117647059, blue: 0.9215686275, alpha: 1) //#179CEB + public static let attention = #colorLiteral(red: 0.9902971387, green: 0.6896653175, blue: 0.4256819189, alpha: 1) //#faa05a + public static let success = #colorLiteral(red: 0.2102436721, green: 0.8444728255, blue: 0.6537195444, alpha: 1) //#32d296 + public static let warning = #colorLiteral(red: 0.9622407556, green: 0.4130832553, blue: 0.5054324269, alpha: 1) //#f0506e + public static let inactive = #colorLiteral(red: 0.5025414228, green: 0.5106091499, blue: 0.5218499899, alpha: 1) //#6d6f72 public static var background: UIColor { - let colorWhiteTheme = UIColor(hex: "#f2f6fa") - let colorDarkTheme = UIColor(hex: "#1c1c1c") + let colorWhiteTheme = #colorLiteral(red: 0.9590962529, green: 0.9721178412, blue: 0.9845080972, alpha: 1) //f2f6fa + let colorDarkTheme = #colorLiteral(red: 0.1462407112, green: 0.1462407112, blue: 0.1462407112, alpha: 1) //1c1c1c return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } @@ -40,32 +37,32 @@ extension UIColor { /// Income Arrow View Background Color public static var incomeArrowBackgroundColor: UIColor { - return UIColor(hex: "36C436") + return #colorLiteral(red: 0.2381577492, green: 0.7938874364, blue: 0.2725245357, alpha: 1) //36C436 } /// Outcome Arrow View Background Color public static var outcomeArrowBackgroundColor: UIColor { - return UIColor(hex: "F44444") + return #colorLiteral(red: 0.9752754569, green: 0.3635693789, blue: 0.3339065611, alpha: 1) //F44444 } /// Default background color public static var backgroundColor: UIColor { let colorWhiteTheme = UIColor.white - let colorDarkTheme = UIColor(hex: "#212121") + let colorDarkTheme = #colorLiteral(red: 0.1726317406, green: 0.1726317406, blue: 0.1726317406, alpha: 1) //212121 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Second default background color public static var secondBackgroundColor: UIColor { - let colorWhiteTheme = UIColor(hex: "#f2f1f6") - let colorDarkTheme = UIColor(hex: "#212121") + let colorWhiteTheme = #colorLiteral(red: 0.9594989419, green: 0.956831634, blue: 0.9719926715, alpha: 1) //f2f1f6 + let colorDarkTheme = #colorLiteral(red: 0.1725490196, green: 0.1725490196, blue: 0.1725490196, alpha: 1) //2C2C2C return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Welcome background color public static var welcomeBackgroundColor: UIColor { let colorWhiteTheme = UIColor(patternImage: .asset(named: "stripeBg") ?? .init()) - let colorDarkTheme = UIColor(hex: "#212121") + let colorDarkTheme = #colorLiteral(red: 0.1294117647, green: 0.1294117647, blue: 0.1294117647, alpha: 1) //212121 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Default text color @@ -78,70 +75,70 @@ extension UIColor { /// Default cell alert text color public static var cellAlertTextColor: UIColor { let colorWhiteTheme = UIColor.white - let colorDarkTheme = UIColor(hex: "#212121") + let colorDarkTheme = #colorLiteral(red: 0.1294117647, green: 0.1294117647, blue: 0.1294117647, alpha: 1) //212121 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Default cell color public static var cellColor: UIColor { let colorWhiteTheme = UIColor.white - let colorDarkTheme = UIColor(hex: "#1c1c1d") + let colorDarkTheme = #colorLiteral(red: 0.1098039216, green: 0.1098039216, blue: 0.1137254902, alpha: 1) //1c1c1d return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Code block background color public static var codeBlock: UIColor { - let colorWhiteTheme = UIColor(red: 0.29, green: 0.29, blue: 0.29, alpha: 0.1) - let colorDarkTheme = UIColor(hex: "#2a2a2b") + let colorWhiteTheme = UIColor(hex: "#4a4a4a").withAlphaComponent(0.1) + let colorDarkTheme = #colorLiteral(red: 0.1647058824, green: 0.1647058824, blue: 0.168627451, alpha: 1) //2a2a2b return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Code block text color public static var codeBlockText: UIColor { - let colorWhiteTheme = UIColor(red: 0.32, green: 0.32, blue: 0.32, alpha: 1) + let colorWhiteTheme = #colorLiteral(red: 0.3215686275, green: 0.3215686275, blue: 0.3215686275, alpha: 1) //525252 let colorDarkTheme = UIColor.white.withAlphaComponent(0.8) return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Reactions background color public static var reactionsBackground: UIColor { - 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) + let colorWhiteTheme = #colorLiteral(red: 0.9490196078, green: 0.9490196078, blue: 0.9490196078, alpha: 1) //F2F2F2 + let colorDarkTheme = #colorLiteral(red: 0.262745098, green: 0.262745098, blue: 0.262745098, alpha: 1) //434343 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// More reactions background button color public static var moreReactionsBackground: UIColor { let colorWhiteTheme = UIColor.white - let colorDarkTheme = UIColor(red: 0.22, green: 0.22, blue: 0.22, alpha: 1) + let colorDarkTheme = #colorLiteral(red: 0.2196078431, green: 0.2196078431, blue: 0.2196078431, alpha: 1) //383838 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Picked reaction background color public static var pickedReactionBackground: UIColor { - let colorWhiteTheme = UIColor(red: 0.92, green: 0.925, blue: 0.93, alpha: 0.85) - let colorDarkTheme = UIColor(red: 0.329, green: 0.329, blue: 0.329, alpha: 1.0) + let colorWhiteTheme = UIColor(hex: "#EBECED").withAlphaComponent(0.85) + let colorDarkTheme = #colorLiteral(red: 0.3294117647, green: 0.3294117647, blue: 0.3294117647, alpha: 1) //545454 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Main dark gray, ~70% gray public static var primary: UIColor { - let colorWhiteTheme = UIColor(red: 0.29, green: 0.29, blue: 0.29, alpha: 1) + let colorWhiteTheme = #colorLiteral(red: 0.2901960784, green: 0.2901960784, blue: 0.2901960784, alpha: 1) //4A4A4A let colorDarkTheme = UIColor.white return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Secondary color, ~50% gray public static var secondary: UIColor { - let colorWhiteTheme = UIColor(red: 0.478, green: 0.478, blue: 0.478, alpha: 1) - let colorDarkTheme = UIColor(red: 0.878, green: 0.878, blue: 0.878, alpha: 1) + let colorWhiteTheme = #colorLiteral(red: 0.4784313725, green: 0.4784313725, blue: 0.4784313725, alpha: 1) //7A7A7A + let colorDarkTheme = #colorLiteral(red: 0.8784313725, green: 0.8784313725, blue: 0.8784313725, alpha: 1) //E0E0E0 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Chat icons color, ~40% gray public static var chatIcons: UIColor { - let colorWhiteTheme = UIColor(red: 0.62, green: 0.62, blue: 0.62, alpha: 1) - let colorDarkTheme = UIColor(red: 0.278, green: 0.278, blue: 0.278, alpha: 1) + let colorWhiteTheme = #colorLiteral(red: 0.6196078431, green: 0.6196078431, blue: 0.6196078431, alpha: 1) //9E9E9E + let colorDarkTheme = #colorLiteral(red: 0.2784313725, green: 0.2784313725, blue: 0.2784313725, alpha: 1) //474747 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } @@ -152,17 +149,30 @@ extension UIColor { return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } + /// Chat list, swipe color + public static var swipeMoreColor: UIColor { + let colorWhiteTheme = #colorLiteral(red: 0.8784313725, green: 0.8784313725, blue: 0.8784313725, alpha: 1) //E0E0E0 + let colorDarkTheme = #colorLiteral(red: 0.3294117647, green: 0.3294117647, blue: 0.3294117647, alpha: 1) //545454 + return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) + } + + public static var swipeBlockColor: UIColor { + let colorWhiteTheme = #colorLiteral(red: 0.9254901961, green: 0.9254901961, blue: 0.9254901961, alpha: 1) //ECECEC + let colorDarkTheme = #colorLiteral(red: 0.2705882353, green: 0.2705882353, blue: 0.2705882353, alpha: 1) //474747 + return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) + } + /// Switch onTintColor public static var switchColor: UIColor { - let colorWhiteTheme = UIColor(hex: "#179cec") - let colorDarkTheme = UIColor(hex: "#05456b") + let colorWhiteTheme = #colorLiteral(red: 0.09019607843, green: 0.6117647059, blue: 0.9254901961, alpha: 1) //179cec + let colorDarkTheme = #colorLiteral(red: 0.01960784314, green: 0.2705882353, blue: 0.4196078431, alpha: 1) //05456b return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Secondary color, ~50% gray public static var errorOkButton: UIColor { - let colorWhiteTheme = UIColor(red: 0.478, green: 0.478, blue: 0.478, alpha: 1) - let colorDarkTheme = UIColor(red: 0.31, green: 0.31, blue: 0.31, alpha: 1) + let colorWhiteTheme = #colorLiteral(red: 0.4784313725, green: 0.4784313725, blue: 0.4784313725, alpha: 1) //7A7A7A + let colorDarkTheme = #colorLiteral(red: 0.3098039216, green: 0.3098039216, blue: 0.3098039216, alpha: 1) //4F4F4F return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } @@ -170,82 +180,76 @@ extension UIColor { /// User chat bubble background, ~4% gray public static var chatRecipientBackground: UIColor { - let colorWhiteTheme = UIColor(red: 0.965, green: 0.973, blue: 0.981, alpha: 1) - let colorDarkTheme = UIColor(red: 0.27, green: 0.27, blue: 0.27, alpha: 1) + let colorWhiteTheme = #colorLiteral(red: 0.9647058824, green: 0.9725490196, blue: 0.9843137255, alpha: 1) //F6F8FB + let colorDarkTheme = #colorLiteral(red: 0.2705882353, green: 0.2705882353, blue: 0.2705882353, alpha: 1) //454545 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } public static var pendingChatBackground: UIColor { let colorWhiteTheme = UIColor(white: 0.98, alpha: 1.0) - let colorDarkTheme = UIColor(red: 0.42, green: 0.42, blue: 0.42, alpha: 1) + let colorDarkTheme = #colorLiteral(red: 0.4196078431, green: 0.4196078431, blue: 0.4196078431, alpha: 1) //6B6B6B return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } public static var failChatBackground: UIColor { let colorWhiteTheme = UIColor(white: 0.8, alpha: 1.0) - let colorDarkTheme = UIColor(red: 0.46, green: 0.46, blue: 0.46, alpha: 1) + let colorDarkTheme = #colorLiteral(red: 0.4588235294, green: 0.4588235294, blue: 0.4588235294, alpha: 1) //757575 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Partner chat bubble background, ~8% gray public static var chatSenderBackground: UIColor { - let colorWhiteTheme = UIColor(red: 0.925, green: 0.925, blue: 0.925, alpha: 1) - let colorDarkTheme = UIColor(red: 0.21, green: 0.21, blue: 0.21, alpha: 1) + let colorWhiteTheme = #colorLiteral(red: 0.9254901961, green: 0.9254901961, blue: 0.9254901961, alpha: 1) //ECECEC + let colorDarkTheme = #colorLiteral(red: 0.2117647059, green: 0.2117647059, blue: 0.2117647059, alpha: 1) //363636 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Partner chat bubble background, ~8% gray public static var chatInputBarBackground: UIColor { - let colorWhiteTheme = UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0) - let colorDarkTheme = UIColor(red: 0.20, green: 0.20, blue: 0.20, alpha: 1) + let colorWhiteTheme = #colorLiteral(red: 0.968627451, green: 0.968627451, blue: 0.968627451, alpha: 1) //F7F7F7 + let colorDarkTheme = #colorLiteral(red: 0.2, green: 0.2, blue: 0.2, alpha: 1) //333333 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// InputBar field background, ~8% gray public static var chatInputFieldBarBackground: UIColor { let colorWhiteTheme = UIColor.white - let colorDarkTheme = UIColor(hex: "#212121") + let colorDarkTheme = #colorLiteral(red: 0.1294117647, green: 0.1294117647, blue: 0.1294117647, alpha: 1) //212121 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Border colors for readOnly mode public static var disableBorderColor: UIColor { - let colorWhiteTheme = UIColor(hex: "#B0B0B0") - let colorDarkTheme = UIColor(hex: "#878787") + let colorWhiteTheme = #colorLiteral(red: 0.6901960784, green: 0.6901960784, blue: 0.6901960784, alpha: 1) //B0B0B0 + let colorDarkTheme = #colorLiteral(red: 0.5294117647, green: 0.5294117647, blue: 0.5294117647, alpha: 1) //878787 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - public static let chatInputBarBorderColor = UIColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 1) + public static let chatInputBarBorderColor = #colorLiteral(red: 0.7843137255, green: 0.7843137255, blue: 0.7843137255, alpha: 1) //C8C8C8 /// Color of input bar placeholder - public static let chatPlaceholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) + public static let chatPlaceholderTextColor = #colorLiteral(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) //999999 // MARK: Context Menu public static var contextMenuLineColor: UIColor { - let colorWhiteTheme = UIColor(red: 0.75, green: 0.75, blue: 0.75, alpha: 0.8) - let colorDarkTheme = UIColor(red: 0.50, green: 0.50, blue: 0.50, alpha: 0.8) + let colorWhiteTheme = UIColor(hex: "#BFBFBF").withAlphaComponent(0.8) + let colorDarkTheme = UIColor(hex: "#808080").withAlphaComponent(0.8) return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } public static var contextMenuSelectColor: UIColor { let colorWhiteTheme = UIColor.black.withAlphaComponent(0.10) - let colorDarkTheme = UIColor(red: 0.214, green: 0.214, blue: 0.214, alpha: 1) + let colorDarkTheme = #colorLiteral(red: 0.2156862745, green: 0.2156862745, blue: 0.2156862745, alpha: 1) //#373737 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } public static var contextMenuDefaultBackgroundColor: UIColor { - 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) + let colorWhiteTheme = #colorLiteral(red: 0.9490196078, green: 0.9490196078, blue: 0.9490196078, alpha: 1) //#F2F2F2 + let colorDarkTheme = #colorLiteral(red: 0.262745098, green: 0.262745098, blue: 0.262745098, alpha: 1) //#434343 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - - public static var contextMenuTextColor: UIColor { - let colorWhiteTheme = UIColor.black - let colorDarkTheme = UIColor.white - return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) - } - + public static var contextMenuOverlayMacColor: UIColor { let colorWhiteTheme = UIColor.black.withAlphaComponent(0.3) let colorDarkTheme = UIColor.white.withAlphaComponent(0.3) @@ -253,18 +257,18 @@ extension UIColor { } public static var contextMenuDestructive: UIColor { - UIColor(red: 1, green: 0.2196078431, blue: 0.137254902, alpha: 1) + #colorLiteral(red: 1, green: 0.2196078431, blue: 0.137254902, alpha: 1) //#FF3823 } // MARK: Pinpad /// Pinpad highligh button background, 12% gray - public static let pinpadHighlightButton = UIColor(red: 0.88, green: 0.88, blue: 0.88, alpha: 1) + public static let pinpadHighlightButton = #colorLiteral(red: 0.8823529412, green: 0.8823529412, blue: 0.8823529412, alpha: 1) //#E1E1E1 // MARK: Transfers /// Income transfer icon background, light green - public static let transferIncomeIconBackground = UIColor(red: 0.7, green: 0.93, blue: 0.55, alpha: 1) + public static let transferIncomeIconBackground = #colorLiteral(red: 0.7019607843, green: 0.9294117647, blue: 0.5490196078, alpha: 1) //#B3ED8C // Outcome transfer icon background, light red - public static let transferOutcomeIconBackground = UIColor(red: 0.94, green: 0.52, blue: 0.53, alpha: 1) + public static let transferOutcomeIconBackground = #colorLiteral(red: 0.9411764706, green: 0.5215686275, blue: 0.5294117647, alpha: 1) //#F08587 } } diff --git a/CommonKit/Sources/CommonKit/Helpers/Version.swift b/CommonKit/Sources/CommonKit/Helpers/Version.swift new file mode 100644 index 000000000..45919001f --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/Version.swift @@ -0,0 +1,60 @@ +// +// Version.swift +// +// +// Created by Andrew G on 22.08.2024. +// + +public struct Version { + public let versions: [Int] + + public init(_ versions: [Int]) { + self.versions = versions + } +} + +public extension Version { + static var zero: Self { .init([.zero]) } + + var string: String { + versions.map { String($0) }.joined(separator: ".") + } + + init?(_ string: String) { + let versions = string + .filter { $0.isNumber || $0 == "." } + .split(separator: ".") + .compactMap { Int($0) } + + guard !versions.isEmpty else { return nil } + self.versions = versions + } +} + +extension Version: Comparable { + public static func < (lhs: Self, rhs: Self) -> Bool { + for i in .zero ..< max(lhs.versions.endIndex, rhs.versions.endIndex) { + let left = lhs.versions[safe: i] ?? .zero + let right = rhs.versions[safe: i] ?? .zero + + if left < right { + return true + } else if left > right { + return false + } + } + + return false + } +} + +extension Version: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + for i in .zero ..< max(lhs.versions.endIndex, rhs.versions.endIndex) { + guard lhs.versions[safe: i] ?? .zero == rhs.versions[safe: i] ?? .zero + else { return false } + } + + return true + } +} diff --git a/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift b/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift index 7e310afcb..bd4768254 100644 --- a/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift +++ b/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift @@ -8,6 +8,16 @@ import Foundation +public protocol Localizable { + var localized: String { get } +} + +extension String: Localizable { + public var localized: String { + .localized(self, comment: "") + } +} + public extension String { enum adamant {} @@ -35,3 +45,75 @@ public extension String { return NSLocalizedString(key, bundle: bundle, comment: comment) } } + +public extension String.adamant { + enum shared { + public static var productName: String { + String.localized("ADAMANT", comment: "Product name") + } + } + + enum sharedErrors { + public static var userNotLogged: String { + String.localized("Error.UserNotLogged", comment: "Shared error: User not logged") + } + public static var networkError: String { + String.localized("Error.NoNetwork", comment: "Shared error: Network problems. In most cases - no connection") + } + public static var requestCancelled: String { + String.localized("Error.RequestCancelled", comment: "Shared error: Request cancelled") + } + public static func commonError(_ text: String) -> String { + String.localizedStringWithFormat( + .localized( + "Error.BaseErrorFormat", + comment: "Shared error: Base format, %@" + ), + text + ) + } + + public static func accountNotFound(_ account: String) -> String { + String.localizedStringWithFormat(.localized("Error.AccountNotFoundFormat", comment: "Shared error: Account not found error. Using %@ for address."), account) + } + + public static var accountNotInitiated: String { + String.localized("Error.AccountNotInitiated", comment: "Shared error: Account not initiated") + } + + public static var unknownError: String { + String.localized("Error.UnknownError", comment: "Shared unknown error") + } + public static func admNodeErrorMessage(_ coin: String) -> String { + String.localizedStringWithFormat(.localized("ApiService.InternalError.NoAdmNodesAvailable", comment: "No active ADM nodes to fetch the partner's %@ address"), coin) + } + + public static var notEnoughMoney: String { + String.localized("WalletServices.SharedErrors.notEnoughMoney", comment: "Wallet Services: Shared error, user do not have enought money.") + } + + public static var dustError: String { + String.localized("TransferScene.Dust.Error", comment: "Tranfser: Dust error.") + } + + public static var transactionUnavailable: String { + String.localized("WalletServices.SharedErrors.transactionUnavailable", comment: "Wallet Services: Transaction unavailable") + } + + public static var inconsistentTransaction: String { + String.localized("WalletServices.SharedErrors.inconsistentTransaction", comment: "Wallet Services: Cannot verify transaction") + } + + public static var walletFrezzed: String { + String.localized("WalletServices.SharedErrors.walletFrezzed", comment: "Wallet Services: Wait until other transactions approved") + } + + public static func internalError(message: String) -> String { + String.localizedStringWithFormat(.localized("Error.InternalErrorFormat", comment: "Shared error: Internal error format, %@ for message"), message) + } + + public static func remoteServerError(message: String) -> String { + String.localizedStringWithFormat(.localized("Error.RemoteServerErrorFormat", comment: "Shared error: Remote error format, %@ for message"), message) + } + } +} diff --git a/Adamant/Models/APIParametersEncoding.swift b/CommonKit/Sources/CommonKit/Models/APIParametersEncoding.swift similarity index 86% rename from Adamant/Models/APIParametersEncoding.swift rename to CommonKit/Sources/CommonKit/Models/APIParametersEncoding.swift index 545507ebd..e2e85b2b5 100644 --- a/Adamant/Models/APIParametersEncoding.swift +++ b/CommonKit/Sources/CommonKit/Models/APIParametersEncoding.swift @@ -9,13 +9,13 @@ import Alamofire import Foundation -enum APIParametersEncoding { +public enum APIParametersEncoding { case url case json case bodyString case forceQueryItems([URLQueryItem]) - var parametersEncoding: ParameterEncoding { + public var parametersEncoding: ParameterEncoding { switch self { case .url: return URLEncoding.default diff --git a/CommonKit/Sources/CommonKit/Models/APIResponseModel.swift b/CommonKit/Sources/CommonKit/Models/APIResponseModel.swift new file mode 100644 index 000000000..4663f55dd --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/APIResponseModel.swift @@ -0,0 +1,21 @@ +// +// APIResponseModel.swift +// Adamant +// +// Created by Andrew G on 17.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +public struct APIResponseModel { + public let result: ApiServiceResult + public let data: Data? + public let code: Int? + + public init(result: ApiServiceResult, data: Data?, code: Int?) { + self.result = result + self.data = data + self.code = code + } +} diff --git a/Adamant/Models/AdamantAccount.swift b/CommonKit/Sources/CommonKit/Models/AdamantAccount.swift similarity index 64% rename from Adamant/Models/AdamantAccount.swift rename to CommonKit/Sources/CommonKit/Models/AdamantAccount.swift index 7cacd8c17..d2425351d 100644 --- a/Adamant/Models/AdamantAccount.swift +++ b/CommonKit/Sources/CommonKit/Models/AdamantAccount.swift @@ -7,19 +7,42 @@ // import Foundation -import CommonKit -struct AdamantAccount { - let address: String - var unconfirmedBalance: Decimal - var balance: Decimal - var publicKey: String? - let unconfirmedSignature: Int - let secondSignature: Int - let secondPublicKey: String? - let multisignatures: [String]? - let uMultisignatures: [String]? - var isDummy: Bool +public struct AdamantAccount { + public let address: String + public var unconfirmedBalance: Decimal + public var balance: Decimal + public var publicKey: String? + public let unconfirmedSignature: Int + public let secondSignature: Int + public let secondPublicKey: String? + public let multisignatures: [String]? + public let uMultisignatures: [String]? + public var isDummy: Bool + + public init( + address: String, + unconfirmedBalance: Decimal, + balance: Decimal, + publicKey: String?, + unconfirmedSignature: Int, + secondSignature: Int, + secondPublicKey: String?, + multisignatures: [String]?, + uMultisignatures: [String]?, + isDummy: Bool + ) { + self.address = address + self.unconfirmedBalance = unconfirmedBalance + self.balance = balance + self.publicKey = publicKey + self.unconfirmedSignature = unconfirmedSignature + self.secondSignature = secondSignature + self.secondPublicKey = secondPublicKey + self.multisignatures = multisignatures + self.uMultisignatures = uMultisignatures + self.isDummy = isDummy + } } extension AdamantAccount: Decodable { @@ -35,7 +58,7 @@ extension AdamantAccount: Decodable { case uMultisignatures = "u_multisignatures" } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.address = try container.decode(String.self, forKey: .address) @@ -55,9 +78,9 @@ extension AdamantAccount: Decodable { } extension AdamantAccount: WrappableModel { - static let ModelKey = "account" + public static let ModelKey = "account" - static func makeEmptyAccount(publicKey: String) -> Self { + public static func makeEmptyAccount(publicKey: String) -> Self { .init( address: AdamantUtilities.generateAddress(publicKey: publicKey), unconfirmedBalance: .zero, diff --git a/Adamant/Helpers/AdamantError.swift b/CommonKit/Sources/CommonKit/Models/AdamantError.swift similarity index 76% rename from Adamant/Helpers/AdamantError.swift rename to CommonKit/Sources/CommonKit/Models/AdamantError.swift index 3dade0f6b..e38deb67a 100644 --- a/Adamant/Helpers/AdamantError.swift +++ b/CommonKit/Sources/CommonKit/Models/AdamantError.swift @@ -8,10 +8,10 @@ import Foundation -struct AdamantError: LocalizedError { +public struct AdamantError: LocalizedError { public let errorDescription: String? - init(message: String) { + public init(message: String) { self.errorDescription = message } } diff --git a/Adamant/Models/ApiServiceError.swift b/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift similarity index 54% rename from Adamant/Models/ApiServiceError.swift rename to CommonKit/Sources/CommonKit/Models/ApiServiceError.swift index 156dc1a04..01d9c7ab2 100644 --- a/Adamant/Models/ApiServiceError.swift +++ b/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift @@ -7,9 +7,8 @@ // import Foundation -import CommonKit -enum ApiServiceError: LocalizedError, Error { +public enum ApiServiceError: LocalizedError { case notLogged case accountNotFound case serverError(error: String) @@ -17,9 +16,9 @@ enum ApiServiceError: LocalizedError, Error { case networkError(error: Error) case requestCancelled case commonError(message: String) - case noEndpointsAvailable(coin: String) + case noEndpointsAvailable(nodeGroupName: String) - var errorDescription: String? { + public var errorDescription: String? { switch self { case .notLogged: return String.adamant.sharedErrors.userNotLogged @@ -43,57 +42,24 @@ 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 + case let .noEndpointsAvailable(nodeGroupName): + return .localizedStringWithFormat( + .localized( + "ApiService.InternalError.NoNodesAvailable", + comment: "Serious internal error: No nodes available" + ), + nodeGroupName + ).localized } } - static func internalError(error: InternalAPIError) -> Self { + public static func internalError(error: InternalAPIError) -> Self { .internalError(message: error.localizedDescription, error: error) } } -extension ApiServiceError: RichError { - var message: String { - localizedDescription - } - - var level: ErrorLevel { - switch self { - case .accountNotFound, .notLogged, .networkError, .requestCancelled, .noEndpointsAvailable: - return .warning - - case .serverError, .commonError: - return .error - - case .internalError: - return .internalError - } - } - - var internalError: Error? { - switch self { - case .accountNotFound, .notLogged, .serverError, .requestCancelled, .commonError, .noEndpointsAvailable: - return nil - - case .internalError(_, let error): - return error - - case .networkError(let error): - return error - } - } -} - extension ApiServiceError: Equatable { - static func == (lhs: ApiServiceError, rhs: ApiServiceError) -> Bool { + public static func == (lhs: ApiServiceError, rhs: ApiServiceError) -> Bool { switch (lhs, rhs) { case (.notLogged, .notLogged): return true @@ -117,7 +83,7 @@ extension ApiServiceError: Equatable { } extension ApiServiceError: HealthCheckableError { - var isNetworkError: Bool { + public var isNetworkError: Bool { switch self { case .networkError: return true @@ -126,16 +92,7 @@ extension ApiServiceError: HealthCheckableError { } } - var isRequestCancelledError: Bool { - switch self { - case .requestCancelled: - return true - default: - return false - } - } - - static func noEndpointsError(coin: String) -> ApiServiceError { - .noEndpointsAvailable(coin: coin) + public static func noEndpointsError(nodeGroupName: String) -> ApiServiceError { + .noEndpointsAvailable(nodeGroupName: nodeGroupName) } } diff --git a/Adamant/Models/ApiServiceResult.swift b/CommonKit/Sources/CommonKit/Models/ApiServiceResult.swift similarity index 50% rename from Adamant/Models/ApiServiceResult.swift rename to CommonKit/Sources/CommonKit/Models/ApiServiceResult.swift index d1c88ddb7..10a8d40f3 100644 --- a/Adamant/Models/ApiServiceResult.swift +++ b/CommonKit/Sources/CommonKit/Models/ApiServiceResult.swift @@ -6,5 +6,4 @@ // Copyright © 2022 Adamant. All rights reserved. // -typealias ApiServiceResult = Result -typealias FileApiServiceResult = Result +public typealias ApiServiceResult = Result diff --git a/CommonKit/Sources/CommonKit/Models/BlockchainHealthCheckParams.swift b/CommonKit/Sources/CommonKit/Models/BlockchainHealthCheckParams.swift new file mode 100644 index 000000000..4ddb9d7d0 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/BlockchainHealthCheckParams.swift @@ -0,0 +1,33 @@ +// +// BlockchainHealthCheckParams.swift +// +// +// Created by Andrew G on 08.08.2024. +// + +import Foundation + +public struct BlockchainHealthCheckParams { + public let group: NodeGroup + public let name: String + public let normalUpdateInterval: TimeInterval + public let crucialUpdateInterval: TimeInterval + public let minNodeVersion: Version? + public let nodeHeightEpsilon: Int + + public init( + group: NodeGroup, + name: String, + normalUpdateInterval: TimeInterval, + crucialUpdateInterval: TimeInterval, + minNodeVersion: Version?, + nodeHeightEpsilon: Int + ) { + self.group = group + self.name = name + self.normalUpdateInterval = normalUpdateInterval + self.crucialUpdateInterval = crucialUpdateInterval + self.minNodeVersion = minNodeVersion + self.nodeHeightEpsilon = nodeHeightEpsilon + } +} diff --git a/Adamant/Models/BodyStringEncoding.swift b/CommonKit/Sources/CommonKit/Models/BodyStringEncoding.swift similarity index 92% rename from Adamant/Models/BodyStringEncoding.swift rename to CommonKit/Sources/CommonKit/Models/BodyStringEncoding.swift index 1b7242d61..ece7a7b30 100644 --- a/Adamant/Models/BodyStringEncoding.swift +++ b/CommonKit/Sources/CommonKit/Models/BodyStringEncoding.swift @@ -9,8 +9,8 @@ import Alamofire import Foundation -struct BodyStringEncoding: ParameterEncoding { - func encode( +public struct BodyStringEncoding: ParameterEncoding { + public func encode( _ urlRequest: URLRequestConvertible, with parameters: Parameters? ) throws -> URLRequest { diff --git a/Adamant/Models/ForceQueryItemsEncoding.swift b/CommonKit/Sources/CommonKit/Models/ForceQueryItemsEncoding.swift similarity index 76% rename from Adamant/Models/ForceQueryItemsEncoding.swift rename to CommonKit/Sources/CommonKit/Models/ForceQueryItemsEncoding.swift index 44ae8296b..ccc664acb 100644 --- a/Adamant/Models/ForceQueryItemsEncoding.swift +++ b/CommonKit/Sources/CommonKit/Models/ForceQueryItemsEncoding.swift @@ -9,10 +9,10 @@ import Alamofire import Foundation -struct ForceQueryItemsEncoding: ParameterEncoding { - let queryItems: [URLQueryItem] +public struct ForceQueryItemsEncoding: ParameterEncoding { + public let queryItems: [URLQueryItem] - func encode( + public func encode( _ urlRequest: URLRequestConvertible, with parameters: Parameters? ) throws -> URLRequest { @@ -29,4 +29,8 @@ struct ForceQueryItemsEncoding: ParameterEncoding { urlRequest.url = urlComponents.url return urlRequest } + + public init(queryItems: [URLQueryItem]) { + self.queryItems = queryItems + } } diff --git a/Adamant/Models/InternalAPIError.swift b/CommonKit/Sources/CommonKit/Models/InternalAPIError.swift similarity index 87% rename from Adamant/Models/InternalAPIError.swift rename to CommonKit/Sources/CommonKit/Models/InternalAPIError.swift index c28ad55af..02fc8f75c 100644 --- a/Adamant/Models/InternalAPIError.swift +++ b/CommonKit/Sources/CommonKit/Models/InternalAPIError.swift @@ -6,20 +6,19 @@ // Copyright © 2023 Adamant. All rights reserved. // -import CommonKit import Foundation -enum InternalAPIError: LocalizedError { +public enum InternalAPIError: LocalizedError { case endpointBuildFailed case signTransactionFailed case parsingFailed case unknownError - func apiServiceErrorWith(error: Error) -> ApiServiceError { + public func apiServiceErrorWith(error: Error) -> ApiServiceError { .internalError(message: localizedDescription, error: error) } - var errorDescription: String? { + public var errorDescription: String? { switch self { case .endpointBuildFailed: return .localized( diff --git a/CommonKit/Sources/CommonKit/Models/Keychain/NodesKeychainDTO.swift b/CommonKit/Sources/CommonKit/Models/Keychain/NodesKeychainDTO.swift new file mode 100644 index 000000000..4ddc87d26 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/Keychain/NodesKeychainDTO.swift @@ -0,0 +1,30 @@ +// +// NodesKeychainDTO.swift +// +// +// Created by Andrew G on 05.09.2024. +// + +import Foundation + +struct NodesKeychainDTO: Codable { + let version: String + let data: SafeDecodingDictionary> + + init(_ data: [NodeGroup: [NodeKeychainDTO]]) { + self.version = "1.0.0" + self.data = .init(data.mapValues { .init($0) }) + } +} + +struct NodeKeychainDTO: Codable { + let mainOrigin: NodeOrigin + let altOrigin: NodeOrigin? + let wsEnabled: Bool + let isEnabled: Bool + let version: String? + let height: Int? + let ping: TimeInterval? + let connectionStatus: NodeConnectionStatus? + let type: NodeType +} diff --git a/CommonKit/Sources/CommonKit/Models/Keychain/OldNodeKeychainDTO.swift b/CommonKit/Sources/CommonKit/Models/Keychain/OldNodeKeychainDTO.swift new file mode 100644 index 000000000..968ed269f --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/Keychain/OldNodeKeychainDTO.swift @@ -0,0 +1,122 @@ +// +// OldNodeKeychainDTO.swift +// +// +// Created by Andrew G on 01.08.2024. +// + +import Foundation + +// TODO: delete after a few updates. It's used for migration. +struct OldNodeKeychainDTO: Codable { + let group: NodeGroup + let node: NodeData +} + +extension OldNodeKeychainDTO { + struct NodeData: Codable { + let id: UUID + let scheme: URLScheme + let host: String + let isEnabled: Bool + let wsEnabled: Bool + let port: Int? + let wsPort: Int? + let version: String? + let height: Int? + let ping: TimeInterval? + let connectionStatus: ConnectionStatus? + } +} + +extension OldNodeKeychainDTO.NodeData { + enum RejectedReason: Codable, Equatable { + case outdatedApiVersion + } + + enum ConnectionStatus: Equatable, Codable { + case offline + case synchronizing + case allowed + case notAllowed(RejectedReason) + } + + enum URLScheme: String, Codable { + case http + case https + } + + func mapToModernDto(group: NodeGroup) -> NodeKeychainDTO { + .init( + mainOrigin: .init( + scheme: scheme.map(), + host: host, + port: port, + wsPort: wsPort + ), + altOrigin: nil, + wsEnabled: wsEnabled, + isEnabled: isEnabled, + version: version, + height: height, + ping: ping, + connectionStatus: connectionStatus?.map(), + type: oldDefaultAdmHosts.contains(host) || group != .adm + ? .default(isHidden: false) + : .custom + ) + } +} + +private extension OldNodeKeychainDTO.NodeData.URLScheme { + func map() -> NodeOrigin.URLScheme { + switch self { + case .http: + return .http + case .https: + return .https + } + } +} + +private extension OldNodeKeychainDTO.NodeData.ConnectionStatus { + func map() -> NodeConnectionStatus { + switch self { + case .offline: + return .offline + case .synchronizing: + return .synchronizing + case .allowed: + return .allowed + case let .notAllowed(reason): + return .notAllowed(reason.map()) + } + } +} + +private extension OldNodeKeychainDTO.NodeData.RejectedReason { + func map() -> NodeConnectionStatus.RejectedReason { + switch self { + case .outdatedApiVersion: + return .outdatedApiVersion + } + } +} + +private let oldDefaultAdmHosts: [String] = [ + "clown.adamant.im", + "lake.adamant.im", + "endless.adamant.im", + "bid.adamant.im", + "unusual.adamant.im", + "debate.adamant.im", + "78.47.205.206", + "5.161.53.74", + "184.94.215.92", + "node1.adamant.business", + "node2.blockchain2fa.io", + "phecda.adm.im", + "tegmine.adm.im", + "tauri.adm.im", + "dschubba.adm.im" +] diff --git a/CommonKit/Sources/CommonKit/Models/MultipartFormDataModel.swift b/CommonKit/Sources/CommonKit/Models/MultipartFormDataModel.swift new file mode 100644 index 000000000..e72d1c616 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/MultipartFormDataModel.swift @@ -0,0 +1,21 @@ +// +// MultipartFormDataModel.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 16.05.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +import Foundation + +public struct MultipartFormDataModel { + public let keyName: String + public let fileName: String + public let data: Data + + public init(keyName: String, fileName: String, data: Data) { + self.keyName = keyName + self.fileName = fileName + self.data = data + } +} diff --git a/CommonKit/Sources/CommonKit/Models/Node.swift b/CommonKit/Sources/CommonKit/Models/Node.swift deleted file mode 100644 index 258648786..000000000 --- a/CommonKit/Sources/CommonKit/Models/Node.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// Node.swift -// Adamant -// -// Created by Anokhov Pavel on 20.06.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation - -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, - 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.version = version - self.height = height - self.ping = ping - self.connectionStatus = connectionStatus - } -} - -public extension Node { - enum RejectedReason: Codable, Equatable { - case outdatedApiVersion - - public var text: String { - switch self { - case .outdatedApiVersion: - return Strings.outdated - } - } - } - - enum ConnectionStatus: Equatable, Codable { - case offline - case synchronizing - case allowed - case notAllowed(RejectedReason) - } - - 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 - } - } - } - - 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 - ) - } - - func asString() -> String { - if let url = asURL(forcePort: scheme != .https) { - return url.absoluteString - } else { - return host - } - } - - func asSocketURL() -> URL? { - asURL(forcePort: false, useWsPort: true) - } - - func asURL() -> URL? { - asURL(forcePort: true) - } -} - -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 - } -} - -private extension Node { - enum Strings { - static var outdated: String { - String.localized( - "NodesList.NodeCell.Outdated", - comment: "NodesList.NodeCell: Node is outdated" - ) - } - } -} diff --git a/CommonKit/Sources/CommonKit/Models/Node/Node.swift b/CommonKit/Sources/CommonKit/Models/Node/Node.swift new file mode 100644 index 000000000..672ce6387 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/Node/Node.swift @@ -0,0 +1,92 @@ +// +// Node.swift +// Adamant +// +// Created by Anokhov Pavel on 20.06.2018. +// Copyright © 2018 Adamant. All rights reserved. +// +// Check AdamantNodesMergingService when the structure is changed +// + +import Foundation + +public struct Node: Equatable, Identifiable { + public let id: UUID + public var mainOrigin: NodeOrigin + public var altOrigin: NodeOrigin? + public var wsEnabled: Bool + public var version: Version? + public var height: Int? + public var ping: TimeInterval? + public var connectionStatus: NodeConnectionStatus? + public var preferMainOrigin: Bool? + public var isEnabled: Bool + public var type: NodeType + + public init( + id: UUID, + isEnabled: Bool, + wsEnabled: Bool, + mainOrigin: NodeOrigin, + altOrigin: NodeOrigin?, + version: Version?, + height: Int?, + ping: TimeInterval?, + connectionStatus: NodeConnectionStatus?, + preferMainOrigin: Bool?, + type: NodeType + ) { + self.id = id + self.mainOrigin = mainOrigin + self.altOrigin = altOrigin + self.isEnabled = isEnabled + self.wsEnabled = wsEnabled + self.version = version + self.height = height + self.ping = ping + self.connectionStatus = connectionStatus + self.preferMainOrigin = preferMainOrigin + self.type = type + } +} + +public extension Node { + var preferredOrigin: NodeOrigin { + preferMainOrigin ?? true + ? mainOrigin + : altOrigin ?? mainOrigin + } + + static func makeDefaultNode(url: URL, altUrl: URL? = nil) -> Self { + .init( + id: .init(), + isEnabled: true, + wsEnabled: false, + mainOrigin: .init(url: url), + altOrigin: altUrl.map { .init(url: $0) }, + version: nil, + height: nil, + ping: nil, + connectionStatus: nil, + preferMainOrigin: nil, + type: .default(isHidden: false) + ) + } + + func asSocketURL() -> URL? { + preferredOrigin.asSocketURL() + } + + func asURL() -> URL? { + preferredOrigin.asURL() + } + + func isSame(_ node: Node) -> Bool { + mainOrigin.host == node.mainOrigin.host + } + + mutating func updateWsPort(_ wsPort: Int?) { + mainOrigin.wsPort = wsPort + altOrigin?.wsPort = wsPort + } +} diff --git a/CommonKit/Sources/CommonKit/Models/Node/NodeConnectionStatus.swift b/CommonKit/Sources/CommonKit/Models/Node/NodeConnectionStatus.swift new file mode 100644 index 000000000..5e0fcad7c --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/Node/NodeConnectionStatus.swift @@ -0,0 +1,33 @@ +// +// NodeConnectionStatus.swift +// +// +// Created by Andrew G on 28.07.2024. +// + +import Foundation + +public enum NodeConnectionStatus: Equatable, Codable { + case offline + case synchronizing + case allowed + case notAllowed(RejectedReason) +} + +public extension NodeConnectionStatus { + enum RejectedReason: Codable, Equatable { + case outdatedApiVersion + } +} + +public extension NodeConnectionStatus.RejectedReason { + var text: String { + switch self { + case .outdatedApiVersion: + return String.localized( + "NodesList.NodeCell.Outdated", + comment: "NodesList.NodeCell: Node is outdated" + ) + } + } +} diff --git a/CommonKit/Sources/CommonKit/Models/NodeGroup.swift b/CommonKit/Sources/CommonKit/Models/Node/NodeGroup.swift similarity index 92% rename from CommonKit/Sources/CommonKit/Models/NodeGroup.swift rename to CommonKit/Sources/CommonKit/Models/Node/NodeGroup.swift index 8d3fe5dae..6435e6e90 100644 --- a/CommonKit/Sources/CommonKit/Models/NodeGroup.swift +++ b/CommonKit/Sources/CommonKit/Models/Node/NodeGroup.swift @@ -14,4 +14,5 @@ public enum NodeGroup: Codable, CaseIterable, Hashable { case dash case adm case ipfs + case infoService } diff --git a/CommonKit/Sources/CommonKit/Models/Node/NodeOrigin.swift b/CommonKit/Sources/CommonKit/Models/Node/NodeOrigin.swift new file mode 100644 index 000000000..b59447547 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/Node/NodeOrigin.swift @@ -0,0 +1,84 @@ +// +// NodeOrigin.swift +// +// +// Created by Andrew G on 27.07.2024. +// + +import Foundation + +public struct NodeOrigin: Codable, Equatable, Hashable { + public var scheme: URLScheme + public var host: String + public var port: Int? + public var wsPort: Int? + + public init( + scheme: URLScheme, + host: String, + port: Int? = nil, + wsPort: Int? = nil + ) { + self.scheme = scheme + self.host = host + self.port = port + self.wsPort = wsPort + } +} + +public extension NodeOrigin { + 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 + } + } + } + + init(url: URL) { + self.init( + scheme: URLScheme(rawValue: url.scheme ?? .empty) ?? .https, + host: url.host ?? .empty, + port: url.port + ) + } + + func asString() -> String { + if let url = asURL(forcePort: scheme != .https) { + return url.absoluteString + } else { + return host + } + } + + func asSocketURL() -> URL? { + asURL(forcePort: false, useWsPort: true) + } + + func asURL() -> URL? { + asURL(forcePort: true) + } +} + +private extension NodeOrigin { + 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 + } +} diff --git a/CommonKit/Sources/CommonKit/Models/Node/NodeStatusInfo.swift b/CommonKit/Sources/CommonKit/Models/Node/NodeStatusInfo.swift new file mode 100644 index 000000000..a661d5009 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/Node/NodeStatusInfo.swift @@ -0,0 +1,31 @@ +// +// NodeStatusInfo.swift +// Adamant +// +// Created by Andrew G on 01.11.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +public struct NodeStatusInfo: Equatable { + public let ping: TimeInterval + public let height: Int + public let wsEnabled: Bool + public let wsPort: Int? + public let version: Version? + + public init( + ping: TimeInterval, + height: Int, + wsEnabled: Bool, + wsPort: Int?, + version: Version? + ) { + self.ping = ping + self.height = height + self.wsEnabled = wsEnabled + self.wsPort = wsPort + self.version = version + } +} diff --git a/CommonKit/Sources/CommonKit/Models/Node/NodeType.swift b/CommonKit/Sources/CommonKit/Models/Node/NodeType.swift new file mode 100644 index 000000000..c809b33a1 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/Node/NodeType.swift @@ -0,0 +1,11 @@ +// +// NodeType.swift +// +// +// Created by Andrew G on 01.08.2024. +// + +public enum NodeType: Codable, Equatable { + case custom + case `default`(isHidden: Bool) +} diff --git a/CommonKit/Sources/CommonKit/Models/ChatAsset.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatAsset.swift similarity index 93% rename from CommonKit/Sources/CommonKit/Models/ChatAsset.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatAsset.swift index 578f22c89..990131292 100644 --- a/CommonKit/Sources/CommonKit/Models/ChatAsset.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatAsset.swift @@ -8,7 +8,7 @@ import Foundation -public struct ChatAsset: Codable, Hashable { +public struct ChatAsset: Codable, Hashable, Sendable { public enum CodingKeys: String, CodingKey { case message, ownMessage = "own_message", type } diff --git a/CommonKit/Sources/CommonKit/Models/ChatRooms.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRooms.swift similarity index 100% rename from CommonKit/Sources/CommonKit/Models/ChatRooms.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRooms.swift diff --git a/CommonKit/Sources/CommonKit/Models/ChatRoomsChats.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRoomsChats.swift similarity index 100% rename from CommonKit/Sources/CommonKit/Models/ChatRoomsChats.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRoomsChats.swift diff --git a/CommonKit/Sources/CommonKit/Models/ChatType.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatType.swift similarity index 97% rename from CommonKit/Sources/CommonKit/Models/ChatType.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatType.swift index 392674225..410abddf7 100644 --- a/CommonKit/Sources/CommonKit/Models/ChatType.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatType.swift @@ -12,7 +12,7 @@ import Foundation /// - message: new and main message type, with 0.001 transaction fee /// - richMessage: json with additional data /// - signal: hidden system message for/from services -public enum ChatType: Hashable { +public enum ChatType: Hashable, Sendable { case unknown(raw: Int) case messageOld // 0 case message // 1 diff --git a/CommonKit/Sources/CommonKit/Models/ContactDescription.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ContactDescription.swift similarity index 100% rename from CommonKit/Sources/CommonKit/Models/ContactDescription.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/ContactDescription.swift diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/Delegate.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/Delegate.swift new file mode 100644 index 000000000..dd859b943 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/Delegate.swift @@ -0,0 +1,184 @@ +// +// Delegate.swift +// Adamant +// +// Created by Anton Boyarkin on 06/07/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +public final class Delegate: Decodable { + public let username: String + public let address: String + public let publicKey: String + public let voteObsolete: String + public let voteFair: String + public let producedblocks: Int + public let missedblocks: Int + public let rate: Int + public let rank: Int + public let approval: Double + public let productivity: Double + + public var voted: Bool = false + + public enum CodingKeys: String, CodingKey { + case username + case address + case publicKey + case voteObsolete = "vote" + case voteFair = "votesWeight" + case producedblocks + case missedblocks + case rate + case rank + case approval + case productivity + } + + public init( + username: String, + address: String, + publicKey: String, + voteObsolete: String, + voteFair: String, + producedblocks: Int, + missedblocks: Int, + rate: Int, + rank: Int, + approval: Double, + productivity: Double, + voted: Bool + ) { + self.username = username + self.address = address + self.publicKey = publicKey + self.voteObsolete = voteObsolete + self.voteFair = voteFair + self.producedblocks = producedblocks + self.missedblocks = missedblocks + self.rate = rate + self.rank = rank + self.approval = approval + self.productivity = productivity + self.voted = voted + } +} + +extension Delegate: WrappableModel { + public static let ModelKey = "delegate" +} + +extension Delegate: WrappableCollection { + public static let CollectionKey = "delegates" +} + +public struct DelegateForgeDetails: Decodable { + public let nodeTimestamp: Date + public let fees: Decimal + public let rewards: Decimal + public let forged: Decimal + + public enum CodingKeys: String, CodingKey { + case nodeTimestamp + case fees + case rewards + case forged + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let feesStr = try container.decode(String.self, forKey: .fees) + let fees = Decimal(string: feesStr) ?? 0 + self.fees = fees.shiftedFromAdamant() + + let rewardsStr = try container.decode(String.self, forKey: .forged) + let rewards = Decimal(string: rewardsStr) ?? 0 + self.rewards = rewards.shiftedFromAdamant() + + let forgedStr = try container.decode(String.self, forKey: .forged) + let forged = Decimal(string: forgedStr) ?? 0 + self.forged = forged.shiftedFromAdamant() + + let timestamp = try container.decode(UInt64.self, forKey: .nodeTimestamp) + self.nodeTimestamp = AdamantUtilities.decodeAdamant(timestamp: TimeInterval(timestamp)) + } +} + +public struct DelegatesCountResult: Decodable { + public let nodeTimestamp: UInt64 + public let count: UInt +} + +public struct NextForgersResult: Decodable { + public let nodeTimestamp: Date + public let currentBlock: UInt64 + public let currentBlockSlot: UInt64 + public let currentSlot: UInt64 + public let delegates: [String] + + public enum CodingKeys: String, CodingKey { + case nodeTimestamp + case currentBlock + case currentBlockSlot + case currentSlot + case delegates + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.currentBlock = try container.decode(UInt64.self, forKey: .currentBlock) + self.currentBlockSlot = try container.decode(UInt64.self, forKey: .currentBlockSlot) + self.currentSlot = try container.decode(UInt64.self, forKey: .currentSlot) + self.delegates = try container.decode([String].self, forKey: .delegates) + + let timestamp = try container.decode(UInt64.self, forKey: .nodeTimestamp) + self.nodeTimestamp = AdamantUtilities.decodeAdamant(timestamp: TimeInterval(timestamp)) + } +} + +public struct Block: Decodable { + public let id: String + public let version: UInt + public let timestamp: UInt64 + public let height: UInt64 + public let previousBlock:String + public let numberOfTransactions: UInt + public let totalAmount: UInt + public let totalFee: UInt + public let reward: UInt + public let payloadLength: UInt + public let payloadHash: String + public let generatorPublicKey: String + public let generatorId: String + public let blockSignature: String + public let confirmations: UInt + public let totalForged: String +} + +extension Block: WrappableModel { + public static let ModelKey = "block" +} + +extension Block: WrappableCollection { + public static let CollectionKey = "blocks" +} + +/* +{ + "username": "permit", + "address": "U8339394976025567725", + "publicKey": "01c5079a2234f69feca1b00daf4ddbd8904e13dfb67ce47c21f26377468706fa", + "producedblocks": 11153, + "missedblocks": 3, + "vote": "13373617430543", + "votesWeight": "13373617430543", + "rate": 1, + "rank": 1, + "approval": 0.95, + "productivity": 99.97 +} +*/ diff --git a/Adamant/Models/ServerResponses/GetPublicKeyResponse.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/GetPublicKeyResponse.swift similarity index 84% rename from Adamant/Models/ServerResponses/GetPublicKeyResponse.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/GetPublicKeyResponse.swift index f86821ca3..711b1599e 100644 --- a/Adamant/Models/ServerResponses/GetPublicKeyResponse.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/GetPublicKeyResponse.swift @@ -7,12 +7,11 @@ // import Foundation -import CommonKit -final class GetPublicKeyResponse: ServerResponse { - let publicKey: String? +public final class GetPublicKeyResponse: ServerResponse { + public let publicKey: String? - required init(from decoder: Decoder) throws { + public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let success = try container.decode(Bool.self, forKey: .success) let error = try? container.decode(String.self, forKey: .error) diff --git a/Adamant/Models/NodeStatus.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/NodeStatus.swift similarity index 52% rename from Adamant/Models/NodeStatus.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/NodeStatus.swift index 9920bb7bd..7c26b3cfc 100644 --- a/Adamant/Models/NodeStatus.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/NodeStatus.swift @@ -8,34 +8,34 @@ import Foundation -struct NodeStatus: Codable { - struct Network: Codable { - let broadhash: String? - let epoch: String? - let height: Int? - let fee: Int? - let milestone: Int? - let nethash: String? - let reward: Int? - let supply: Int? +public struct NodeStatus: Codable { + public struct Network: Codable { + public let broadhash: String? + public let epoch: String? + public let height: Int? + public let fee: Int? + public let milestone: Int? + public let nethash: String? + public let reward: Int? + public let supply: Int? } - struct Version: Codable { - let build: String? - let commit: String? - let version: String? + public struct Version: Codable { + public let build: String? + public let commit: String? + public let version: String? } - struct WsClient: Codable { - let enabled: Bool? - let port: Int? + public struct WsClient: Codable { + public let enabled: Bool? + public let port: Int? } - let success: Bool - let nodeTimestamp: TimeInterval - let network: Network? - let version: Version? - let wsClient: WsClient? + public let success: Bool + public let nodeTimestamp: TimeInterval + public let network: Network? + public let version: Version? + public let wsClient: WsClient? } /* JSON diff --git a/Adamant/Models/NormalizedTransaction.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/NormalizedTransaction.swift similarity index 62% rename from Adamant/Models/NormalizedTransaction.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/NormalizedTransaction.swift index 04ccfbc29..5bd7331e6 100644 --- a/Adamant/Models/NormalizedTransaction.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/NormalizedTransaction.swift @@ -7,25 +7,45 @@ // import Foundation -import CommonKit -struct NormalizedTransaction: SignableTransaction { - let type: TransactionType - let amount: Decimal - let senderPublicKey: String - let requesterPublicKey: String? - let timestamp: UInt64 - let recipientId: String? - let asset: TransactionAsset +public struct NormalizedTransaction: SignableTransaction { + public let type: TransactionType + public let amount: Decimal + public let senderPublicKey: String + public let requesterPublicKey: String? + public let timestamp: UInt64 + public let recipientId: String? + public let asset: TransactionAsset - var date: Date { - return AdamantUtilities.decodeAdamant(timestamp: TimeInterval(timestamp)) + init( + type: TransactionType, + amount: Decimal, + senderPublicKey: String, + requesterPublicKey: String?, + timestamp: UInt64, + recipientId: String?, + asset: TransactionAsset + ) { + self.type = type + self.amount = amount + self.senderPublicKey = senderPublicKey + self.requesterPublicKey = requesterPublicKey + self.timestamp = timestamp + self.recipientId = recipientId + self.asset = asset } } -// Convinient init -extension NormalizedTransaction { - init(type: TransactionType, amount: Decimal, senderPublicKey: String, requesterPublicKey: String?, date: Date, recipientId: String?, asset: TransactionAsset) { +public extension NormalizedTransaction { + init( + type: TransactionType, + amount: Decimal, + senderPublicKey: String, + requesterPublicKey: String?, + date: Date, + recipientId: String?, + asset: TransactionAsset + ) { self.type = type self.amount = amount self.senderPublicKey = senderPublicKey @@ -34,6 +54,10 @@ extension NormalizedTransaction { self.recipientId = recipientId self.asset = asset } + + var date: Date { + return AdamantUtilities.decodeAdamant(timestamp: TimeInterval(timestamp)) + } } extension NormalizedTransaction: Decodable { @@ -47,7 +71,7 @@ extension NormalizedTransaction: Decodable { case asset } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.type = try container.decode(TransactionType.self, forKey: .type) @@ -63,7 +87,7 @@ extension NormalizedTransaction: Decodable { } extension NormalizedTransaction: WrappableModel { - static let ModelKey = "transaction" + public static let ModelKey = "transaction" } // MARK: - JSON diff --git a/Adamant/Models/RPCResponseModel.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/RPCResponseModel.swift similarity index 72% rename from Adamant/Models/RPCResponseModel.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/RPCResponseModel.swift index 4baf1c05c..b3ccd7e2f 100644 --- a/Adamant/Models/RPCResponseModel.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/RPCResponseModel.swift @@ -8,22 +8,22 @@ import Foundation -struct RPCResponseModel: Codable { - let id: String - let result: Data +public struct RPCResponseModel: Codable { + public let id: String + public let result: Data private enum CodingKeys: String, CodingKey { case id case result } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) result = try container.decode(forKey: .result) } - func serialize() -> Response? { + public func serialize() -> Response? { try? JSONDecoder().decode(Response.self, from: result) } } diff --git a/CommonKit/Sources/CommonKit/Models/RichMessage.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/RichMessage.swift similarity index 100% rename from CommonKit/Sources/CommonKit/Models/RichMessage.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/RichMessage.swift diff --git a/Adamant/Models/RpcRequestModel.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/RpcRequestModel.swift similarity index 66% rename from Adamant/Models/RpcRequestModel.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/RpcRequestModel.swift index d0b4d18c8..c0fd9cc3d 100644 --- a/Adamant/Models/RpcRequestModel.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/RpcRequestModel.swift @@ -8,32 +8,32 @@ import Foundation -struct RpcRequest: Encodable { - let method: String - let id: String - let params: [Parameter] - let jsonrpc: String = "2.0" +public struct RpcRequest: Encodable { + public let method: String + public let id: String + public let params: [Parameter] + public let jsonrpc: String = "2.0" - init(method: String, id: String, params: [Parameter]) { + public init(method: String, id: String, params: [Parameter]) { self.method = method self.id = id self.params = params } - init(method: String, params: [Parameter]) { + public init(method: String, params: [Parameter]) { self.method = method self.id = method self.params = params } - init(method: String) { + public init(method: String) { self.method = method self.id = method self.params = [] } } -extension RpcRequest { +public extension RpcRequest { enum Parameter { case string(String) case bool(Bool) @@ -41,7 +41,7 @@ extension RpcRequest { } extension RpcRequest.Parameter: Encodable { - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { diff --git a/CommonKit/Sources/CommonKit/Models/ServerResponse.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ServerResponse.swift similarity index 100% rename from CommonKit/Sources/CommonKit/Models/ServerResponse.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/ServerResponse.swift diff --git a/CommonKit/Sources/CommonKit/Models/StateAsset.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateAsset.swift similarity index 89% rename from CommonKit/Sources/CommonKit/Models/StateAsset.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/StateAsset.swift index 098049634..aeb9b5131 100644 --- a/CommonKit/Sources/CommonKit/Models/StateAsset.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateAsset.swift @@ -8,7 +8,7 @@ import Foundation -public struct StateAsset: Codable, Hashable { +public struct StateAsset: Codable, Hashable, Sendable { public let key: String public let value: String public let type: StateType diff --git a/CommonKit/Sources/CommonKit/Models/StateType.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateType.swift similarity index 94% rename from CommonKit/Sources/CommonKit/Models/StateType.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/StateType.swift index 104462171..fd8d46470 100644 --- a/CommonKit/Sources/CommonKit/Models/StateType.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateType.swift @@ -8,7 +8,7 @@ import Foundation -public enum StateType: Equatable, Hashable { +public enum StateType: Equatable, Hashable, Sendable { case unknown(raw: Int) case keyValue // 0 diff --git a/CommonKit/Sources/CommonKit/Models/Transaction.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/Transaction.swift similarity index 99% rename from CommonKit/Sources/CommonKit/Models/Transaction.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/Transaction.swift index f584a1f77..2a19cc909 100644 --- a/CommonKit/Sources/CommonKit/Models/Transaction.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/Transaction.swift @@ -8,7 +8,7 @@ import Foundation -public struct Transaction { +public struct Transaction: Sendable { public let id: UInt64 public let height: Int64 public let blockId: String diff --git a/CommonKit/Sources/CommonKit/Models/TransactionAsset.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionAsset.swift similarity index 87% rename from CommonKit/Sources/CommonKit/Models/TransactionAsset.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionAsset.swift index 8b54df965..45e155158 100644 --- a/CommonKit/Sources/CommonKit/Models/TransactionAsset.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionAsset.swift @@ -8,7 +8,7 @@ import Foundation -public struct TransactionAsset: Codable, Hashable { +public struct TransactionAsset: Codable, Hashable, Sendable { public let chat: ChatAsset? public let state: StateAsset? public let votes: VotesAsset? diff --git a/Adamant/Models/ServerResponses/TransactionIdResponse.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionIdResponse.swift similarity index 85% rename from Adamant/Models/ServerResponses/TransactionIdResponse.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionIdResponse.swift index 158a09337..49ca1d0ca 100644 --- a/Adamant/Models/ServerResponses/TransactionIdResponse.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionIdResponse.swift @@ -7,12 +7,11 @@ // import Foundation -import CommonKit -final class TransactionIdResponse: ServerResponse { - let transactionId: UInt64? +public final class TransactionIdResponse: ServerResponse { + public let transactionId: UInt64? - required init(from decoder: Decoder) throws { + public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let success = try container.decode(Bool.self, forKey: .success) let error = try? container.decode(String.self, forKey: .error) diff --git a/CommonKit/Sources/CommonKit/Models/TransactionType.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionType.swift similarity index 97% rename from CommonKit/Sources/CommonKit/Models/TransactionType.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionType.swift index 74b69e3f9..87eb8b369 100644 --- a/CommonKit/Sources/CommonKit/Models/TransactionType.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionType.swift @@ -8,7 +8,7 @@ import Foundation -public enum TransactionType: Hashable { +public enum TransactionType: Hashable, Sendable { case unknown(raw: Int) case send // 0 case signature // 1 diff --git a/Adamant/Models/UnregisteredTransaction.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/UnregisteredTransaction.swift similarity index 64% rename from Adamant/Models/UnregisteredTransaction.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/UnregisteredTransaction.swift index e23543153..32bcb1e2a 100644 --- a/Adamant/Models/UnregisteredTransaction.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/UnregisteredTransaction.swift @@ -7,19 +7,40 @@ // import Foundation -import CommonKit import BigInt -struct UnregisteredTransaction: Hashable { - let type: TransactionType - let timestamp: UInt64 - let senderPublicKey: String - let senderId: String - let recipientId: String? - let amount: Decimal - let signature: String - let asset: TransactionAsset - let requesterPublicKey: String? +public struct UnregisteredTransaction: Hashable { + public let type: TransactionType + public let timestamp: UInt64 + public let senderPublicKey: String + public let senderId: String + public let recipientId: String? + public let amount: Decimal + public let signature: String + public let asset: TransactionAsset + public let requesterPublicKey: String? + + public init( + type: TransactionType, + timestamp: UInt64, + senderPublicKey: String, + senderId: String, + recipientId: String?, + amount: Decimal, + signature: String, + asset: TransactionAsset, + requesterPublicKey: String? + ) { + self.type = type + self.timestamp = timestamp + self.senderPublicKey = senderPublicKey + self.senderId = senderId + self.recipientId = recipientId + self.amount = amount + self.signature = signature + self.asset = asset + self.requesterPublicKey = requesterPublicKey + } } extension UnregisteredTransaction: Codable { @@ -34,7 +55,7 @@ extension UnregisteredTransaction: Codable { case asset } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.type = try container.decode(TransactionType.self, forKey: .type) @@ -50,7 +71,7 @@ extension UnregisteredTransaction: Codable { self.requesterPublicKey = "" } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type, forKey: .type) // TransactionType diff --git a/CommonKit/Sources/CommonKit/Models/VotesAsset.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/VotesAsset.swift similarity index 93% rename from CommonKit/Sources/CommonKit/Models/VotesAsset.swift rename to CommonKit/Sources/CommonKit/Models/ServerDTOs/VotesAsset.swift index ff311145b..b31b8d4bd 100644 --- a/CommonKit/Sources/CommonKit/Models/VotesAsset.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/VotesAsset.swift @@ -8,7 +8,7 @@ import Foundation -public struct VotesAsset: Hashable { +public struct VotesAsset: Hashable, Sendable { public let votes: [String] public init(votes: [String]) { diff --git a/CommonKit/Sources/CommonKit/Models/ethereumTokensList.swift b/CommonKit/Sources/CommonKit/Models/ethereumTokensList.swift index ba9b9f614..defd1f647 100644 --- a/CommonKit/Sources/CommonKit/Models/ethereumTokensList.swift +++ b/CommonKit/Sources/CommonKit/Models/ethereumTokensList.swift @@ -12,9 +12,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "BUSD", name: "Binance USD", @@ -25,9 +25,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "BZZ", name: "Swarm", @@ -38,9 +38,9 @@ defaultOrdinalLevel: 95, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "DAI", name: "Dai", @@ -51,9 +51,9 @@ defaultOrdinalLevel: 80, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "ENS", name: "Ethereum Name Service", @@ -64,9 +64,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "FLOKI", name: "Floki", @@ -77,9 +77,9 @@ defaultOrdinalLevel: 100, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "FLUX", name: "Flux", @@ -90,9 +90,9 @@ defaultOrdinalLevel: 90, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "GT", name: "Gate", @@ -103,9 +103,9 @@ defaultOrdinalLevel: 115, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "HOT", name: "Holo", @@ -116,9 +116,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "INJ", name: "Injective", @@ -129,9 +129,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "LINK", name: "Chainlink", @@ -142,9 +142,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "MANA", name: "Decentraland", @@ -155,9 +155,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "MATIC", name: "Polygon", @@ -168,9 +168,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "PAXG", name: "PAX Gold", @@ -181,9 +181,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "QNT", name: "Quant", @@ -194,9 +194,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "REN", name: "Ren", @@ -207,9 +207,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "SKL", name: "SKALE", @@ -220,9 +220,9 @@ defaultOrdinalLevel: 85, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "SNT", name: "Status", @@ -233,9 +233,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "SNX", name: "Synthetix Network", @@ -246,9 +246,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "STORJ", name: "Storj", @@ -259,9 +259,9 @@ defaultOrdinalLevel: 105, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "TUSD", name: "TrueUSD", @@ -272,9 +272,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "UNI", name: "Uniswap", @@ -285,9 +285,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "USDC", name: "USD Coin", @@ -298,9 +298,9 @@ defaultOrdinalLevel: 40, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "USDP", name: "PAX Dollar", @@ -311,9 +311,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "USDS", name: "Stably USD", @@ -324,9 +324,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "USDT", name: "Tether", @@ -337,9 +337,9 @@ defaultOrdinalLevel: 30, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "VERSE", name: "Verse", @@ -350,9 +350,9 @@ defaultOrdinalLevel: 95, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "WOO", name: "WOO Network", @@ -363,9 +363,9 @@ defaultOrdinalLevel: nil, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ERC20Token(symbol: "XCN", name: "Onyxcoin", @@ -376,9 +376,9 @@ defaultOrdinalLevel: 110, reliabilityGasPricePercent: 10, reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 30, + defaultGasPriceGwei: 10, defaultGasLimit: 58000, - warningGasPriceGwei: 70, + warningGasPriceGwei: 25, transferDecimals: 6), ] diff --git a/Adamant/ServiceProtocols/APICoreProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/APICoreProtocol.swift similarity index 79% rename from Adamant/ServiceProtocols/APICoreProtocol.swift rename to CommonKit/Sources/CommonKit/Protocols/APICoreProtocol.swift index 1a5e8c28a..fc67a2941 100644 --- a/Adamant/ServiceProtocols/APICoreProtocol.swift +++ b/CommonKit/Sources/CommonKit/Protocols/APICoreProtocol.swift @@ -8,102 +8,93 @@ import Foundation import Alamofire -import CommonKit -import UIKit -enum ApiCommands {} +public enum ApiCommands {} -protocol APICoreProtocol: Actor { +public enum TimeoutSize: CaseIterable, Hashable { + case common + case extended +} + +public protocol APICoreProtocol: Actor { func sendRequestMultipartFormData( - node: Node, + origin: NodeOrigin, path: String, models: [MultipartFormDataModel], + timeout: TimeoutSize, uploadProgress: @escaping ((Progress) -> Void) ) async -> APIResponseModel func sendRequestBasic( - node: Node, + origin: NodeOrigin, path: String, method: HTTPMethod, parameters: Parameters, encoding: APIParametersEncoding, + timeout: TimeoutSize, downloadProgress: @escaping ((Progress) -> Void) ) async -> APIResponseModel /// jsonParameters - arrays and dictionaries are allowed only func sendRequestBasic( - node: Node, + origin: NodeOrigin, path: String, method: HTTPMethod, - jsonParameters: Any + jsonParameters: Any, + timeout: TimeoutSize ) async -> APIResponseModel } -extension APICoreProtocol { +public extension APICoreProtocol { var emptyParameters: [String: Bool] { [:] } func sendRequest( - node: Node, + origin: NodeOrigin, path: String, method: HTTPMethod, parameters: Parameters, encoding: APIParametersEncoding ) async -> ApiServiceResult { await sendRequestBasic( - node: node, - path: path, - method: method, - parameters: parameters, - encoding: encoding, - downloadProgress: { _ in } - ).result - } - - func sendRequest( - node: Node, - path: String, - method: HTTPMethod, - parameters: Parameters, - encoding: APIParametersEncoding, - downloadProgress: @escaping ((Progress) -> Void) - ) async -> ApiServiceResult { - await sendRequestBasic( - node: node, + origin: origin, path: path, method: method, parameters: parameters, encoding: encoding, - downloadProgress: downloadProgress + timeout: .common, + downloadProgress: { _ in } ).result } func sendRequest( - node: Node, + origin: NodeOrigin, path: String, method: HTTPMethod, parameters: Parameters, encoding: APIParametersEncoding, + timeout: TimeoutSize, downloadProgress: @escaping ((Progress) -> Void) ) async -> APIResponseModel { await sendRequestBasic( - node: node, + origin: origin, path: path, method: method, parameters: parameters, encoding: encoding, + timeout: timeout, downloadProgress: downloadProgress ) } func sendRequestJsonResponse( - node: Node, + origin: NodeOrigin, path: String, method: HTTPMethod, parameters: Parameters, encoding: APIParametersEncoding ) async -> ApiServiceResult { await sendRequest( - node: node, + origin: origin, path: path, method: method, parameters: parameters, @@ -112,11 +103,11 @@ extension APICoreProtocol { } func sendRequestJsonResponse( - node: Node, + origin: NodeOrigin, path: String ) async -> ApiServiceResult { await sendRequestJsonResponse( - node: node, + origin: origin, path: path, method: .get, parameters: emptyParameters, @@ -125,11 +116,11 @@ extension APICoreProtocol { } func sendRequest( - node: Node, + origin: NodeOrigin, path: String ) async -> ApiServiceResult { await sendRequest( - node: node, + origin: origin, path: path, method: .get, parameters: emptyParameters, @@ -138,65 +129,72 @@ extension APICoreProtocol { } func sendRequest( - node: Node, + origin: NodeOrigin, path: String, + timeout: TimeoutSize, downloadProgress: @escaping ((Progress) -> Void) ) async -> ApiServiceResult { await sendRequest( - node: node, + origin: origin, path: path, method: .get, parameters: emptyParameters, encoding: .url, + timeout: timeout, downloadProgress: downloadProgress - ) + ).result } func sendRequest( - node: Node, + origin: NodeOrigin, path: String, + timeout: TimeoutSize, downloadProgress: @escaping ((Progress) -> Void) ) async -> APIResponseModel { await sendRequest( - node: node, + origin: origin, path: path, method: .get, parameters: emptyParameters, encoding: .url, + timeout: timeout, downloadProgress: downloadProgress ) } func sendRequestJsonResponse( - node: Node, + origin: NodeOrigin, path: String, method: HTTPMethod, jsonParameters: Any ) async -> ApiServiceResult { await sendRequestBasic( - node: node, + origin: origin, path: path, method: method, - jsonParameters: jsonParameters + jsonParameters: jsonParameters, + timeout: .common ).result.flatMap { parseJSON(data: $0) } } func sendRequestMultipartFormDataJsonResponse( - node: Node, + origin: NodeOrigin, path: String, models: [MultipartFormDataModel], + timeout: TimeoutSize, uploadProgress: @escaping ((Progress) -> Void) ) async -> ApiServiceResult { await sendRequestMultipartFormData( - node: node, + origin: origin, path: path, models: models, + timeout: timeout, uploadProgress: uploadProgress ).result.flatMap { parseJSON(data: $0) } } func sendRequestRPC( - node: Node, + origin: NodeOrigin, path: String, requests: [RpcRequest] ) async -> ApiServiceResult<[RPCResponseModel]> { @@ -205,7 +203,7 @@ extension APICoreProtocol { } return await sendRequestJsonResponse( - node: node, + origin: origin, path: path, method: .post, jsonParameters: parameters @@ -213,12 +211,12 @@ extension APICoreProtocol { } func sendRequestRPC( - node: Node, + origin: NodeOrigin, path: String, request: RpcRequest ) async -> ApiServiceResult { await sendRequestJsonResponse( - node: node, + origin: origin, path: path, method: .post, jsonParameters: request.asDictionary() ?? [:] diff --git a/Adamant/ServiceProtocols/ApiService.swift b/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift similarity index 96% rename from Adamant/ServiceProtocols/ApiService.swift rename to CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift index 343aab1b3..3d443f38e 100644 --- a/Adamant/ServiceProtocols/ApiService.swift +++ b/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift @@ -8,9 +8,8 @@ import Foundation import Alamofire -import CommonKit -protocol ApiService: WalletApiService { +public protocol AdamantApiServiceProtocol: ApiServiceProtocol { // MARK: - Accounts func getAccount(byPassphrase passphrase: String) async -> ApiServiceResult func getAccount(byPublicKey publicKey: String) async -> ApiServiceResult @@ -46,7 +45,8 @@ protocol ApiService: WalletApiService { func getChatRooms( address: String, - offset: Int? + offset: Int?, + waitsForConnectivity: Bool ) async -> ApiServiceResult func getChatMessages( diff --git a/Adamant/ServiceProtocols/AdamantCore/AdamantCore.swift b/CommonKit/Sources/CommonKit/Protocols/AdamantCore.swift similarity index 92% rename from Adamant/ServiceProtocols/AdamantCore/AdamantCore.swift rename to CommonKit/Sources/CommonKit/Protocols/AdamantCore.swift index c6dd059ea..e064b1e82 100644 --- a/Adamant/ServiceProtocols/AdamantCore/AdamantCore.swift +++ b/CommonKit/Sources/CommonKit/Protocols/AdamantCore.swift @@ -7,13 +7,11 @@ // import Foundation -import CommonKit -protocol AdamantCore: AnyObject { +public protocol AdamantCore: AnyObject { // MARK: - Keys func createHashFor(passphrase: String) -> String? func createKeypairFor(passphrase: String) -> Keypair? - func generateNewPassphrase() -> String // MARK: - Signing transactions func sign(transaction: SignableTransaction, senderId: String, keypair: Keypair) -> String? @@ -38,7 +36,7 @@ protocol AdamantCore: AnyObject { ) -> Data? } -protocol SignableTransaction { +public protocol SignableTransaction { var type: TransactionType { get } var amount: Decimal { get } var senderPublicKey: String { get } diff --git a/Adamant/Modules/Wallets/WalletApiService.swift b/CommonKit/Sources/CommonKit/Protocols/ApiServiceProtocol.swift similarity index 52% rename from Adamant/Modules/Wallets/WalletApiService.swift rename to CommonKit/Sources/CommonKit/Protocols/ApiServiceProtocol.swift index 097854e0b..3ed1fb9e4 100644 --- a/Adamant/Modules/Wallets/WalletApiService.swift +++ b/CommonKit/Sources/CommonKit/Protocols/ApiServiceProtocol.swift @@ -1,5 +1,5 @@ // -// WalletApiService.swift +// ApiServiceProtocol.swift // Adamant // // Created by Andrew G on 20.11.2023. @@ -8,8 +8,9 @@ import Foundation -protocol WalletApiService { - var preferredNodeIds: [UUID] { get } +public protocol ApiServiceProtocol { + var chosenFastestNodeId: UUID? { get } + var hasActiveNode: Bool { get } func healthCheck() } diff --git a/Adamant/ServiceProtocols/NodesAdditionalParamsStorageProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/NodesAdditionalParamsStorageProtocol.swift similarity index 73% rename from Adamant/ServiceProtocols/NodesAdditionalParamsStorageProtocol.swift rename to CommonKit/Sources/CommonKit/Protocols/NodesAdditionalParamsStorageProtocol.swift index 16a5e4df8..1209229ff 100644 --- a/Adamant/ServiceProtocols/NodesAdditionalParamsStorageProtocol.swift +++ b/CommonKit/Sources/CommonKit/Protocols/NodesAdditionalParamsStorageProtocol.swift @@ -6,16 +6,14 @@ // Copyright © 2023 Adamant. All rights reserved. // -import CommonKit - // MARK: - SecuredStore keys -extension StoreKey { +public extension StoreKey { enum NodesAdditionalParamsStorage { - static let fastestNodeMode = "nodesAdditionalParamsStorage.fastestNodeMode" + public static let fastestNodeMode = "nodesAdditionalParamsStorage.fastestNodeMode" } } -protocol NodesAdditionalParamsStorageProtocol { +public protocol NodesAdditionalParamsStorageProtocol { func isFastestNodeMode(group: NodeGroup) -> Bool func fastestNodeMode(group: NodeGroup) -> AnyObservable func setFastestNodeMode(groups: Set, value: Bool) diff --git a/CommonKit/Sources/CommonKit/Protocols/NodesMergingServiceProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/NodesMergingServiceProtocol.swift new file mode 100644 index 000000000..67b37f70a --- /dev/null +++ b/CommonKit/Sources/CommonKit/Protocols/NodesMergingServiceProtocol.swift @@ -0,0 +1,14 @@ +// +// NodesMergingServiceProtocol.swift +// Adamant +// +// Created by Andrew G on 02.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +public protocol NodesMergingServiceProtocol { + func merge( + savedNodes: [NodeGroup: [Node]], + defaultNodes: [NodeGroup: [Node]] + ) -> [NodeGroup: [Node]] +} diff --git a/CommonKit/Sources/CommonKit/Protocols/NodesStorageProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/NodesStorageProtocol.swift new file mode 100644 index 000000000..04e633c52 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Protocols/NodesStorageProtocol.swift @@ -0,0 +1,26 @@ +// +// NodesStorageProtocol.swift +// Adamant +// +// Created by Andrew G on 30.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +// MARK: - SecuredStore keys +public extension StoreKey { + enum NodesStorage { + public static let nodes = "nodesStorage.nodes" + } +} + +public protocol NodesStorageProtocol { + var nodesPublisher: AnyObservable<[NodeGroup: [Node]]> { get } + + func getNodesPublisher(group: NodeGroup) -> AnyObservable<[Node]> + func addNode(_ node: Node, group: NodeGroup) + func resetNodes(_ groups: Set) + func removeNode(id: UUID, group: NodeGroup) + func updateNode(id: UUID, group: NodeGroup, mutate: (inout Node) -> Void) +} diff --git a/Adamant/Services/APICore.swift b/CommonKit/Sources/CommonKit/Services/APICore.swift similarity index 50% rename from Adamant/Services/APICore.swift rename to CommonKit/Sources/CommonKit/Services/APICore.swift index bfcd37c0f..92845dd7e 100644 --- a/Adamant/Services/APICore.swift +++ b/CommonKit/Sources/CommonKit/Services/APICore.swift @@ -8,41 +8,35 @@ import Foundation import Alamofire -import CommonKit -actor APICore: APICoreProtocol { +public 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 = timeoutIntervalForRequest - configuration.timeoutIntervalForResource = timeoutIntervalForResource - configuration.requestCachePolicy = .reloadIgnoringLocalCacheData - configuration.httpMaximumConnectionsPerHost = maximumConnectionsPerHost - return Alamofire.Session.init(configuration: configuration) - }() + private var sessions: [TimeoutSize: Session] = .init() - func sendRequestMultipartFormData( - node: Node, + public func sendRequestMultipartFormData( + origin: NodeOrigin, path: String, models: [MultipartFormDataModel], + timeout: TimeoutSize, uploadProgress: @escaping ((Progress) -> Void) ) async -> APIResponseModel { do { - let request = session.upload(multipartFormData: { multipartFormData in - models.forEach { file in - multipartFormData.append( - file.data, - withName: file.keyName, - fileName: file.fileName - ) - } - }, to: try buildUrl(node: node, path: path)) - .uploadProgress(queue: .global(), closure: uploadProgress) + let request = getSession(timeout).upload( + multipartFormData: { multipartFormData in + models.forEach { file in + multipartFormData.append( + file.data, + withName: file.keyName, + fileName: file.fileName + ) + } + }, + to: try buildUrl(origin: origin, path: path) + ).uploadProgress(queue: .global(), closure: uploadProgress) return await sendRequest(request: request) } catch { @@ -54,17 +48,18 @@ actor APICore: APICoreProtocol { } } - func sendRequestBasic( - node: Node, + public func sendRequestBasic( + origin: NodeOrigin, path: String, method: HTTPMethod, parameters: Parameters, encoding: APIParametersEncoding, + timeout: TimeoutSize, downloadProgress: @escaping ((Progress) -> Void) ) async -> APIResponseModel { do { - let request = session.request( - try buildUrl(node: node, path: path), + let request = getSession(timeout).request( + try buildUrl(origin: origin, path: path), method: method, parameters: parameters.asDictionary(), encoding: encoding.parametersEncoding, @@ -81,11 +76,12 @@ actor APICore: APICoreProtocol { } } - func sendRequestBasic( - node: Node, + public func sendRequestBasic( + origin: NodeOrigin, path: String, method: HTTPMethod, - jsonParameters: Any + jsonParameters: Any, + timeout: TimeoutSize ) async -> APIResponseModel { do { let data = try JSONSerialization.data( @@ -93,13 +89,13 @@ actor APICore: APICoreProtocol { ) var request = try URLRequest( - url: try buildUrl(node: node, path: path), + url: try buildUrl(origin: origin, path: path), method: method ) request.httpBody = data request.headers.update(.contentType("application/json")) - return await sendRequest(request: AF.request(request)) + return await sendRequest(request: getSession(timeout).request(request)) } catch { return .init( result: .failure(.internalError(message: error.localizedDescription, error: error)), @@ -108,28 +104,59 @@ actor APICore: APICoreProtocol { ) } } + + public init() {} } 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 - )) - } - } + await withTaskCancellationHandler( + operation: { + 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 + )) + } + } + }, + onCancel: { request.cancel() } + ) } - func buildUrl(node: Node, path: String) throws -> URL { - guard let url = node.asURL()?.appendingPathComponent(path, conformingTo: .url) + func buildUrl(origin: NodeOrigin, path: String) throws -> URL { + guard let url = origin.asURL()?.appendingPathComponent(path, conformingTo: .url) else { throw InternalAPIError.endpointBuildFailed } return url } + + func getSession(_ timeout: TimeoutSize) -> Session { + if let session = sessions[timeout] { + return session + } + + let configuration = AF.sessionConfiguration + configuration.waitsForConnectivity = true + configuration.timeoutIntervalForRequest = requestTimeout + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + configuration.httpMaximumConnectionsPerHost = maximumConnectionsPerHost + + configuration.timeoutIntervalForResource = switch timeout { + case .common: + resourceTimeout + case .extended: + extendedResourceTimeout + } + + let session = Alamofire.Session.init(configuration: configuration) + sessions[timeout] = session + return session + } } -private let timeoutIntervalForRequest: TimeInterval = 15 -private let timeoutIntervalForResource: TimeInterval = 24 * 3600 +private let requestTimeout: TimeInterval = 15 +private let resourceTimeout: TimeInterval = 15 +private let extendedResourceTimeout: TimeInterval = 24 * 3600 private let maximumConnectionsPerHost = 100 diff --git a/Adamant/Services/ApiService/AdamantApi+Accounts.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Accounts.swift similarity index 78% rename from Adamant/Services/ApiService/AdamantApi+Accounts.swift rename to CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Accounts.swift index 7b8c6bd71..7d40a12e9 100644 --- a/Adamant/Services/ApiService/AdamantApi+Accounts.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Accounts.swift @@ -7,9 +7,8 @@ // import Foundation -import CommonKit -extension ApiCommands { +public extension ApiCommands { static let Accounts = ( root: "/api/accounts", getPublicKey: "/api/accounts/getPublicKey", @@ -20,7 +19,7 @@ extension ApiCommands { // MARK: - Accounts extension AdamantApiService { /// Get account by passphrase. - func getAccount(byPassphrase passphrase: String) async -> ApiServiceResult { + public func getAccount(byPassphrase passphrase: String) async -> ApiServiceResult { guard let keypair = adamantCore.createKeypairFor(passphrase: passphrase) else { return .failure(.accountNotFound) } @@ -29,10 +28,10 @@ extension AdamantApiService { } /// Get account by publicKey - func getAccount(byPublicKey publicKey: String) async -> ApiServiceResult { - switch await request({ apiCore, node in + public func getAccount(byPublicKey publicKey: String) async -> ApiServiceResult { + switch await request({ apiCore, origin in let response: ApiServiceResult> = await apiCore.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Accounts.root, method: .get, parameters: ["publicKey": publicKey], @@ -53,12 +52,12 @@ extension AdamantApiService { } } - func getAccount(byAddress address: String) async -> ApiServiceResult { - await request { apiCore, node in + public func getAccount(byAddress address: String) async -> ApiServiceResult { + await request { apiCore, origin in let response: ApiServiceResult< ServerModelResponse > = await apiCore.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Accounts.root, method: .get, parameters: ["address": address], diff --git a/Adamant/Services/ApiService/AdamantApi+Chats.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Chats.swift similarity index 82% rename from Adamant/Services/ApiService/AdamantApi+Chats.swift rename to CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Chats.swift index 1ec2c866e..9ca677061 100644 --- a/Adamant/Services/ApiService/AdamantApi+Chats.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Chats.swift @@ -8,9 +8,8 @@ import Foundation import UIKit -import CommonKit -extension ApiCommands { +public extension ApiCommands { static let Chats = ( root: "/api/chats", get: "/api/chats/get", @@ -21,7 +20,7 @@ extension ApiCommands { } extension AdamantApiService { - func getMessageTransactions( + public func getMessageTransactions( address: String, height: Int64?, offset: Int? @@ -40,9 +39,9 @@ extension AdamantApiService { } let response: ApiServiceResult> - response = await request { [parameters] service, node in + response = await request { [parameters] service, origin in await service.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Chats.get, method: .get, parameters: parameters, @@ -53,7 +52,7 @@ extension AdamantApiService { return response.flatMap { $0.resolved() } } - func sendMessageTransaction( + public func sendMessageTransaction( transaction: UnregisteredTransaction ) async -> ApiServiceResult { await sendTransaction( @@ -62,9 +61,10 @@ extension AdamantApiService { ) } - func getChatRooms( + public func getChatRooms( address: String, - offset: Int? + offset: Int?, + waitsForConnectivity: Bool ) async -> ApiServiceResult { var parameters = ["limit": "20"] @@ -72,9 +72,10 @@ extension AdamantApiService { parameters["offset"] = String(offset) } - return await request { [parameters] service, node in + return await request(waitsForConnectivity: waitsForConnectivity) { + [parameters] service, origin in await service.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Chats.getChatRooms + "/\(address)", method: .get, parameters: parameters, @@ -83,7 +84,7 @@ extension AdamantApiService { } } - func getChatMessages( + public func getChatMessages( address: String, addressRecipient: String, offset: Int?, @@ -99,9 +100,9 @@ extension AdamantApiService { parameters["limit"] = String(limit) } - return await request { [parameters] service, node in + return await request { [parameters] service, origin in await service.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Chats.getChatRooms + "/\(address)/\(addressRecipient)", method: .get, parameters: parameters, diff --git a/Adamant/Services/ApiService/AdamantApi+Delegates.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Delegates.swift similarity index 86% rename from Adamant/Services/ApiService/AdamantApi+Delegates.swift rename to CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Delegates.swift index 0ce14a3f2..a466aef71 100644 --- a/Adamant/Services/ApiService/AdamantApi+Delegates.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Delegates.swift @@ -8,9 +8,8 @@ import Foundation import UIKit -import CommonKit -extension ApiCommands { +public extension ApiCommands { static let Delegates = ( root: "/api/delegates", getDelegates: "/api/delegates", @@ -23,19 +22,19 @@ extension ApiCommands { } extension AdamantApiService { - func getDelegates(limit: Int) async -> ApiServiceResult<[Delegate]> { + public func getDelegates(limit: Int) async -> ApiServiceResult<[Delegate]> { await getDelegates(limit: limit, offset: .zero, currentDelegates: [Delegate]()) } - func getDelegates( + public func getDelegates( limit: Int, offset: Int, currentDelegates: [Delegate] ) async -> ApiServiceResult<[Delegate]> { let response: ApiServiceResult> - response = await request { core, node in + response = await request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Delegates.getDelegates, method: .get, parameters: ["limit": String(limit), "offset": String(offset)], @@ -58,7 +57,7 @@ extension AdamantApiService { } } - func getDelegatesWithVotes(for address: String, limit: Int) async -> ApiServiceResult<[Delegate]> { + public func getDelegatesWithVotes(for address: String, limit: Int) async -> ApiServiceResult<[Delegate]> { let response = await getVotes(for: address) switch response { @@ -81,10 +80,10 @@ extension AdamantApiService { } } - func getForgedByAccount(publicKey: String) async -> ApiServiceResult { - await request { core, node in + public func getForgedByAccount(publicKey: String) async -> ApiServiceResult { + await request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Delegates.getForgedByAccount, method: .get, parameters: ["generatorPublicKey": publicKey], @@ -93,7 +92,7 @@ extension AdamantApiService { } } - func getForgingTime(for delegate: Delegate) async -> ApiServiceResult { + public func getForgingTime(for delegate: Delegate) async -> ApiServiceResult { await getNextForgers().map { nextForgers in var forgingTime = -1 if let fIndex = nextForgers.delegates.firstIndex(of: delegate.publicKey) { @@ -104,18 +103,18 @@ extension AdamantApiService { } private func getDelegatesCount() async -> ApiServiceResult { - await request { core, node in + await request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Delegates.getDelegatesCount ) } } private func getNextForgers() async -> ApiServiceResult { - await request { core, node in + await request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Delegates.getNextForgers, method: .get, parameters: ["limit": "\(101)"], @@ -124,11 +123,11 @@ extension AdamantApiService { } } - func getVotes(for address: String) async -> ApiServiceResult<[Delegate]> { + public func getVotes(for address: String) async -> ApiServiceResult<[Delegate]> { let response: ApiServiceResult> - response = await request { core, node in + response = await request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Delegates.votes, method: .get, parameters: ["address": address], @@ -139,7 +138,7 @@ extension AdamantApiService { return response.map { $0.collection ?? .init() } } - func voteForDelegates( + public func voteForDelegates( from address: String, keypair: Keypair, votes: [DelegateVote] @@ -183,9 +182,9 @@ extension AdamantApiService { // MARK: - Private methods private func getBlocks() async -> ApiServiceResult<[Block]> { - let response: ApiServiceResult> = await request { core, node in + let response: ApiServiceResult> = await request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Delegates.getBlocks, method: .get, parameters: ["orderBy": "height:desc", "limit": "\(101)"], diff --git a/Adamant/Services/ApiService/AdamantApi+Keys.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Keys.swift similarity index 77% rename from Adamant/Services/ApiService/AdamantApi+Keys.swift rename to CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Keys.swift index 2226421f4..2e6f2ca6f 100644 --- a/Adamant/Services/ApiService/AdamantApi+Keys.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Keys.swift @@ -7,13 +7,12 @@ // import Foundation -import CommonKit extension AdamantApiService { - func getPublicKey(byAddress address: String) async -> ApiServiceResult { - let response: ApiServiceResult = await request { service, node in + public func getPublicKey(byAddress address: String) async -> ApiServiceResult { + let response: ApiServiceResult = await request { service, origin in await service.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Accounts.getPublicKey, method: .get, parameters: ["address": address], diff --git a/Adamant/Services/ApiService/AdamantApi+States.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+States.swift similarity index 87% rename from Adamant/Services/ApiService/AdamantApi+States.swift rename to CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+States.swift index 4800bb41b..705d51a1d 100644 --- a/Adamant/Services/ApiService/AdamantApi+States.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+States.swift @@ -8,9 +8,8 @@ import Foundation import UIKit -import CommonKit -extension ApiCommands { +public extension ApiCommands { static let States = ( root: "/api/states", get: "/api/states/get", @@ -19,9 +18,9 @@ extension ApiCommands { } extension AdamantApiService { - static let KvsFee: Decimal = 0.001 + public static let KvsFee: Decimal = 0.001 - func store( + public func store( key: String, value: String, type: StateType, @@ -54,7 +53,7 @@ extension AdamantApiService { ) } - func get(key: String, sender: String) async -> ApiServiceResult { + public func get(key: String, sender: String) async -> ApiServiceResult { // MARK: 1. Prepare let parameters = [ "senderId": sender, @@ -63,9 +62,9 @@ extension AdamantApiService { ] let response: ApiServiceResult> - response = await request { [parameters] core, node in + response = await request { [parameters] core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.States.get, method: .get, parameters: parameters, diff --git a/Adamant/Services/ApiService/AdamantApi+Transactions.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transactions.swift similarity index 85% rename from Adamant/Services/ApiService/AdamantApi+Transactions.swift rename to CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transactions.swift index f63400128..9abd6da8c 100644 --- a/Adamant/Services/ApiService/AdamantApi+Transactions.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transactions.swift @@ -7,9 +7,8 @@ // import Foundation -import CommonKit -extension ApiCommands { +public extension ApiCommands { static let Transactions = ( root: "/api/transactions", getTransaction: "/api/transactions/get", @@ -19,13 +18,13 @@ extension ApiCommands { } extension AdamantApiService { - func sendTransaction( + public func sendTransaction( path: String, transaction: UnregisteredTransaction ) async -> ApiServiceResult { - let response: ApiServiceResult = await request { core, node in + let response: ApiServiceResult = await request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: path, method: .post, parameters: ["transaction": transaction], @@ -36,13 +35,13 @@ extension AdamantApiService { return response.flatMap { $0.resolved() } } - func sendDelegateVoteTransaction( + public func sendDelegateVoteTransaction( path: String, transaction: UnregisteredTransaction ) async -> ApiServiceResult { - let response: ApiServiceResult = await request { core, node in + let response: ApiServiceResult = await request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: path, method: .post, parameters: transaction, @@ -56,15 +55,15 @@ extension AdamantApiService { } } - func getTransaction(id: UInt64) async -> ApiServiceResult { + public func getTransaction(id: UInt64) async -> ApiServiceResult { await getTransaction(id: id, withAsset: false) } - func getTransaction(id: UInt64, withAsset: Bool) async -> ApiServiceResult { + public func getTransaction(id: UInt64, withAsset: Bool) async -> ApiServiceResult { let response: ApiServiceResult> - response = await request { core, node in + response = await request { core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Transactions.getTransaction, method: .get, parameters: [ @@ -78,7 +77,7 @@ extension AdamantApiService { return response.flatMap { $0.resolved() } } - func getTransactions( + public func getTransactions( forAccount account: String, type: TransactionType, fromHeight: Int64?, @@ -95,7 +94,7 @@ extension AdamantApiService { ) } - func getTransactions( + public func getTransactions( forAccount account: String, type: TransactionType, fromHeight: Int64?, @@ -129,9 +128,9 @@ extension AdamantApiService { } let response: ApiServiceResult> - response = await request { [queryItems] core, node in + response = await request { [queryItems] core, origin in await core.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.Transactions.root, method: .get, parameters: core.emptyParameters, diff --git a/Adamant/Services/ApiService/AdamantApi+Transfers.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transfers.swift similarity index 89% rename from Adamant/Services/ApiService/AdamantApi+Transfers.swift rename to CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transfers.swift index e2c9cf7b3..700f0ed3c 100644 --- a/Adamant/Services/ApiService/AdamantApi+Transfers.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transfers.swift @@ -7,19 +7,18 @@ // import Foundation -import CommonKit import CryptoSwift import BigInt extension AdamantApiService { - func transferFunds(transaction: UnregisteredTransaction) async -> ApiServiceResult { + public func transferFunds(transaction: UnregisteredTransaction) async -> ApiServiceResult { return await sendTransaction( path: ApiCommands.Transactions.processTransaction, transaction: transaction ) } - func transferFunds( + public func transferFunds( sender: String, recipient: String, amount: Decimal, diff --git a/Adamant/Services/ApiService/AdamantApiCore.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiCore.swift similarity index 63% rename from Adamant/Services/ApiService/AdamantApiCore.swift rename to CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiCore.swift index 1a4861d62..1ada27df5 100644 --- a/Adamant/Services/ApiService/AdamantApiCore.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiCore.swift @@ -8,32 +8,35 @@ import Foundation import Alamofire -import CommonKit -extension ApiCommands { +public extension ApiCommands { static let status = "/api/node/status" static let version = "/api/peers/version" } -final class AdamantApiCore { - let apiCore: APICoreProtocol +public final class AdamantApiCore { + public let apiCore: APICoreProtocol - init(apiCore: APICoreProtocol) { + public init(apiCore: APICoreProtocol) { self.apiCore = apiCore } - func getNodeStatus(node: Node) async -> ApiServiceResult { + public func getNodeStatus( + origin: NodeOrigin + ) async -> ApiServiceResult { await apiCore.sendRequestJsonResponse( - node: node, + origin: origin, path: ApiCommands.status ) } } extension AdamantApiCore: BlockchainHealthCheckableService { - func getStatusInfo(node: Node) async -> ApiServiceResult { + public func getStatusInfo( + origin: NodeOrigin + ) async -> ApiServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 - let statusResponse = await getNodeStatus(node: node) + let statusResponse = await getNodeStatus(origin: origin) let ping = Date.now.timeIntervalSince1970 - startTimestamp return statusResponse.map { statusDto in @@ -42,7 +45,7 @@ extension AdamantApiCore: BlockchainHealthCheckableService { height: statusDto.network?.height ?? .zero, wsEnabled: statusDto.wsClient?.enabled ?? false, wsPort: statusDto.wsClient?.port, - version: statusDto.version?.version + version: statusDto.version?.version.flatMap { .init($0) } ) } } diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift new file mode 100644 index 000000000..9bc3904c2 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift @@ -0,0 +1,47 @@ +// +// AdamantApiService.swift +// Adamant +// +// Created by Anokhov Pavel on 06.01.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +public final class AdamantApiService { + public let adamantCore: AdamantCore + public let service: BlockchainHealthCheckWrapper + + public init( + healthCheckWrapper: BlockchainHealthCheckWrapper, + adamantCore: AdamantCore + ) { + service = healthCheckWrapper + self.adamantCore = adamantCore + } + + public func request( + waitsForConnectivity: Bool = false, + _ request: @Sendable (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> ApiServiceResult { + await service.request( + waitsForConnectivity: waitsForConnectivity + ) { admApiCore, origin in + await request(admApiCore.apiCore, origin) + } + } +} + +extension AdamantApiService: AdamantApiServiceProtocol { + public var chosenFastestNodeId: UUID? { + service.chosenFastestNodeId + } + + public func healthCheck() { + service.healthCheck() + } + + public var hasActiveNode: Bool { + !service.sortedAllowedNodes.isEmpty + } +} diff --git a/CommonKit/Sources/CommonKit/Services/HealthCheck/BlockchainHealthCheckWrapper.swift b/CommonKit/Sources/CommonKit/Services/HealthCheck/BlockchainHealthCheckWrapper.swift new file mode 100644 index 000000000..ced190455 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Services/HealthCheck/BlockchainHealthCheckWrapper.swift @@ -0,0 +1,239 @@ +// +// BlockchainHealthCheckWrapper.swift +// Adamant +// +// Created by Andrew G on 22.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation + +public protocol BlockchainHealthCheckableService { + associatedtype Error: HealthCheckableError + + func getStatusInfo(origin: NodeOrigin) async -> Result +} + +public final class BlockchainHealthCheckWrapper< + Service: BlockchainHealthCheckableService +>: HealthCheckWrapper { + private let nodesStorage: NodesStorageProtocol + private let updateNodesAvailabilityLock = NSLock() + private let params: BlockchainHealthCheckParams + + @Atomic private var currentRequests = Set() + + public init( + service: Service, + nodesStorage: NodesStorageProtocol, + nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol, + isActive: Bool, + params: BlockchainHealthCheckParams, + connection: AnyObservable + ) { + self.nodesStorage = nodesStorage + self.params = params + + super.init( + service: service, + isActive: isActive, + name: params.name, + normalUpdateInterval: params.normalUpdateInterval, + crucialUpdateInterval: params.crucialUpdateInterval, + connection: connection, + nodes: nodesStorage.getNodesPublisher(group: params.group) + ) + + nodesAdditionalParamsStorage + .fastestNodeMode(group: params.group) + .sink { [weak self] in self?.fastestNodeMode = $0 } + .store(in: &subscriptions) + } + + public override func healthCheck() { + super.healthCheck() + guard isActive else { return } + + Task { + updateNodesAvailability(update: nil) + + 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, + let update = await updateNodeStatusInfo(node: node) + else { return } + + updateNodesAvailability(update: update) + } + } + + await group.waitForAll() + } + } + } +} + +private extension BlockchainHealthCheckWrapper { + struct NodeUpdate { + let id: UUID + let info: NodeStatusInfo? + let preferMainOrigin: Bool? + } + + func updateNodeStatusInfo(node: Node) async -> NodeUpdate? { + guard !currentRequests.contains(node.id) else { return nil } + currentRequests.insert(node.id) + defer { currentRequests.remove(node.id) } + + guard + node.preferMainOrigin == nil, + let altOrigin = node.altOrigin + else { + return .init( + id: node.id, + info: try? await service.getStatusInfo(origin: node.preferredOrigin).get(), + preferMainOrigin: nil + ) + } + + switch await service.getStatusInfo(origin: node.mainOrigin) { + case let .success(info): + return .init( + id: node.id, + info: info, + preferMainOrigin: true + ) + case .failure: + switch await service.getStatusInfo(origin: altOrigin) { + case let .success(info): + return .init( + id: node.id, + info: info, + preferMainOrigin: false + ) + case .failure: + return .init( + id: node.id, + info: nil, + preferMainOrigin: nil + ) + } + } + } + + func applyUpdate(update: NodeUpdate) { + updateNode(id: update.id) { node in + if let preferMainOrigin = update.preferMainOrigin { + node.preferMainOrigin = preferMainOrigin + } + + guard let info = update.info else { return node.connectionStatus = .offline } + node.wsEnabled = info.wsEnabled + node.updateWsPort(info.wsPort) + node.version = info.version + node.height = info.height + node.ping = info.ping + + guard + let version = info.version, + let minNodeVersion = params.minNodeVersion, + version < minNodeVersion + else { return } + + node.connectionStatus = .notAllowed(.outdatedApiVersion) + } + } + + func updateNodesAvailability(update: NodeUpdate?) { + updateNodesAvailabilityLock.lock() + defer { updateNodesAvailabilityLock.unlock() } + let forceIncludeId = update?.info != nil ? update?.id : nil + + if let update = update { + applyUpdate(update: update) + } + + let workingNodes = nodes.filter { + $0.isEnabled && ($0.isWorkingStatus) || $0.id == forceIncludeId + } + + let actualHeightsRange = getActualNodeHeightsRange( + heights: workingNodes.compactMap { $0.height }, + group: params.group, + nodeHeightEpsilon: params.nodeHeightEpsilon + ) + + workingNodes.forEach { node in + var status: NodeConnectionStatus? + + if + let version = node.version, + let minNodeVersion = params.minNodeVersion, + version < minNodeVersion + { + status = .notAllowed(.outdatedApiVersion) + } else { + status = node.height.map { height in + actualHeightsRange?.contains(height) ?? false + ? .allowed + : .synchronizing + } ?? .none + } + + updateNode(id: node.id) { $0.connectionStatus = status } + } + } + + func updateNode(id: UUID, mutate: (inout Node) -> Void) { + nodesStorage.updateNode( + id: id, + group: params.group, + mutate: mutate + ) + } +} + +private extension Node { + var isWorkingStatus: Bool { + switch connectionStatus { + case .allowed, .synchronizing, .none: + return isEnabled + case .offline, .notAllowed: + return false + } + } +} + +private struct NodeHeightsInterval { + let range: ClosedRange + var count: Int +} + +private func getActualNodeHeightsRange( + heights: [Int], + group: NodeGroup, + nodeHeightEpsilon: Int +) -> ClosedRange? { + let heights = heights.sorted() + var bestInterval: NodeHeightsInterval? + + for i in heights.indices { + var currentInterval = NodeHeightsInterval( + range: heights[i] ... heights[i] + 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/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift b/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift new file mode 100644 index 000000000..32dc5e9ea --- /dev/null +++ b/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift @@ -0,0 +1,198 @@ +// +// HealthCheckWrapper.swift +// Adamant +// +// Created by Andrew G on 22.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation +import Combine +import UIKit + +public protocol HealthCheckableError: Error { + var isNetworkError: Bool { get } + + static func noEndpointsError(nodeGroupName: String) -> Self +} + +open class HealthCheckWrapper { + @ObservableValue public private(set) var nodes: [Node] = .init() + @ObservableValue public private(set) var sortedAllowedNodes: [Node] = .init() + + public let service: Service + public let isActive: Bool + public let name: String + + private let normalUpdateInterval: TimeInterval + private let crucialUpdateInterval: TimeInterval + + @Atomic public var fastestNodeMode = true + @Atomic public var healthCheckTimerSubscription: AnyCancellable? + @Atomic public var subscriptions: Set = .init() + + @Atomic private var previousAppState: UIApplication.State? + @Atomic private var lastUpdateTime: Date? + + public var chosenFastestNodeId: UUID? { + fastestNodeMode + ? sortedAllowedNodes.first?.id + : nil + } + + public init( + service: Service, + isActive: Bool, + name: String, + normalUpdateInterval: TimeInterval, + crucialUpdateInterval: TimeInterval, + connection: AnyObservable, + nodes: AnyObservable<[Node]> + ) { + self.service = service + self.isActive = isActive + self.name = name + self.normalUpdateInterval = normalUpdateInterval + self.crucialUpdateInterval = crucialUpdateInterval + + let connection = connection + .removeDuplicates() + .filter { $0 } + + nodes + .removeDuplicates() + .handleEvents(receiveOutput: { [weak self] in self?.updateNodes($0) }) + .removeDuplicates { !$0.doesNeedHealthCheck($1) } + .combineLatest(connection) + .sink { [weak self] _ in self?.healthCheck() } + .store(in: &subscriptions) + + $sortedAllowedNodes + .map { $0.isEmpty } + .removeDuplicates() + .sink { [weak self] _ in self?.updateHealthCheckTimerSubscription() } + .store(in: &subscriptions) + + NotificationCenter.default + .publisher(for: UIApplication.didBecomeActiveNotification, object: nil) + .sink { [weak self] _ in self?.didBecomeActiveAction() } + .store(in: &subscriptions) + + NotificationCenter.default + .publisher(for: UIApplication.willResignActiveNotification, object: nil) + .sink { [weak self] _ in self?.previousAppState = .background } + .store(in: &subscriptions) + } + + public func request( + waitsForConnectivity: Bool = false, + _ requestAction: @Sendable (Service, NodeOrigin) async -> Result + ) async -> Result { + let nodesList = await nodesForRequest(waitsForConnectivity: waitsForConnectivity) + + var lastConnectionError = nodesList.isEmpty + ? Error.noEndpointsError(nodeGroupName: name) + : nil + + for node in nodesList { + let response = await requestAction(service, node.preferredOrigin) + + switch response { + case .success: + return response + case let .failure(error): + guard error.isNetworkError else { return response } + lastConnectionError = error + } + } + + if lastConnectionError != nil { healthCheck() } + + return await waitsForConnectivity + ? request(waitsForConnectivity: waitsForConnectivity, requestAction) + : .failure(lastConnectionError ?? .noEndpointsError(nodeGroupName: name)) + } + + open func healthCheck() { + guard isActive else { return } + lastUpdateTime = .now + updateHealthCheckTimerSubscription() + } +} + +private extension HealthCheckWrapper { + func nodesForRequest(waitsForConnectivity: Bool) async -> [Node] { + await $sortedAllowedNodes.compactMap { [fastestNodeMode = $fastestNodeMode] in + guard !waitsForConnectivity || !$0.isEmpty else { return nil } + return fastestNodeMode.value ? $0 : $0.shuffled() + }.values.first { _ in true } ?? .init() + } + + func updateHealthCheckTimerSubscription() { + healthCheckTimerSubscription = Timer.publish( + every: sortedAllowedNodes.isEmpty + ? crucialUpdateInterval + : normalUpdateInterval, + on: .main, + in: .default + ).autoconnect().sink { [weak self] _ in + self?.healthCheck() + } + } + + func didBecomeActiveAction() { + defer { previousAppState = .active } + + guard + previousAppState == .background, + let timeToUpdate = lastUpdateTime?.addingTimeInterval(normalUpdateInterval / 3), + Date.now > timeToUpdate + else { return } + + healthCheck() + } + + func updateNodes(_ newNodes: [Node]) { + nodes = newNodes + + sortedAllowedNodes = newNodes.getAllowedNodes( + sortedBySpeedDescending: true, + needWS: false + ) + } +} + +private extension Sequence where Element == Node { + func doesNeedHealthCheck( + _ nodes: Nodes + ) -> Bool where Nodes.Element == Self.Element { + Set(self.map { NodeComparisonInfo(node: $0) }) + != Set(nodes.map { NodeComparisonInfo(node: $0) }) + } +} + +private struct NodeComparisonInfo: Hashable { + let id: UUID + let mainOrigin: NodeOriginComparisonInfo + let altOrigin: NodeOriginComparisonInfo? + let isEnabled: Bool + + init(node: Node) { + id = node.id + mainOrigin = .init(origin: node.mainOrigin) + altOrigin = node.altOrigin.map { .init(origin: $0) } + isEnabled = node.isEnabled + } +} + +private struct NodeOriginComparisonInfo: Hashable { + let scheme: NodeOrigin.URLScheme + let host: String + let port: Int? + + init(origin: NodeOrigin) { + scheme = origin.scheme + host = origin.host + port = origin.port + } +} diff --git a/CommonKit/Sources/CommonKit/Services/KeychainStore.swift b/CommonKit/Sources/CommonKit/Services/KeychainStore.swift index bd290b26d..9be433fe9 100644 --- a/CommonKit/Sources/CommonKit/Services/KeychainStore.swift +++ b/CommonKit/Sources/CommonKit/Services/KeychainStore.swift @@ -9,116 +9,221 @@ import Foundation import KeychainAccess import RNCryptor +import CryptoKit public final class KeychainStore: SecuredStore { // MARK: - Properties private static let keychain = Keychain(service: "\(AdamantSecret.appIdentifierPrefix).im.adamant.messenger") - public init() {} + private let secureStorage: SecureStorageProtocol + + private let keychainStoreIdAlias = "com.adamant.messenger.id" + private var keychainPassword: String? + + private let oldKeychainService = "im.adamant" + private let migrationKey = "migrated" + private let migrationValue = "2" + private lazy var userDefaults = UserDefaults(suiteName: sharedGroup) + + public init(secureStorage: SecureStorageProtocol) { + self.secureStorage = secureStorage + + migrateUserDefaultsIfNeeded() + clearIfNeeded() + configure() + migrateIfNeeded() + } // MARK: - SecuredStore public func get(_ key: String) -> T? { - guard !(T.self == String.self) else { return getString(key) as? T } + guard let data = getValue(key) else { return nil } - guard - let raw = getString(key), - let data = raw.data(using: .utf8) - else { return nil } + guard !(T.self == String.self) else { + return String(data: data, encoding: .utf8) as? T + } return try? JSONDecoder().decode(T.self, from: data) } public func set(_ value: T, for key: String) { - if let string = value as? String { - setString(string, for: key) + if let string = value as? String, + let data = string.data(using: .utf8) { + setValue(data, for: key) return } guard let data = try? JSONEncoder().encode(value) else { return } - String(data: data, encoding: .utf8).map { setString($0, for: key) } + setValue(data, for: key) } public func remove(_ key: String) { try? KeychainStore.keychain.remove(key) } - - public func purgeStore() { - try? KeychainStore.keychain.removeAll() - NotificationCenter.default.post(name: Notification.Name.SecuredStore.securedStorePurged, object: self) +} + +private extension KeychainStore { + func configure() { + guard let privateKey = secureStorage.getPrivateKey(), + let publicKey = secureStorage.getPublicKey(privateKey: privateKey) + else { return } + + if let savedKey = getData(for: keychainStoreIdAlias) { + let decryptedData = secureStorage.decrypt( + data: savedKey, + privateKey: privateKey + ) + + keychainPassword = decryptedData?.base64EncodedString() + return + } + + let keychainRandomKeyData = SymmetricKey(size: .bits256) + .withUnsafeBytes { Data($0) } + let keychainRandomKey = keychainRandomKeyData.base64EncodedString() + + guard let encryptedData = secureStorage.encrypt( + data: keychainRandomKeyData, + publicKey: publicKey + ) else { return } + + keychainPassword = keychainRandomKey + setData(encryptedData, for: keychainStoreIdAlias) } - // MARK: - Tools + func clearIfNeeded() { + guard let userDefaults = userDefaults else { return } + + let isFirstRun = !userDefaults.bool(forKey: firstRun) + + guard isFirstRun else { return } + + userDefaults.set(true, forKey: firstRun) + + purgeStore() + } - private func getString(_ key: String) -> String? { - guard let encryptedValue = KeychainStore.keychain[key], - let decryptedValue = KeychainStore.decrypt(string: encryptedValue, password: AdamantSecret.keychainValuePassword) else { - return nil - } - - return decryptedValue + func getValue(_ key: String) -> Data? { + guard let keychainPassword = keychainPassword, + let data = getData(for: key) + else { return nil} + + return decrypt( + data: data, + password: keychainPassword + ) } - - private func setString(_ value: String, for key: String) { - guard let encryptedValue = KeychainStore.encrypt(string: value, password: AdamantSecret.keychainValuePassword) else { + + func setValue(_ value: Data, for key: String) { + guard let keychainPassword = keychainPassword else { return } - - try? KeychainStore.keychain.set(encryptedValue, key: key) + + let encryptedValue = encrypt( + data: value, + password: keychainPassword + ) + + setData(encryptedValue, for: key) } - private static func encrypt(string: String, password: String, encoding: String.Encoding = .utf8) -> String? { - guard let data = string.data(using: encoding) else { - return nil - } - - return RNCryptor.encrypt(data: data, withPassword: password).base64EncodedString() + func getData(for key: String) -> Data? { + try? KeychainStore.keychain.getData(key) + } + + func setData(_ value: Data, for key: String) { + try? KeychainStore.keychain.set(value, key: key) + } + + func encrypt( + data: Data, + password: String + ) -> Data { + RNCryptor.encrypt( + data: data, + withPassword: password + ) + } + + func decrypt( + data: Data, + password: String + ) -> Data? { + try? RNCryptor.decrypt(data: data, withPassword: password) } - private static func decrypt(string: String, password: String, encoding: String.Encoding = .utf8) -> String? { - if let encryptedData = Data(base64Encoded: string), - let data = try? RNCryptor.decrypt(data: encryptedData, withPassword: password), - let string = String(data: data, encoding: encoding) { - return string + func decryptOld( + string: String, + password: String + ) -> Data? { + guard let encryptedData = Data(base64Encoded: string) else { + return nil } - - return nil + return try? RNCryptor.decrypt(data: encryptedData, withPassword: password) } + func purgeStore() { + try? KeychainStore.keychain.removeAll() + NotificationCenter.default.post(name: Notification.Name.SecuredStore.securedStorePurged, object: self) + } +} + +private extension KeychainStore { // MARK: - Migration /* * Long time ago, we didn't use shared keychain. Now we do. We need to move all items from old keychain to new. And drop old one. */ - private static let oldKeychainService = "im.adamant" - private static let migrationKey = "migrated" - private static let migrationValue = "1" - public static func migrateIfNeeded() { - // Check flag - if let migrated = KeychainStore.keychain[migrationKey], migrated == migrationValue { - return - } + func migrateIfNeeded() { + let migrated = KeychainStore.keychain[migrationKey] - // Get old keychain - let oldKeychain = Keychain(service: KeychainStore.oldKeychainService) - for key in oldKeychain.allKeys() { - // Get value, decode with old pass - guard let oldEncryptedValue = oldKeychain[key], let value = decrypt(string: oldEncryptedValue, password: AdamantSecret.oldKeychainPass) else { - continue - } - - // Encode value and key with new pass - guard let encryptedValue = encrypt(string: value, password: AdamantSecret.keychainValuePassword) else { - continue - } - - try? KeychainStore.keychain.set(encryptedValue, key: key) - } + guard keychainPassword != nil, + migrated != migrationValue + else { return } + + let oldKeychain = Keychain(service: oldKeychainService) + + migrate( + keychain: oldKeychain, + oldPassword: AdamantSecret.oldKeychainPass + ) + + migrate( + keychain: KeychainStore.keychain, + oldPassword: AdamantSecret.keychainValuePassword + ) - // Set flag try? KeychainStore.keychain.set(migrationValue, key: migrationKey) - // Drop old keychain try? oldKeychain.removeAll() } + + func migrate( + keychain: Keychain, + oldPassword: String + ) { + for key in keychain.allKeys() { + guard key != keychainStoreIdAlias, + let oldEncryptedValue = keychain[key], + let value = decryptOld( + string: oldEncryptedValue, + password: oldPassword + ) + else { continue } + + try? KeychainStore.keychain.remove(key) + setValue(value, for: key) + } + } + + func migrateUserDefaultsIfNeeded() { + let migrated = KeychainStore.keychain[migrationKey] + guard migrated != migrationValue else { return } + + let value = UserDefaults.standard.bool(forKey: firstRun) + userDefaults?.set(value, forKey: firstRun) + } } + +private let firstRun = "app.firstRun" +private let sharedGroup = "group.adamant.adamant-messenger" diff --git a/Adamant/Services/NodesAdditionalParamsStorage.swift b/CommonKit/Sources/CommonKit/Services/NodesAdditionalParamsStorage.swift similarity index 78% rename from Adamant/Services/NodesAdditionalParamsStorage.swift rename to CommonKit/Sources/CommonKit/Services/NodesAdditionalParamsStorage.swift index 9dad62d1f..6da72bbeb 100644 --- a/Adamant/Services/NodesAdditionalParamsStorage.swift +++ b/CommonKit/Sources/CommonKit/Services/NodesAdditionalParamsStorage.swift @@ -7,27 +7,26 @@ // import Foundation -import CommonKit import Combine -final class NodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol { +public final class NodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol { @Atomic private var fastestNodeModeValues: ObservableValue<[NodeGroup: Bool]> private let securedStore: SecuredStore private var subscription: AnyCancellable? - func isFastestNodeMode(group: NodeGroup) -> Bool { + public func isFastestNodeMode(group: NodeGroup) -> Bool { fastestNodeModeValues.wrappedValue[group] ?? group.defaultFastestNodeMode } - func fastestNodeMode(group: NodeGroup) -> AnyObservable { + public func fastestNodeMode(group: NodeGroup) -> AnyObservable { fastestNodeModeValues .map { $0[group] ?? group.defaultFastestNodeMode } .removeDuplicates() .eraseToAnyPublisher() } - func setFastestNodeMode(groups: Set, value: Bool) { + public func setFastestNodeMode(groups: Set, value: Bool) { $fastestNodeModeValues.mutate { dict in groups.forEach { dict.wrappedValue[$0] = value @@ -35,11 +34,11 @@ final class NodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol { } } - func setFastestNodeMode(group: NodeGroup, value: Bool) { + public func setFastestNodeMode(group: NodeGroup, value: Bool) { fastestNodeModeValues.wrappedValue[group] = value } - init(securedStore: SecuredStore) { + public init(securedStore: SecuredStore) { self.securedStore = securedStore _fastestNodeModeValues = .init(wrappedValue: .init( diff --git a/CommonKit/Sources/CommonKit/Services/NodesMergingService.swift b/CommonKit/Sources/CommonKit/Services/NodesMergingService.swift new file mode 100644 index 000000000..24aa3a00a --- /dev/null +++ b/CommonKit/Sources/CommonKit/Services/NodesMergingService.swift @@ -0,0 +1,94 @@ +// +// AdamantNodesMergingService.swift +// Adamant +// +// Created by Andrew G on 02.08.2024. +// Copyright © 2024 Adamant. All rights reserved. +// + +public struct NodesMergingService: NodesMergingServiceProtocol { + public func merge( + savedNodes: [NodeGroup: [Node]], + defaultNodes: [NodeGroup: [Node]] + ) -> [NodeGroup: [Node]] { + var resultNodes = savedNodes + + defaultNodes.keys.forEach { group in + guard resultNodes[group] == nil else { return } + resultNodes[group] = .init() + } + + resultNodes.forEach { group, nodes in + guard let defaultNodes = defaultNodes[group] else { return } + resultNodes[group] = merge(savedNodes: nodes, defaultNodes: defaultNodes) + } + + return resultNodes + } + + public init() {} +} + +private extension NodesMergingService { + func merge(savedNodes: [Node], defaultNodes: [Node]) -> [Node] { + var resultNodes = savedNodes + var defaultNodes = defaultNodes + var removedNodesIndexes: [Int] = .init() + + // Merging default nodes + resultNodes.enumerated().forEach { index, node in + switch node.type { + case .default: + let defaultNodeIndex = defaultNodes.firstIndex { $0.isSame(node) } + + if let defaultNodeIndex = defaultNodeIndex { + resultNodes[index].merge(defaultNodes[defaultNodeIndex]) + defaultNodes.remove(at: defaultNodeIndex) + } else { + // If the default node saved, but not persists in the defaultNodes list, + // it has to be removed + removedNodesIndexes.append(index) + } + case .custom: + break + } + } + + removedNodesIndexes.reversed().forEach { + resultNodes.remove(at: $0) + } + + // We are filtering default nodes to avoid duplications. + // Maybe a new default node is a user's old custom node + return resultNodes + defaultNodes.filter { defaultNode in + !resultNodes.contains { $0.isSame(defaultNode) } + } + } +} + +private extension Node { + mutating func merge(_ node: Node) { + mainOrigin.merge(node.mainOrigin) + + guard let mergedAltOrigin = node.altOrigin else { + altOrigin = nil + return + } + + guard altOrigin != nil else { + altOrigin = mergedAltOrigin + return + } + + altOrigin?.merge(mergedAltOrigin) + } +} + +private extension NodeOrigin { + mutating func merge(_ origin: NodeOrigin) { + scheme = origin.scheme + host = origin.host + port = origin.port + origin.wsPort.map { wsPort = $0 } + } +} diff --git a/CommonKit/Sources/CommonKit/Services/NodesStorage.swift b/CommonKit/Sources/CommonKit/Services/NodesStorage.swift new file mode 100644 index 000000000..da6f2960c --- /dev/null +++ b/CommonKit/Sources/CommonKit/Services/NodesStorage.swift @@ -0,0 +1,157 @@ +// +// NodesStorage.swift +// Adamant +// +// Created by Andrew G on 30.10.2023. +// Copyright © 2023 Adamant. All rights reserved. +// + +import Foundation +import Combine + +public final class NodesStorage: NodesStorageProtocol { + public typealias DefaultNodesGetter = @Sendable (Set) -> [NodeGroup: [Node]] + + @Atomic private var items: ObservableValue<[NodeGroup: [Node]]> + + public var nodesPublisher: AnyObservable<[NodeGroup: [Node]]> { + items + .map { $0.mapValues { $0.filter { !$0.isHidden } } } + .removeDuplicates() + .eraseToAnyPublisher() + } + + private var subscription: AnyCancellable? + private let securedStore: SecuredStore + private let defaultNodes: DefaultNodesGetter + + public func getNodesPublisher(group: NodeGroup) -> AnyObservable<[Node]> { + nodesPublisher + .map { $0[group] ?? .init() } + .removeDuplicates() + .eraseToAnyPublisher() + } + + public func addNode(_ node: Node, group: NodeGroup) { + $items.mutate { items in + if items.wrappedValue[group] == nil { + items.wrappedValue[group] = [node] + } else { + items.wrappedValue[group]?.append(node) + } + } + } + + public func removeNode(id: UUID, group: NodeGroup) { + $items.mutate { items in + guard + let index = items.wrappedValue[group]?.firstIndex(where: { $0.id == id }) + else { return } + + switch items.wrappedValue[group]?[safe: index]?.type { + case .default: + items.wrappedValue[group]?[index].type = .default(isHidden: true) + case .custom: + items.wrappedValue[group]?.remove(at: index) + case .none: + break + } + } + } + + public func updateNode(id: UUID, group: NodeGroup, mutate: (inout Node) -> Void) { + $items.mutate { items in + guard + let index = items.wrappedValue[group]?.firstIndex(where: { $0.id == id }), + var node = items.wrappedValue[group]?[safe: index] + else { return } + + let previousValue = node + mutate(&node) + + if !node.isEnabled { + node.connectionStatus = nil + } + + switch node.type { + case .default: + guard !previousValue.isSame(node) else { break } + node.type = .custom + case .custom: + break + } + + guard node != previousValue else { return } + items.wrappedValue[group]?[index] = node + } + } + + public func resetNodes(_ groups: Set) { + let defaultNodes = defaultNodes(groups) + + $items.mutate { items in + for group in groups { + items.wrappedValue[group] = defaultNodes[group] ?? .init() + } + } + } + + public init( + securedStore: SecuredStore, + nodesMergingService: NodesMergingServiceProtocol, + defaultNodes: @escaping DefaultNodesGetter + ) { + self.securedStore = securedStore + self.defaultNodes = defaultNodes + + let dto: NodesKeychainDTO? = securedStore.get(StoreKey.NodesStorage.nodes) + + let savedNodes = dto?.data.values.mapValues { $0.map { $0.mapToModel() } } + ?? migrateOldNodesData(securedStore: securedStore) + ?? .init() + + _items = .init(.init(wrappedValue: nodesMergingService.merge( + savedNodes: savedNodes, + defaultNodes: defaultNodes(.init(NodeGroup.allCases)) + ))) + + subscription = items.removeDuplicates().sink { [weak self] in + guard let self = self else { return } + saveNodes(nodes: $0) + } + } +} + +private extension NodesStorage { + func saveNodes(nodes: [NodeGroup: [Node]]) { + let nodesDto = NodesKeychainDTO(nodes.mapValues { $0.map { $0.mapToDto() } }) + securedStore.set(nodesDto, for: StoreKey.NodesStorage.nodes) + } +} + +private func migrateOldNodesData(securedStore: SecuredStore) -> [NodeGroup: [Node]]? { + let dto: SafeDecodingArray? = securedStore.get(StoreKey.NodesStorage.nodes) + guard let dto = dto else { return nil } + var result: [NodeGroup: [Node]] = [:] + + dto.forEach { + if result[$0.group] == nil { + result[$0.group] = [] + } + + result[$0.group]?.append($0.node.mapToModernDto(group: $0.group).mapToModel()) + } + + return result +} + +private extension Node { + var isHidden: Bool { + switch type { + case let .default(isHidden): + return isHidden + case .custom: + return false + } + } +} diff --git a/LiskKit/Sources/API/Service/Models/ServiceTransactionModel.swift b/LiskKit/Sources/API/Service/Models/ServiceTransactionModel.swift index a8fa3a812..a06b98862 100644 --- a/LiskKit/Sources/API/Service/Models/ServiceTransactionModel.swift +++ b/LiskKit/Sources/API/Service/Models/ServiceTransactionModel.swift @@ -84,6 +84,7 @@ public struct ServiceTransactionModel: APIModel { public struct Params: APIModel { public let amount: String public let recipientAddress: String + public let data: String? } public let id: String diff --git a/LiskKit/Sources/API/Service/Service.swift b/LiskKit/Sources/API/Service/Service.swift index 8624f755b..27f2d835d 100644 --- a/LiskKit/Sources/API/Service/Service.swift +++ b/LiskKit/Sources/API/Service/Service.swift @@ -75,48 +75,6 @@ extension Service { 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 { - case .success(response: let value): - completionHandler(.success(response: value.data)) - case .error(response: let error): - completionHandler(.error(response: error)) - } - } - return - } - if version == .v2 { - transactionsV2(id: id, block: block, sender: sender, recipient: recipient, senderIdOrRecipientId: senderIdOrRecipientId, limit: limit, offset: offset, sort: sort) { result in - 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, - isOutgoing: $0.senderId.lowercased() == ownerAddress?.lowercased(), - nonce: $0.nonce, - executionStatus: $0.executionStatus - ) - } - completionHandler(.success(response: transaction)) - case .error(response: let error): - completionHandler(.error(response: error)) - } - } - } - if version == .v3 { transactionsV3( id: id, @@ -147,7 +105,8 @@ extension Service { confirmations: $0.confirmations, isOutgoing: $0.senderId.lowercased() == ownerAddress?.lowercased(), nonce: $0.nonce, - executionStatus: $0.executionStatus + executionStatus: $0.executionStatus, + txData: $0.params.data ) } completionHandler(.success(response: transaction)) @@ -158,34 +117,6 @@ extension Service { } } - private func transactionsV1(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 (Response) -> Void) { - var options: RequestOptions = [:] - if let value = id { options["id"] = value } - if let value = block { options["blockId"] = value } - if let value = limit { options["limit"] = value } - if let value = offset { options["offset"] = value } - if let value = sort?.value { options["sort"] = value } - if let value = sender { options["senderId"] = value } - if let value = recipient { options["recipientId"] = value } - if let value = senderIdOrRecipientId { options["senderIdOrRecipientId"] = value } - - client.get(path: "\(Version.v1.rawValue)/transactions", options: options, completionHandler: completionHandler) - } - - private func transactionsV2(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 (Response) -> Void) { - var options: RequestOptions = [:] - if let value = id { options["transactionId"] = value } - if let value = block { options["blockId"] = value } - if let value = limit { options["limit"] = value } - if let value = offset { options["offset"] = value } - if let value = sort?.value { options["sort"] = value } - if let value = sender { options["senderAddress"] = value } - if let value = recipient { options["recipientAddress"] = value } - if let value = senderIdOrRecipientId { options["address"] = value } - - client.get(path: "\(Version.v2.rawValue)/transactions", options: options, completionHandler: completionHandler) - } - private func transactionsV3( id: String? = nil, block: String? = nil, diff --git a/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift b/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift index 4a2d5d4fd..01708120a 100644 --- a/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift +++ b/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift @@ -57,6 +57,8 @@ extension Transactions { public var executionStatus: ExecutionStatus + public var txData: String? + // MARK: - Hashable public static func == (lhs: TransactionModel, rhs: TransactionModel) -> Bool { diff --git a/MessageNotificationContentExtension/Debug.entitlements b/MessageNotificationContentExtension/Debug.entitlements index 6aa188f4e..630574fd2 100644 --- a/MessageNotificationContentExtension/Debug.entitlements +++ b/MessageNotificationContentExtension/Debug.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.adamant.adamant-messenger + com.apple.security.network.client keychain-access-groups diff --git a/MessageNotificationContentExtension/NotificationViewController.swift b/MessageNotificationContentExtension/NotificationViewController.swift index 47d1ae2e5..07e7cf269 100644 --- a/MessageNotificationContentExtension/NotificationViewController.swift +++ b/MessageNotificationContentExtension/NotificationViewController.swift @@ -18,6 +18,10 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi private let passphraseStoreKey = "accountService.passphrase" private let sizeWithoutMessageLabel: CGFloat = 123.0 + private lazy var securedStore: SecureStorageProtocol = { + AdamantSecureStorage() + }() + // MARK: - IBOutlets @IBOutlet weak var senderAvatarImageView: UIImageView! @@ -42,9 +46,14 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi func didReceive(_ notification: UNNotification) { // MARK: 0. Necessary services let avatarService = AdamantAvatarService() - var keychainStore: KeychainStore? - var extensionApi: ExtensionsApi? - var nativeCore: NativeAdamantCore? + let keychainStore = KeychainStore(secureStorage: securedStore) + let nativeCore = NativeAdamantCore() + + let extensionApi = ExtensionsApiFactory( + core: nativeCore, + securedStore: keychainStore + ).make() + var keypair: Keypair? // MARK: 1. Get the transaction @@ -56,13 +65,8 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi showError() return } - - let store = KeychainStore() - let api = ExtensionsApi(keychainStore: store) - trs = api.getTransaction(by: id) - - keychainStore = store - extensionApi = api + + trs = extensionApi.getTransaction(by: id) } guard let transaction = trs else { @@ -76,13 +80,10 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi if let raw = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.decodedMessage] as? String { message = raw } else { - let keychainStore = keychainStore ?? KeychainStore() - nativeCore = NativeAdamantCore() - guard let passphrase: String = keychainStore.get(passphraseStoreKey), - let keys = nativeCore!.createKeypairFor(passphrase: passphrase), + let keys = nativeCore.createKeypairFor(passphrase: passphrase), let chat = transaction.asset.chat, - let raw = nativeCore!.decodeMessage(rawMessage: chat.message, + let raw = nativeCore.decodeMessage(rawMessage: chat.message, rawNonce: chat.ownMessage, senderPublicKey: transaction.senderPublicKey, privateKey: keys.privateKey) else { @@ -107,14 +108,11 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi } // No name, no flag - something broke. Check sender name, if we have a recipient address else if let recipient = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.pushRecipient] as? String { - let keychain = keychainStore ?? KeychainStore() - let core = nativeCore ?? NativeAdamantCore() - let api: ExtensionsApi = extensionApi ?? ExtensionsApi(keychainStore: keychain) let key: Keypair? if let keypair = keypair { key = keypair - } else if let passphrase: String = keychain.get(passphraseStoreKey), let keypair = core.createKeypairFor(passphrase: passphrase) { + } else if let passphrase: String = keychainStore.get(passphraseStoreKey), let keypair = nativeCore.createKeypairFor(passphrase: passphrase) { key = keypair } else { key = nil @@ -122,7 +120,7 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi let id = transaction.senderId if let key = key { - checkName(of: id, for: recipient, api: api, core: core, keypair: key) + checkName(of: id, for: recipient, api: extensionApi, core: nativeCore, keypair: key) } senderName = nil diff --git a/MessageNotificationContentExtension/Release.entitlements b/MessageNotificationContentExtension/Release.entitlements index 155317021..125fd8842 100644 --- a/MessageNotificationContentExtension/Release.entitlements +++ b/MessageNotificationContentExtension/Release.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.adamant.adamant-messenger + com.apple.security.network.client keychain-access-groups diff --git a/NotificationServiceExtension/Debug.entitlements b/NotificationServiceExtension/Debug.entitlements index 6aa188f4e..630574fd2 100644 --- a/NotificationServiceExtension/Debug.entitlements +++ b/NotificationServiceExtension/Debug.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.adamant.adamant-messenger + com.apple.security.network.client keychain-access-groups diff --git a/NotificationServiceExtension/NotificationService.swift b/NotificationServiceExtension/NotificationService.swift index 18e9fb1a1..bc689cefd 100644 --- a/NotificationServiceExtension/NotificationService.swift +++ b/NotificationServiceExtension/NotificationService.swift @@ -18,6 +18,10 @@ class NotificationService: UNNotificationServiceExtension { return AdamantProvider() }() + private lazy var securedStore: SecuredStore = { + KeychainStore(secureStorage: AdamantSecureStorage()) + }() + /// Lazy constructors private lazy var richMessageProviders: [String: TransferNotificationContentProvider] = { var providers: [String: TransferNotificationContentProvider] = [ @@ -59,17 +63,8 @@ class NotificationService: UNNotificationServiceExtension { } // MARK: 1. Getting services - let securedStore = KeychainStore() let core = NativeAdamantCore() - let api = ExtensionsApi(keychainStore: securedStore) - - if let sound: String = securedStore.get(StoreKey.notificationsService.notificationsSound) { - if sound.isEmpty { - bestAttemptContent.sound = nil - } else { - bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(sound)) - } - } + let api = ExtensionsApiFactory(core: core, securedStore: securedStore).make() // No passphrase - no point of trying to get and decode guard @@ -116,6 +111,8 @@ class NotificationService: UNNotificationServiceExtension { var shouldIgnoreNotification = false + var isReaction = false + // MARK: 5. Content switch transaction.type { // MARK: Messages @@ -238,6 +235,8 @@ class NotificationService: UNNotificationServiceExtension { attachments: nil, categoryIdentifier: AdamantNotificationCategories.message ) + + isReaction = true } // rich file reply @@ -299,6 +298,11 @@ class NotificationService: UNNotificationServiceExtension { return } + bestAttemptContent.sound = getSound( + securedStore: securedStore, + isReaction: isReaction + ) + // MARK: 6. Other configurations bestAttemptContent.threadIdentifier = partnerAddress @@ -319,6 +323,18 @@ class NotificationService: UNNotificationServiceExtension { } } + private func getSound(securedStore: SecuredStore, isReaction: Bool) -> UNNotificationSound? { + let key = isReaction + ? StoreKey.notificationsService.notificationsReactionSound + : StoreKey.notificationsService.notificationsSound + + let sound: String = securedStore.get(key) ?? .empty + + return sound.isEmpty + ? nil + : UNNotificationSound(named: UNNotificationSoundName(sound)) + } + private func handleAdamantTransfer( notificationContent: UNMutableNotificationContent, partnerAddress address: String, diff --git a/NotificationServiceExtension/Release.entitlements b/NotificationServiceExtension/Release.entitlements index 155317021..125fd8842 100644 --- a/NotificationServiceExtension/Release.entitlements +++ b/NotificationServiceExtension/Release.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.adamant.adamant-messenger + com.apple.security.network.client keychain-access-groups diff --git a/Podfile.lock b/Podfile.lock index 22fee2d0a..4e31a8ce2 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -21,4 +21,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: a30619b79caa4b5a7497b0600d449f34b5620eec -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/TransferNotificationContentExtension/Debug.entitlements b/TransferNotificationContentExtension/Debug.entitlements index 6aa188f4e..630574fd2 100644 --- a/TransferNotificationContentExtension/Debug.entitlements +++ b/TransferNotificationContentExtension/Debug.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.adamant.adamant-messenger + com.apple.security.network.client keychain-access-groups diff --git a/TransferNotificationContentExtension/NotificationViewController.swift b/TransferNotificationContentExtension/NotificationViewController.swift index 952281d2f..fcb42c158 100644 --- a/TransferNotificationContentExtension/NotificationViewController.swift +++ b/TransferNotificationContentExtension/NotificationViewController.swift @@ -22,6 +22,10 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi return AdamantProvider() }() + private lazy var keychain: SecuredStore = { + KeychainStore(secureStorage: AdamantSecureStorage()) + }() + /// Lazy contstructors private lazy var richMessageProviders: [String: TransferNotificationContentProvider] = { var providers: [String: TransferNotificationContentProvider] = [ @@ -119,12 +123,13 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi func didReceive(_ notification: UNNotification) { // MARK: 0. Services - let keychain = KeychainStore() let core = NativeAdamantCore() let avatarService = AdamantAvatarService() - var api: ExtensionsApi? + let api = ExtensionsApiFactory(core: core, securedStore: keychain).make() - guard let passphrase: String = keychain.get(passphraseStoreKey), let keypair = core.createKeypairFor(passphrase: passphrase) else { + guard let passphrase: String = keychain.get(passphraseStoreKey), + let keypair = core.createKeypairFor(passphrase: passphrase) + else { showError() return } @@ -139,8 +144,7 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi return } - api = ExtensionsApi(keychainStore: keychain) - trs = api!.getTransaction(by: id) + trs = api.getTransaction(by: id) } guard let transaction = trs else { @@ -233,8 +237,7 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi else if let flag = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.partnerNoDislpayNameKey] as? String, flag == AdamantNotificationUserInfoKeys.partnerNoDisplayNameValue { senderName = nil } else { - let _api = api ?? ExtensionsApi(keychainStore: keychain) - checkName(of: transaction.senderId, for: transaction.recipientId, api: _api, core: core, keypair: keypair) + checkName(of: transaction.senderId, for: transaction.recipientId, api: api, core: core, keypair: keypair) senderName = nil } diff --git a/TransferNotificationContentExtension/Release.entitlements b/TransferNotificationContentExtension/Release.entitlements index 155317021..125fd8842 100644 --- a/TransferNotificationContentExtension/Release.entitlements +++ b/TransferNotificationContentExtension/Release.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.adamant.adamant-messenger + com.apple.security.network.client keychain-access-groups 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 95b8b354c..beda64862 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuViewController.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuViewController.swift @@ -246,7 +246,7 @@ extension AMenuViewController: UITableViewDelegate, UITableViewDataSource { cell.configure( with: menuItem, - accentColor: .adamant.contextMenuTextColor, + accentColor: .adamant.textColor, backgroundColor: .adamant.contextMenuDefaultBackgroundColor, font: font, rowPosition: rowPosition