diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 4a61a7729c..cf3af5fd24 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -104,7 +104,7 @@ 2AC7BC7E2731604C001D99B0 /* ChainAccountChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC7D2731604B001D99B0 /* ChainAccountChanged.swift */; }; 2AC7BC8027319FC7001D99B0 /* BalanceLockType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC7F27319FC7001D99B0 /* BalanceLockType.swift */; }; 2AC7BC822731A1A1001D99B0 /* BalanceLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC812731A1A1001D99B0 /* BalanceLock.swift */; }; - 2AC7BC842731A214001D99B0 /* BalanceLocks+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC832731A214001D99B0 /* BalanceLocks+Sort.swift */; }; + 2AC7BC842731A214001D99B0 /* AssetLocks+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC832731A214001D99B0 /* AssetLocks+Sort.swift */; }; 2AC7BC8927343484001D99B0 /* BottomSheetInfoBalanceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC8827343483001D99B0 /* BottomSheetInfoBalanceCell.swift */; }; 2AC7BC8B273435CE001D99B0 /* BottomSheetInfoTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC8A273435CE001D99B0 /* BottomSheetInfoTableCell.swift */; }; 2AD0A16A25D3854700312428 /* TransferConfirmCommandProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD0A16925D3854700312428 /* TransferConfirmCommandProxy.swift */; }; @@ -127,6 +127,7 @@ 2F6FA089995FD12FB2AA814B /* ParitySignerWelcomePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45CB342D1F680257A548CF5 /* ParitySignerWelcomePresenter.swift */; }; 2F95EEA6CBFDF483124ECF8F /* ParaStkUnstakePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CBA1296E4C6E04EC9C5CA98 /* ParaStkUnstakePresenter.swift */; }; 2FCB062A2D873BD72B795DB3 /* AssetSelectionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A0A5EE9BE2862B085712A0 /* AssetSelectionPresenter.swift */; }; + 30413A3C5ADB96B7D663F94D /* LocksWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30E541992BF608923DABE5F /* LocksWireframe.swift */; }; 30542C0BD486FD1583F36BA2 /* LedgerNetworkSelectionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B62C2CBCFF1865A1CA0F1B4 /* LedgerNetworkSelectionProtocols.swift */; }; 3086C94FE01CDFC4F79A9D7F /* DAppAuthConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21BCBFCBE606A354CB652289 /* DAppAuthConfirmViewController.swift */; }; 3133215566E418F40844A60E /* ExportMnemonicWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ACA4A5B186EE6D40BFE9D66 /* ExportMnemonicWireframe.swift */; }; @@ -691,10 +692,8 @@ 8438E1D224BFAAD2001BDB13 /* JSONRPCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8438E1D124BFAAD2001BDB13 /* JSONRPCTests.swift */; }; 843910B0253ED36C00E3C217 /* ChainStorageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910AF253ED36C00E3C217 /* ChainStorageItem.swift */; }; 843910B2253ED4D100E3C217 /* CDChainStorageItem+CoreDataDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910B1253ED4D100E3C217 /* CDChainStorageItem+CoreDataDecodable.swift */; }; - 843910B4253EE52100E3C217 /* WalletBalanceChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910B3253EE52100E3C217 /* WalletBalanceChanged.swift */; }; 843910B6253EE62B00E3C217 /* DataProviderChange+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910B5253EE62B00E3C217 /* DataProviderChange+Result.swift */; }; 843910B9253EFB8100E3C217 /* StorageKeyFactory+Implicit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910B8253EFB8100E3C217 /* StorageKeyFactory+Implicit.swift */; }; - 843910BB253F021E00E3C217 /* WalletStakingChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910BA253F021E00E3C217 /* WalletStakingChanged.swift */; }; 843910C1253F36F300E3C217 /* BaseStorageChildSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910C0253F36F300E3C217 /* BaseStorageChildSubscription.swift */; }; 843910C3253F39B100E3C217 /* WalletNetworkFacade+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910C2253F39B100E3C217 /* WalletNetworkFacade+Storage.swift */; }; 843910C5253F561500E3C217 /* CompoundOperationWrapper+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910C4253F561500E3C217 /* CompoundOperationWrapper+Result.swift */; }; @@ -1123,6 +1122,10 @@ 847999B628894FE200D1BAD2 /* AccountInputViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847999B528894FE200D1BAD2 /* AccountInputViewDelegate.swift */; }; 847999B82889510C00D1BAD2 /* TextInputViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847999B72889510C00D1BAD2 /* TextInputViewDelegate.swift */; }; 8479F31426CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8479F31326CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift */; }; + 847A25B928D7BB1F006AC9F5 /* BalancesTransferEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25B828D7BB1F006AC9F5 /* BalancesTransferEvent.swift */; }; + 847A25BB28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25BA28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift */; }; + 847A25BD28D7C0E7006AC9F5 /* TokenTransferedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25BC28D7C0E7006AC9F5 /* TokenTransferedEvent.swift */; }; + 847A25BF28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25BE28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift */; }; 847A6C0928817DC700477F77 /* AssetListBaseInteractorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A6C0828817DC700477F77 /* AssetListBaseInteractorProtocol.swift */; }; 847A6C0B28817E4000477F77 /* AssetListBaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A6C0A28817E4000477F77 /* AssetListBaseInteractor.swift */; }; 847ABE3128532E1B00851218 /* ConsesusType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847ABE3028532E1B00851218 /* ConsesusType.swift */; }; @@ -1525,6 +1528,7 @@ 84B018AC26E01A4100C75E28 /* StakingStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B018AB26E01A4100C75E28 /* StakingStateView.swift */; }; 84B018AE26E03FB500C75E28 /* NominatorStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B018AD26E03FB500C75E28 /* NominatorStateView.swift */; }; 84B018B026E0450F00C75E28 /* ValidatorStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B018AF26E0450F00C75E28 /* ValidatorStateView.swift */; }; + 84B28FC428C54441007A1006 /* OnChainTransferAmount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B28FC328C54441007A1006 /* OnChainTransferAmount.swift */; }; 84B5DE53283F7BE500193ED3 /* CollatorsSortType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B5DE52283F7BE500193ED3 /* CollatorsSortType.swift */; }; 84B5DE56283F7C8500193ED3 /* CollatorSelectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B5DE55283F7C8500193ED3 /* CollatorSelectionCell.swift */; }; 84B5DE59283F8B5400193ED3 /* CollatorSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B5DE58283F8B5400193ED3 /* CollatorSelectionViewModel.swift */; }; @@ -1543,7 +1547,6 @@ 84B73AD6279B4E0B0071AE16 /* AssetDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73AD5279B4E0B0071AE16 /* AssetDetails.swift */; }; 84B73AD8279C2EDA0071AE16 /* AssetAccountSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73AD7279C2EDA0071AE16 /* AssetAccountSubscription.swift */; }; 84B73ADA279D265A0071AE16 /* AssetsSubscriptionHandlingFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73AD9279D265A0071AE16 /* AssetsSubscriptionHandlingFactory.swift */; }; - 84B73ADC279D7C100071AE16 /* WalletNetworkFacade+BalanceLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73ADB279D7C0F0071AE16 /* WalletNetworkFacade+BalanceLocks.swift */; }; 84B73ADE279D90BD0071AE16 /* AssetsTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73ADD279D90BD0071AE16 /* AssetsTransfer.swift */; }; 84B73AE0279E6A600071AE16 /* FeeMetadataContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73ADF279E6A600071AE16 /* FeeMetadataContext.swift */; }; 84B73AE2279E95810071AE16 /* TransferInfoContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73AE1279E95810071AE16 /* TransferInfoContext.swift */; }; @@ -1601,7 +1604,6 @@ 84B7C741289BFA79001A3566 /* AccountConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6F8289BFA79001A3566 /* AccountConfirmTests.swift */; }; 84B7C742289BFA79001A3566 /* AssetSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6FA289BFA79001A3566 /* AssetSelectionTests.swift */; }; 84B7C743289BFA79001A3566 /* AnalyticsRewardDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6FC289BFA79001A3566 /* AnalyticsRewardDetailsTests.swift */; }; - 84B7C744289BFA79001A3566 /* BalanceLocksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C700289BFA79001A3566 /* BalanceLocksTests.swift */; }; 84B7C745289BFA79001A3566 /* AssetsManageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C702289BFA79001A3566 /* AssetsManageTests.swift */; }; 84B7C746289BFA79001A3566 /* WalletHistoryFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C704289BFA79001A3566 /* WalletHistoryFilterTests.swift */; }; 84B7C747289BFA79001A3566 /* AccountManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C706289BFA79001A3566 /* AccountManagementTests.swift */; }; @@ -1959,7 +1961,7 @@ 84F13F1426F20AA2006725FF /* StakingSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F13F1326F20AA2006725FF /* StakingSettingsTests.swift */; }; 84F13F1C26F2B8C2006725FF /* JSONRPCError+Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F13F1B26F2B8C1006725FF /* JSONRPCError+Presentable.swift */; }; 84F18D4A27A1869E00CA7554 /* OrmlAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F18D4927A1869E00CA7554 /* OrmlAccount.swift */; }; - 84F18D4C27A1874000CA7554 /* OrmlAccountSubcriptionHandlingFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F18D4B27A1874000CA7554 /* OrmlAccountSubcriptionHandlingFactory.swift */; }; + 84F18D4C27A1874000CA7554 /* TokenSubscriptionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F18D4B27A1874000CA7554 /* TokenSubscriptionFactory.swift */; }; 84F18D4E27A18C1400CA7554 /* OrmlAccountSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F18D4D27A18C1400CA7554 /* OrmlAccountSubscription.swift */; }; 84F1A0712869DA51007DB053 /* AssetBalanceExistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F1A0702869DA51007DB053 /* AssetBalanceExistence.swift */; }; 84F1A073286A5DDA007DB053 /* CommonRetryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F1A072286A5DDA007DB053 /* CommonRetryable.swift */; }; @@ -2091,6 +2093,14 @@ 85A093F6055DDD2E2E9253F2 /* ControllerAccountProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F829E7F8B39EE7D977001510 /* ControllerAccountProtocols.swift */; }; 86EB789787B731691B36C827 /* OnChainTransferSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A2F7E5E278FDCC89FE097 /* OnChainTransferSetupPresenter.swift */; }; 87F7556E02F6F5BB6F1B1AEA /* ParitySignerTxQrViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A5DCA28ABF42D342BBDF9A /* ParitySignerTxQrViewLayout.swift */; }; + 880855ED28D062A9004255E7 /* Array+AddOrReplace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855EC28D062A9004255E7 /* Array+AddOrReplace.swift */; }; + 880855F028D099F2004255E7 /* CrowdloanOnChainSyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855EF28D099F2004255E7 /* CrowdloanOnChainSyncService.swift */; }; + 880855F228D09A0B004255E7 /* CrowdloanContributionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855F128D09A0B004255E7 /* CrowdloanContributionData.swift */; }; + 880855F428D09A26004255E7 /* RemoteCrowdloanContribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855F328D09A26004255E7 /* RemoteCrowdloanContribution.swift */; }; + 880855F628D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855F528D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift */; }; + 880855F828D09DA8004255E7 /* CrowdloanContributionDataMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855F728D09DA8004255E7 /* CrowdloanContributionDataMapper.swift */; }; + 880855FA28D0BAA2004255E7 /* CrowdloanContributionLocalSubscriptionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855F928D0BAA2004255E7 /* CrowdloanContributionLocalSubscriptionFactory.swift */; }; + 880855FC28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855FB28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift */; }; 8828C05828B4A67000555CB6 /* Prism.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8828C05728B4A67000555CB6 /* Prism.swift */; }; 8828C05A28B4A6A800555CB6 /* Samples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8828C05928B4A6A800555CB6 /* Samples.swift */; }; 8828F4F328AD2734009E0B7C /* CrowdloansCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8828F4F228AD2734009E0B7C /* CrowdloansCalculator.swift */; }; @@ -2098,10 +2108,15 @@ 882A5CED28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882A5CEC28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift */; }; 882A5CEF28AFCE6000D0D798 /* FormattedReturnInIntervalsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882A5CEE28AFCE6000D0D798 /* FormattedReturnInIntervalsViewModel.swift */; }; 882AA13028AE64DC0093BC63 /* CrowdloanYourContributionsTotalCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882AA12F28AE64DC0093BC63 /* CrowdloanYourContributionsTotalCell.swift */; }; + 882C29AA28DC7B3D009CA4B6 /* SubstrateStorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882C29A928DC7B3D009CA4B6 /* SubstrateStorageMigrator.swift */; }; + 882C29AC28DC7B7F009CA4B6 /* SubstrateStorageVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882C29AB28DC7B7F009CA4B6 /* SubstrateStorageVersion.swift */; }; + 882C29AE28DC7CB4009CA4B6 /* StorageMigrating+CheckVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882C29AD28DC7CB4009CA4B6 /* StorageMigrating+CheckVersion.swift */; }; + 8831F10028C65B95009F7682 /* AssetLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8831F0FF28C65B95009F7682 /* AssetLock.swift */; }; 8836AF4428AA293500A94EDD /* CurrencyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8836AF4328AA293500A94EDD /* CurrencyManagerTests.swift */; }; 8836AF4828AA49AB00A94EDD /* Currency+btc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8836AF4728AA49AB00A94EDD /* Currency+btc.swift */; }; 8836AF4A28AA4B9300A94EDD /* CurrencyRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8836AF4928AA4B9300A94EDD /* CurrencyRepositoryTests.swift */; }; 8836AF4D28AA515900A94EDD /* currencies.json in Resources */ = {isa = PBXBuildFile; fileRef = 8836AF4B28AA4E4800A94EDD /* currencies.json */; }; + 884048D428C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884048D328C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift */; }; 8842104C289BBA6400306F2C /* currencies.json in Resources */ = {isa = PBXBuildFile; fileRef = 8842104B289BBA6400306F2C /* currencies.json */; }; 88421055289BBA8D00306F2C /* CurrencyViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8842104E289BBA8D00306F2C /* CurrencyViewLayout.swift */; }; 88421056289BBA8D00306F2C /* CurrencyPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8842104F289BBA8D00306F2C /* CurrencyPresenter.swift */; }; @@ -2130,23 +2145,42 @@ 8887813C28B62B0A00E7290F /* FlexibleSpaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8887813B28B62B0A00E7290F /* FlexibleSpaceView.swift */; }; 8887813E28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8887813D28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift */; }; 8887814028B7AAB700E7290F /* RoundedIconTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8887813F28B7AAB700E7290F /* RoundedIconTitleView.swift */; }; + 8890E51628DDC98C001D3994 /* SubstrateStorageMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8890E51528DDC98C001D3994 /* SubstrateStorageMigrationTests.swift */; }; + 88A0C52128D49A090083A524 /* CrowdloanOffChainSyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A0C52028D49A090083A524 /* CrowdloanOffChainSyncService.swift */; }; 88A0E0FF28A284C700A9C940 /* SelectedCurrencyDepending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A0E0FC28A284C700A9C940 /* SelectedCurrencyDepending.swift */; }; 88A0E10028A284C700A9C940 /* CurrencyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A0E0FD28A284C700A9C940 /* CurrencyManager.swift */; }; 88A0E10128A284C800A9C940 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A0E0FE28A284C700A9C940 /* Observable.swift */; }; 88A5317B28B9149600AF18F5 /* UIImage+DrawableIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A5317A28B9149600AF18F5 /* UIImage+DrawableIcon.swift */; }; 88A5317D28B9170100AF18F5 /* NSCollectionLayoutSection+create.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A5317C28B9170100AF18F5 /* NSCollectionLayoutSection+create.swift */; }; 88A5318028B9328E00AF18F5 /* YourWalletsViewSectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A5317F28B9328E00AF18F5 /* YourWalletsViewSectionModel.swift */; }; + 88A6BCFF28CA15400047E4C2 /* LocksBalanceViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A6BCFE28CA15400047E4C2 /* LocksBalanceViewModelFactory.swift */; }; + 88A6BD0128CA15710047E4C2 /* LocksViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A6BD0028CA15710047E4C2 /* LocksViewInput.swift */; }; 88AA0FB828B60E6A00931800 /* YourWalletsControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AA0FB728B60E6A00931800 /* YourWalletsControlView.swift */; }; + 88AC186128CA3EE100892A9B /* LocksViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AC186028CA3EE100892A9B /* LocksViewLayout.swift */; }; + 88AC186328CA3F0000892A9B /* GenericCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AC186228CA3F0000892A9B /* GenericCollectionViewLayout.swift */; }; + 88AC186528CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AC186428CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift */; }; + 88AF35DE28C21D28003730DA /* LocksSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AF35DD28C21D28003730DA /* LocksSubscription.swift */; }; + 88BB21A028D34C660019C6B4 /* DataProviderChange+Identifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88BB219F28D34C660019C6B4 /* DataProviderChange+Identifier.swift */; }; + 88C017E628C60A65003B2D28 /* AssetLockMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C017E528C60A65003B2D28 /* AssetLockMapper.swift */; }; + 88C7165428C894510015D1E9 /* CollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165328C894510015D1E9 /* CollectionViewDelegate.swift */; }; + 88C7165628C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165528C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift */; }; + 88C7165828C8D3280015D1E9 /* LockCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165728C8D3270015D1E9 /* LockCollectionViewCell.swift */; }; + 88C7165A28C8D3450015D1E9 /* LocksHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165928C8D3450015D1E9 /* LocksHeaderView.swift */; }; + 88CD321028E2137300542F0D /* CrowdloanContributionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CD320F28E2137200542F0D /* CrowdloanContributionId.swift */; }; 88D997AE28AB86FE006135A5 /* YourContributionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D997AD28AB86FD006135A5 /* YourContributionsView.swift */; }; 88D997B028ABC8C0006135A5 /* BlurredTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D997AF28ABC8C0006135A5 /* BlurredTableViewCell.swift */; }; 88D997B228ABC90E006135A5 /* AboutCrowdloansView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D997B128ABC90E006135A5 /* AboutCrowdloansView.swift */; }; 88E1E896289C021F00C123A8 /* CurrencyCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E1E895289C021F00C123A8 /* CurrencyCollectionViewCell.swift */; }; 88E1E898289C024400C123A8 /* UIView+Create.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E1E897289C024400C123A8 /* UIView+Create.swift */; }; + 88E8CF5E28E3789600C90112 /* CrowdloanEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E8CF5D28E3789600C90112 /* CrowdloanEmptyView.swift */; }; + 88F19DDE28D8D0A100F6E459 /* Either.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F19DDD28D8D0A100F6E459 /* Either.swift */; }; + 88F19DE028D8D0F600F6E459 /* LoadableViewModelState+Addition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F19DDF28D8D0F600F6E459 /* LoadableViewModelState+Addition.swift */; }; 88F3A9FB9CEA464275F1115E /* ExportMnemonicViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47759907380BE9300E54DC78 /* ExportMnemonicViewFactory.swift */; }; 88F7716028BEA589008C028A /* YourWalletsIconDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F7715F28BEA589008C028A /* YourWalletsIconDetailsView.swift */; }; 88F7716428BF6B59008C028A /* GenericMultiValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F7716328BF6B59008C028A /* GenericMultiValueView.swift */; }; 8916E9179CF5409E65D1B3A6 /* NftDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46EE888D60C1538A0A3EFC /* NftDetailsProtocols.swift */; }; 8A19EC93E6A6972327116D80 /* ParaStkStakeConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1139FB38E7D8D25A36726089 /* ParaStkStakeConfirmProtocols.swift */; }; + 8A23DD1F4146639EA2F7AEF6 /* LocksViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81B239BD9C150BFE9A82B0 /* LocksViewFactory.swift */; }; 8AEF593AFE8F59F7DC0A5753 /* CustomValidatorListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365CAE2753E7D5F9B9DB7D1F /* CustomValidatorListInteractor.swift */; }; 8BBA871751CAB9F0A8506317 /* AnalyticsValidatorsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B896BA49EE0D4C77401D097 /* AnalyticsValidatorsWireframe.swift */; }; 8BF525D6B5DFB7CF6C03B015 /* AnalyticsValidatorsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A6977CCE5861E4F5DCFBB /* AnalyticsValidatorsViewController.swift */; }; @@ -2196,6 +2230,7 @@ 9D5926790B055C56FB74B282 /* AccountManagementProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5072E250B7277F605855B3 /* AccountManagementProtocols.swift */; }; 9DFB37659A6B911A4D54623E /* AccountConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E992CCDC1D581F7E9D3F1CA /* AccountConfirmInteractor.swift */; }; 9E15912C35D50C6D738FD04C /* AccountConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5002B8FA2695F470587677D2 /* AccountConfirmProtocols.swift */; }; + 9E40464B7687006B1EE75C72 /* LocksProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F9944B0577EFF25A0643FE /* LocksProtocols.swift */; }; 9E4E458C92D12B24D5EAD893 /* ControllerAccountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748E0AF1A286016CB220155C /* ControllerAccountInteractor.swift */; }; 9F3E2D64D77BF89B474BF1E3 /* DAppOperationConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43FBC2EA83A121CEBD25549D /* DAppOperationConfirmViewController.swift */; }; 9F4A48B1BE3A1110A0CF9F36 /* ReferralCrowdloanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDDB2B35CD3299F50613141 /* ReferralCrowdloanViewController.swift */; }; @@ -2362,6 +2397,7 @@ BA7AEE82627CFC0AFD69B299 /* RecommendedValidatorListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2580363AC3E4A9CD40256E /* RecommendedValidatorListPresenter.swift */; }; BB29490A4E8472A7DB781BC4 /* TransferOnChainConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D385A8FCB0E6F3F6B6872F01 /* TransferOnChainConfirmPresenter.swift */; }; BD571417BD18C711B76E1D62 /* ExportSeedWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B4C1B5D56DB69BA0AECF731 /* ExportSeedWireframe.swift */; }; + BE301A0F2286CCEF6A02D341 /* LocksPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F9E9C70B0C14570AB783AF /* LocksPresenter.swift */; }; BE3F6213B26F35EB6324DBD8 /* ControllerAccountWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB9EDB05686DF11958145E1 /* ControllerAccountWireframe.swift */; }; BE8CF97B6EA62C75277B78AA /* MoonbeamTermsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D8F02830944DBAF72D8A41 /* MoonbeamTermsProtocols.swift */; }; BEA539EE97A287868FD8BE46 /* AssetSelectionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9622C6C3102EF12BEE78D63D /* AssetSelectionViewFactory.swift */; }; @@ -2407,6 +2443,7 @@ CDB78A5A733E4A4F1A2C48C8 /* AssetSelectionWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AD1285797131E836CD994B /* AssetSelectionWireframe.swift */; }; CDED41B125E1D5128736B933 /* ParitySignerTxScanViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CD8C618D61C78EA8C58532 /* ParitySignerTxScanViewLayout.swift */; }; CE2792E78B14CE02394D8CF4 /* ReferralCrowdloanViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 594BC61689EC942ED0A64A4A /* ReferralCrowdloanViewLayout.swift */; }; + CE4C1344F03A5132C601A594 /* LocksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3D4D2E89D40718677685CE1 /* LocksViewController.swift */; }; CE773CEC15A83AA6D0B404B8 /* DAppListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2BB29AE8E556E6756A4F02 /* DAppListViewController.swift */; }; D1C6EABB48DC3EE254E5A095 /* CrowdloanContributionConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F5B57A24265C36A5F19B78 /* CrowdloanContributionConfirmPresenter.swift */; }; D344C6DAC1F8BB6152BA8DD0 /* RecommendedValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C6573C52692E4A56E35FF9 /* RecommendedValidatorListProtocols.swift */; }; @@ -2780,6 +2817,7 @@ 256215C11DC0E091660034EA /* CrowdloanYourContributionsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanYourContributionsViewController.swift; sourceTree = ""; }; 2667181A57442FB4D93B7F36 /* ParitySignerWelcomeViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerWelcomeViewFactory.swift; sourceTree = ""; }; 26A0FFF412031C1373EBE2B8 /* ParaStkRebondProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRebondProtocols.swift; sourceTree = ""; }; + 26F9E9C70B0C14570AB783AF /* LocksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksPresenter.swift; sourceTree = ""; }; 270B309EC85D8897A4ADD98A /* CustomValidatorListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListViewController.swift; sourceTree = ""; }; 27A5489E97F846FE3D5931E5 /* ParaStkYieldBoostStopViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStopViewFactory.swift; sourceTree = ""; }; 27D5AF2F7609ADE855308089 /* AccountExportPasswordViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordViewController.swift; sourceTree = ""; }; @@ -2808,7 +2846,7 @@ 2AC7BC7D2731604B001D99B0 /* ChainAccountChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainAccountChanged.swift; sourceTree = ""; }; 2AC7BC7F27319FC7001D99B0 /* BalanceLockType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceLockType.swift; sourceTree = ""; }; 2AC7BC812731A1A1001D99B0 /* BalanceLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceLock.swift; sourceTree = ""; }; - 2AC7BC832731A214001D99B0 /* BalanceLocks+Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BalanceLocks+Sort.swift"; sourceTree = ""; }; + 2AC7BC832731A214001D99B0 /* AssetLocks+Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetLocks+Sort.swift"; sourceTree = ""; }; 2AC7BC8827343483001D99B0 /* BottomSheetInfoBalanceCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetInfoBalanceCell.swift; sourceTree = ""; }; 2AC7BC8A273435CE001D99B0 /* BottomSheetInfoTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetInfoTableCell.swift; sourceTree = ""; }; 2ACA4A5B186EE6D40BFE9D66 /* ExportMnemonicWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicWireframe.swift; sourceTree = ""; }; @@ -2991,6 +3029,7 @@ 7A092ADC09DA0429548EBC08 /* NftListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListPresenter.swift; sourceTree = ""; }; 7ACF32611D345B87BCE29FE0 /* DAppAddFavoriteWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAddFavoriteWireframe.swift; sourceTree = ""; }; 7B1A00299D9B50045E1A1983 /* DAppAddFavoriteProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAddFavoriteProtocols.swift; sourceTree = ""; }; + 7B81B239BD9C150BFE9A82B0 /* LocksViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksViewFactory.swift; sourceTree = ""; }; 7C70EBF83B2547452417E588 /* StakingRewardDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardDetailsViewController.swift; sourceTree = ""; }; 7CBA1296E4C6E04EC9C5CA98 /* ParaStkUnstakePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakePresenter.swift; sourceTree = ""; }; 7DDDB2B35CD3299F50613141 /* ReferralCrowdloanViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanViewController.swift; sourceTree = ""; }; @@ -3398,10 +3437,8 @@ 8438E1D324BFAAD2001BDB13 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 843910AF253ED36C00E3C217 /* ChainStorageItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainStorageItem.swift; sourceTree = ""; }; 843910B1253ED4D100E3C217 /* CDChainStorageItem+CoreDataDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CDChainStorageItem+CoreDataDecodable.swift"; sourceTree = ""; }; - 843910B3253EE52100E3C217 /* WalletBalanceChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletBalanceChanged.swift; sourceTree = ""; }; 843910B5253EE62B00E3C217 /* DataProviderChange+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataProviderChange+Result.swift"; sourceTree = ""; }; 843910B8253EFB8100E3C217 /* StorageKeyFactory+Implicit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageKeyFactory+Implicit.swift"; sourceTree = ""; }; - 843910BA253F021E00E3C217 /* WalletStakingChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletStakingChanged.swift; sourceTree = ""; }; 843910C0253F36F300E3C217 /* BaseStorageChildSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStorageChildSubscription.swift; sourceTree = ""; }; 843910C2253F39B100E3C217 /* WalletNetworkFacade+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletNetworkFacade+Storage.swift"; sourceTree = ""; }; 843910C4253F561500E3C217 /* CompoundOperationWrapper+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompoundOperationWrapper+Result.swift"; sourceTree = ""; }; @@ -3835,6 +3872,10 @@ 847999B528894FE200D1BAD2 /* AccountInputViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputViewDelegate.swift; sourceTree = ""; }; 847999B72889510C00D1BAD2 /* TextInputViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputViewDelegate.swift; sourceTree = ""; }; 8479F31326CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainRegistryIntegrationTests.swift; sourceTree = ""; }; + 847A25B828D7BB1F006AC9F5 /* BalancesTransferEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalancesTransferEvent.swift; sourceTree = ""; }; + 847A25BA28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExtrinsicProcessor+Events.swift"; sourceTree = ""; }; + 847A25BC28D7C0E7006AC9F5 /* TokenTransferedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenTransferedEvent.swift; sourceTree = ""; }; + 847A25BE28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountIdCodingWrapper.swift; sourceTree = ""; }; 847A6C0828817DC700477F77 /* AssetListBaseInteractorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBaseInteractorProtocol.swift; sourceTree = ""; }; 847A6C0A28817E4000477F77 /* AssetListBaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBaseInteractor.swift; sourceTree = ""; }; 847ABE3028532E1B00851218 /* ConsesusType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsesusType.swift; sourceTree = ""; }; @@ -4244,6 +4285,7 @@ 84B018AB26E01A4100C75E28 /* StakingStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingStateView.swift; sourceTree = ""; }; 84B018AD26E03FB500C75E28 /* NominatorStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominatorStateView.swift; sourceTree = ""; }; 84B018AF26E0450F00C75E28 /* ValidatorStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatorStateView.swift; sourceTree = ""; }; + 84B28FC328C54441007A1006 /* OnChainTransferAmount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnChainTransferAmount.swift; sourceTree = ""; }; 84B5DE52283F7BE500193ED3 /* CollatorsSortType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollatorsSortType.swift; sourceTree = ""; }; 84B5DE55283F7C8500193ED3 /* CollatorSelectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollatorSelectionCell.swift; sourceTree = ""; }; 84B5DE58283F8B5400193ED3 /* CollatorSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollatorSelectionViewModel.swift; sourceTree = ""; }; @@ -4262,7 +4304,6 @@ 84B73AD5279B4E0B0071AE16 /* AssetDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetDetails.swift; sourceTree = ""; }; 84B73AD7279C2EDA0071AE16 /* AssetAccountSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetAccountSubscription.swift; sourceTree = ""; }; 84B73AD9279D265A0071AE16 /* AssetsSubscriptionHandlingFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsSubscriptionHandlingFactory.swift; sourceTree = ""; }; - 84B73ADB279D7C0F0071AE16 /* WalletNetworkFacade+BalanceLocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletNetworkFacade+BalanceLocks.swift"; sourceTree = ""; }; 84B73ADD279D90BD0071AE16 /* AssetsTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsTransfer.swift; sourceTree = ""; }; 84B73ADF279E6A600071AE16 /* FeeMetadataContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeMetadataContext.swift; sourceTree = ""; }; 84B73AE1279E95810071AE16 /* TransferInfoContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferInfoContext.swift; sourceTree = ""; }; @@ -4320,7 +4361,6 @@ 84B7C6F8289BFA79001A3566 /* AccountConfirmTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountConfirmTests.swift; sourceTree = ""; }; 84B7C6FA289BFA79001A3566 /* AssetSelectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetSelectionTests.swift; sourceTree = ""; }; 84B7C6FC289BFA79001A3566 /* AnalyticsRewardDetailsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsRewardDetailsTests.swift; sourceTree = ""; }; - 84B7C700289BFA79001A3566 /* BalanceLocksTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceLocksTests.swift; sourceTree = ""; }; 84B7C702289BFA79001A3566 /* AssetsManageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetsManageTests.swift; sourceTree = ""; }; 84B7C704289BFA79001A3566 /* WalletHistoryFilterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterTests.swift; sourceTree = ""; }; 84B7C706289BFA79001A3566 /* AccountManagementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManagementTests.swift; sourceTree = ""; }; @@ -4681,7 +4721,7 @@ 84F13F1326F20AA2006725FF /* StakingSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSettingsTests.swift; sourceTree = ""; }; 84F13F1B26F2B8C1006725FF /* JSONRPCError+Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONRPCError+Presentable.swift"; sourceTree = ""; }; 84F18D4927A1869E00CA7554 /* OrmlAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrmlAccount.swift; sourceTree = ""; }; - 84F18D4B27A1874000CA7554 /* OrmlAccountSubcriptionHandlingFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrmlAccountSubcriptionHandlingFactory.swift; sourceTree = ""; }; + 84F18D4B27A1874000CA7554 /* TokenSubscriptionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenSubscriptionFactory.swift; sourceTree = ""; }; 84F18D4D27A18C1400CA7554 /* OrmlAccountSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrmlAccountSubscription.swift; sourceTree = ""; }; 84F1A0702869DA51007DB053 /* AssetBalanceExistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetBalanceExistence.swift; sourceTree = ""; }; 84F1A072286A5DDA007DB053 /* CommonRetryable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonRetryable.swift; sourceTree = ""; }; @@ -4815,6 +4855,14 @@ 86F7A369E31DCB9ABD556EE9 /* CrowdloanListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListPresenter.swift; sourceTree = ""; }; 86F9063B2DF46E7B65B5248E /* Pods_novawalletAll_novawalletIntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletAll_novawalletIntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 87CD8C618D61C78EA8C58532 /* ParitySignerTxScanViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxScanViewLayout.swift; sourceTree = ""; }; + 880855EC28D062A9004255E7 /* Array+AddOrReplace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+AddOrReplace.swift"; sourceTree = ""; }; + 880855EF28D099F2004255E7 /* CrowdloanOnChainSyncService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanOnChainSyncService.swift; sourceTree = ""; }; + 880855F128D09A0B004255E7 /* CrowdloanContributionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionData.swift; sourceTree = ""; }; + 880855F328D09A26004255E7 /* RemoteCrowdloanContribution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCrowdloanContribution.swift; sourceTree = ""; }; + 880855F528D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionStreamableSource.swift; sourceTree = ""; }; + 880855F728D09DA8004255E7 /* CrowdloanContributionDataMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionDataMapper.swift; sourceTree = ""; }; + 880855F928D0BAA2004255E7 /* CrowdloanContributionLocalSubscriptionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionLocalSubscriptionFactory.swift; sourceTree = ""; }; + 880855FB28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloansLocalStorageSubscriber.swift; sourceTree = ""; }; 8821119C96944A0E3526E93A /* StakingRedeemViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemViewFactory.swift; sourceTree = ""; }; 8828C05728B4A67000555CB6 /* Prism.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Prism.swift; sourceTree = ""; }; 8828C05928B4A6A800555CB6 /* Samples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Samples.swift; sourceTree = ""; }; @@ -4823,10 +4871,15 @@ 882A5CEC28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReturnInIntervalsViewModel.swift; sourceTree = ""; }; 882A5CEE28AFCE6000D0D798 /* FormattedReturnInIntervalsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormattedReturnInIntervalsViewModel.swift; sourceTree = ""; }; 882AA12F28AE64DC0093BC63 /* CrowdloanYourContributionsTotalCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanYourContributionsTotalCell.swift; sourceTree = ""; }; + 882C29A928DC7B3D009CA4B6 /* SubstrateStorageMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateStorageMigrator.swift; sourceTree = ""; }; + 882C29AB28DC7B7F009CA4B6 /* SubstrateStorageVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateStorageVersion.swift; sourceTree = ""; }; + 882C29AD28DC7CB4009CA4B6 /* StorageMigrating+CheckVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageMigrating+CheckVersion.swift"; sourceTree = ""; }; + 8831F0FF28C65B95009F7682 /* AssetLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetLock.swift; sourceTree = ""; }; 8836AF4328AA293500A94EDD /* CurrencyManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyManagerTests.swift; sourceTree = ""; }; 8836AF4728AA49AB00A94EDD /* Currency+btc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Currency+btc.swift"; sourceTree = ""; }; 8836AF4928AA4B9300A94EDD /* CurrencyRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyRepositoryTests.swift; sourceTree = ""; }; 8836AF4B28AA4E4800A94EDD /* currencies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = currencies.json; sourceTree = ""; }; + 884048D328C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrmlTokenSubscriptionHandlingFactory.swift; sourceTree = ""; }; 8842104B289BBA6400306F2C /* currencies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = currencies.json; sourceTree = ""; }; 8842104E289BBA8D00306F2C /* CurrencyViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyViewLayout.swift; sourceTree = ""; }; 8842104F289BBA8D00306F2C /* CurrencyPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyPresenter.swift; sourceTree = ""; }; @@ -4846,6 +4899,7 @@ 8860F3E1289D4FFD00C0BF86 /* SectionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionProtocol.swift; sourceTree = ""; }; 8860F3E3289D50BA00C0BF86 /* Array+SectionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+SectionProtocol.swift"; sourceTree = ""; }; 8860F3E7289D7CF400C0BF86 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; + 88787F0328DB3A7B00B115AB /* SubstrateDataModel2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel2.xcdatamodel; sourceTree = ""; }; 887AFC8628BC95F0002A0422 /* MetaAccountChainResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaAccountChainResponse.swift; sourceTree = ""; }; 887AFC8928BCB313002A0422 /* PolkadotIconDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PolkadotIconDetailsView.swift; sourceTree = ""; }; 887AFC8A28BCB313002A0422 /* SelectableIconSubtitleCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectableIconSubtitleCollectionViewCell.swift; sourceTree = ""; }; @@ -4853,19 +4907,37 @@ 8887813B28B62B0A00E7290F /* FlexibleSpaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleSpaceView.swift; sourceTree = ""; }; 8887813D28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedIconTitleCollectionHeaderView.swift; sourceTree = ""; }; 8887813F28B7AAB700E7290F /* RoundedIconTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedIconTitleView.swift; sourceTree = ""; }; + 8890E51528DDC98C001D3994 /* SubstrateStorageMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateStorageMigrationTests.swift; sourceTree = ""; }; 889A825F58F5CB54118A9D35 /* SelectValidatorsStartWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartWireframe.swift; sourceTree = ""; }; + 88A0C52028D49A090083A524 /* CrowdloanOffChainSyncService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanOffChainSyncService.swift; sourceTree = ""; }; 88A0E0FC28A284C700A9C940 /* SelectedCurrencyDepending.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectedCurrencyDepending.swift; sourceTree = ""; }; 88A0E0FD28A284C700A9C940 /* CurrencyManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyManager.swift; sourceTree = ""; }; 88A0E0FE28A284C700A9C940 /* Observable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 88A5317A28B9149600AF18F5 /* UIImage+DrawableIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+DrawableIcon.swift"; sourceTree = ""; }; 88A5317C28B9170100AF18F5 /* NSCollectionLayoutSection+create.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSCollectionLayoutSection+create.swift"; sourceTree = ""; }; 88A5317F28B9328E00AF18F5 /* YourWalletsViewSectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourWalletsViewSectionModel.swift; sourceTree = ""; }; + 88A6BCFE28CA15400047E4C2 /* LocksBalanceViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksBalanceViewModelFactory.swift; sourceTree = ""; }; + 88A6BD0028CA15710047E4C2 /* LocksViewInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksViewInput.swift; sourceTree = ""; }; 88AA0FB728B60E6A00931800 /* YourWalletsControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourWalletsControlView.swift; sourceTree = ""; }; + 88AC186028CA3EE100892A9B /* LocksViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksViewLayout.swift; sourceTree = ""; }; + 88AC186228CA3F0000892A9B /* GenericCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericCollectionViewLayout.swift; sourceTree = ""; }; + 88AC186428CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalSheetCollectionViewProtocol.swift; sourceTree = ""; }; + 88AF35DD28C21D28003730DA /* LocksSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksSubscription.swift; sourceTree = ""; }; + 88BB219F28D34C660019C6B4 /* DataProviderChange+Identifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataProviderChange+Identifier.swift"; sourceTree = ""; }; + 88C017E528C60A65003B2D28 /* AssetLockMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetLockMapper.swift; sourceTree = ""; }; + 88C7165328C894510015D1E9 /* CollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewDelegate.swift; sourceTree = ""; }; + 88C7165528C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionViewDiffableDataSource+apply.swift"; sourceTree = ""; }; + 88C7165728C8D3270015D1E9 /* LockCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockCollectionViewCell.swift; sourceTree = ""; }; + 88C7165928C8D3450015D1E9 /* LocksHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksHeaderView.swift; sourceTree = ""; }; + 88CD320F28E2137200542F0D /* CrowdloanContributionId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionId.swift; sourceTree = ""; }; 88D997AD28AB86FD006135A5 /* YourContributionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YourContributionsView.swift; sourceTree = ""; }; 88D997AF28ABC8C0006135A5 /* BlurredTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurredTableViewCell.swift; sourceTree = ""; }; 88D997B128ABC90E006135A5 /* AboutCrowdloansView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutCrowdloansView.swift; sourceTree = ""; }; 88E1E895289C021F00C123A8 /* CurrencyCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyCollectionViewCell.swift; sourceTree = ""; }; 88E1E897289C024400C123A8 /* UIView+Create.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Create.swift"; sourceTree = ""; }; + 88E8CF5D28E3789600C90112 /* CrowdloanEmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrowdloanEmptyView.swift; sourceTree = ""; }; + 88F19DDD28D8D0A100F6E459 /* Either.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Either.swift; sourceTree = ""; }; + 88F19DDF28D8D0F600F6E459 /* LoadableViewModelState+Addition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoadableViewModelState+Addition.swift"; sourceTree = ""; }; 88F7715F28BEA589008C028A /* YourWalletsIconDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourWalletsIconDetailsView.swift; sourceTree = ""; }; 88F7716328BF6B59008C028A /* GenericMultiValueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericMultiValueView.swift; sourceTree = ""; }; 899686C7351A2600FFA08371 /* TransferConfirmOnChainViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmOnChainViewFactory.swift; sourceTree = ""; }; @@ -5174,6 +5246,7 @@ E20124142C4011901EF55AAA /* ParitySignerAddressesViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddressesViewLayout.swift; sourceTree = ""; }; E29DAC8F2DB0F7BF909812FA /* DAppTxDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppTxDetailsViewLayout.swift; sourceTree = ""; }; E2F3E725280823CF00CF31B5 /* ETHAccountInjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETHAccountInjection.swift; sourceTree = ""; }; + E30E541992BF608923DABE5F /* LocksWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksWireframe.swift; sourceTree = ""; }; E4C77FD258A19F08F3955AC4 /* ParaStkUnstakeConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeConfirmInteractor.swift; sourceTree = ""; }; E4E78D69E8EBC3EB4D01F8EF /* CrowdloanListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListInteractor.swift; sourceTree = ""; }; E54289A8A9354D5DDA15F0E1 /* ChangeWatchOnlyViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChangeWatchOnlyViewFactory.swift; sourceTree = ""; }; @@ -5213,6 +5286,7 @@ F2B438707EA6C81C48EAB4CE /* ParaStkYieldBoostStopViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStopViewController.swift; sourceTree = ""; }; F2B676982F60C55530BDD569 /* AccountManagementPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementPresenter.swift; sourceTree = ""; }; F31A3D4E3894582CB49013F0 /* ParaStkYieldBoostStartViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStartViewLayout.swift; sourceTree = ""; }; + F3D4D2E89D40718677685CE1 /* LocksViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksViewController.swift; sourceTree = ""; }; F400A7C1260CE1670061D576 /* StakingRewardStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardStatus.swift; sourceTree = ""; }; F402BC82273ACDC30075F803 /* AstarBonusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AstarBonusService.swift; sourceTree = ""; }; F402BC8A273AD20D0075F803 /* AstarBonusServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AstarBonusServiceError.swift; sourceTree = ""; }; @@ -5376,6 +5450,7 @@ F4F65C3726D8B86F002EE838 /* FWXAxisEmptyValueFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FWXAxisEmptyValueFormatter.swift; sourceTree = ""; }; F4F65C3C26D8B9DD002EE838 /* FWYAxisChartFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FWYAxisChartFormatter.swift; sourceTree = ""; }; F4F69E272731B0B200214542 /* CrowdloanTableHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrowdloanTableHeaderView.swift; sourceTree = ""; }; + F4F9944B0577EFF25A0643FE /* LocksProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksProtocols.swift; sourceTree = ""; }; F4FDA0F726A57626003D753B /* BabeEraOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BabeEraOperationFactory.swift; sourceTree = ""; }; F4FDA0FC26A57860003D753B /* EraCountdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraCountdown.swift; sourceTree = ""; }; F52B8815D6AF5E69B145D245 /* CustomValidatorListViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListViewFactory.swift; sourceTree = ""; }; @@ -5438,6 +5513,9 @@ 84ED3A7899C88876AB3DCA5F /* YourWalletsViewController.swift */, 6E2509FEA85677165C4CCCFF /* YourWalletsViewLayout.swift */, C9978451AB2F4958E6FF117D /* YourWalletsViewFactory.swift */, + 88C7165328C894510015D1E9 /* CollectionViewDelegate.swift */, + 88AC186228CA3F0000892A9B /* GenericCollectionViewLayout.swift */, + 88AC186428CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift */, ); path = YourWallets; sourceTree = ""; @@ -6184,6 +6262,22 @@ path = SelectValidatorsStart; sourceTree = ""; }; + 83C426015DF863EBA46F1E3E /* Locks */ = { + isa = PBXGroup; + children = ( + 88C7165B28C8D37E0015D1E9 /* View */, + F4F9944B0577EFF25A0643FE /* LocksProtocols.swift */, + E30E541992BF608923DABE5F /* LocksWireframe.swift */, + 26F9E9C70B0C14570AB783AF /* LocksPresenter.swift */, + F3D4D2E89D40718677685CE1 /* LocksViewController.swift */, + 7B81B239BD9C150BFE9A82B0 /* LocksViewFactory.swift */, + 88A6BCFE28CA15400047E4C2 /* LocksBalanceViewModelFactory.swift */, + 88A6BD0028CA15710047E4C2 /* LocksViewInput.swift */, + 88AC186028CA3EE100892A9B /* LocksViewLayout.swift */, + ); + path = Locks; + sourceTree = ""; + }; 8401620F25E144DA0087A5F3 /* AmountInputView */ = { isa = PBXGroup; children = ( @@ -6315,6 +6409,7 @@ 84155DE8253980D700A27058 /* Services */ = { isa = PBXGroup; children = ( + 880855EE28D099BD004255E7 /* CrowdloanService */, 8411707D285B15C8006F4DFB /* XcmService */, 8466781427EC9B6C007935D3 /* PersistExtrinsicService */, 841AAC2B26F7311200F0A25E /* RemoteSubscription */, @@ -6368,13 +6463,14 @@ 841E2E4D2738159400F250C1 /* RemoteSubscriptionHandlingFactory.swift */, 841E2E4F27381B2A00F250C1 /* AccountInfoSubscriptionHandlingFactory.swift */, 84B73AD9279D265A0071AE16 /* AssetsSubscriptionHandlingFactory.swift */, - 84F18D4B27A1874000CA7554 /* OrmlAccountSubcriptionHandlingFactory.swift */, + 84F18D4B27A1874000CA7554 /* TokenSubscriptionFactory.swift */, 84D1ABD927E0ACFA0073C631 /* WalletRemoteSubscriptionWrapper.swift */, 848DAEF6282274E700D56F55 /* ParachainStakingRemoteSubscriptionService.swift */, 841E553B282D44BA00C8438F /* ParachainStakingAccountSubscriptionService.swift */, 849E07F12849E70C00DE0440 /* ParaStkScheduledRequestsUpdater.swift */, 849E07F3284A04F400DE0440 /* ParaStkAccountSubscribeHandlingFactory.swift */, 842643BA2878572D0031B5B5 /* TuringStakingRemoteSubscriptionService.swift */, + 884048D328C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift */, ); path = RemoteSubscription; sourceTree = ""; @@ -6744,6 +6840,7 @@ 842EBB2C289096D300B952D8 /* Common */ = { isa = PBXGroup; children = ( + 88CD320F28E2137200542F0D /* CrowdloanContributionId.swift */, A8FC21B4670E7B22B787357D /* WalletsListProtocols.swift */, 6F404EE82BC45BFE0F42E0A4 /* WalletsListWireframe.swift */, 0D6E67AD564867E121601F18 /* WalletsListPresenter.swift */, @@ -7013,6 +7110,8 @@ 84F1CB3F27CF6BEF0095D523 /* UniquesClassDetails.swift */, 8430D6C42800040A00FFB6AE /* EthereumExecuted.swift */, 84A3B8A12836DA2600DE2669 /* LastAccountIdKeyWrapper.swift */, + 847A25B828D7BB1F006AC9F5 /* BalancesTransferEvent.swift */, + 847A25BC28D7C0E7006AC9F5 /* TokenTransferedEvent.swift */, ); path = Types; sourceTree = ""; @@ -7424,6 +7523,8 @@ 84CA68DE26BEAA0F003B9453 /* ChainModelMapper.swift */, 845B822026EF8F1A00D25C72 /* ManagedMetaAccountMapper.swift */, 849A4EF9279ABC8800AB6709 /* AssetBalanceMapper.swift */, + 88C017E528C60A65003B2D28 /* AssetLockMapper.swift */, + 880855F728D09DA8004255E7 /* CrowdloanContributionDataMapper.swift */, 8499FECB27BF8F4A00712589 /* NftModelMapper.swift */, 84F3B27727F4179A00D64CF5 /* PhishingSiteMapper.swift */, 849E07F5284A114B00DE0440 /* ParaStkScheduledRequestsMapper.swift */, @@ -7545,6 +7646,7 @@ 848CCB432832EE9B00A1FD00 /* GeneralStorageSubscriptionFactory.swift */, 842643BC28785A940031B5B5 /* TuringStakingLocalSubscriptionFactory.swift */, 84EF8D38288FCE8100265346 /* WalletListLocalSubscriptionFactory.swift */, + 880855F928D0BAA2004255E7 /* CrowdloanContributionLocalSubscriptionFactory.swift */, ); path = DataProvider; sourceTree = ""; @@ -7903,6 +8005,7 @@ 8454C26E2632BBAA00657DAD /* ExtrinsicProcessing.swift */, 849B563227A70D71007D5528 /* ExtrinsicProcessor+Fee.swift */, 849B563427A70DDE007D5528 /* ExtrinsicProcessor+Matching.swift */, + 847A25BA28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift */, 84452F9725D6728B00F47EC5 /* RuntimeVersionSubscription.swift */, 8470D6D1253E3382009E9A5D /* StorageSubscriptionContainer.swift */, 8470D6CF253E321C009E9A5D /* StorageSubscriptionProtocols.swift */, @@ -7913,6 +8016,7 @@ 84F18D4D27A18C1400CA7554 /* OrmlAccountSubscription.swift */, 84C3420C283192D000156569 /* CallbackStorageSubscription.swift */, 84FA7C1D284FED0A00B648E1 /* CallbackBatchStorageSubscription.swift */, + 88AF35DD28C21D28003730DA /* LocksSubscription.swift */, ); path = StorageSubscription; sourceTree = ""; @@ -8064,6 +8168,8 @@ children = ( 847F2D4C27AA68DD00AFD476 /* AssetListAssetModel.swift */, 847F2D4E27AA695F00AFD476 /* AssetListGroupModel.swift */, + 88F19DDD28D8D0A100F6E459 /* Either.swift */, + 88F19DDF28D8D0F600F6E459 /* LoadableViewModelState+Addition.swift */, ); path = Models; sourceTree = ""; @@ -8134,6 +8240,7 @@ isa = PBXGroup; children = ( 849ECD3426DE70B900F542A3 /* SingleToMultiassetUserMigrationTests.swift */, + 8890E51528DDC98C001D3994 /* SubstrateStorageMigrationTests.swift */, ); path = Migration; sourceTree = ""; @@ -8215,6 +8322,7 @@ children = ( 848F8B212863BD1000204BC4 /* TransferSetupInputState.swift */, 842B17FE28649CCD0014CC57 /* CrossChainDestinationSelectionState.swift */, + 84B28FC328C54441007A1006 /* OnChainTransferAmount.swift */, ); path = Model; sourceTree = ""; @@ -8405,6 +8513,7 @@ A29C55960FE9EADBDEAC6F03 /* AssetsSearch */, 486ADD5F84F3B18E6F5BC0DA /* WalletsList */, 018DE0E8A60963E2BDD94D13 /* YourWallets */, + 83C426015DF863EBA46F1E3E /* Locks */, ); path = Modules; sourceTree = ""; @@ -8496,7 +8605,6 @@ 84F13F0926F14122006725FF /* ChainAsset.swift */, 841AAC2826F6A59C00F0A25E /* MultiassetCryptoType.swift */, 2AC7BC7F27319FC7001D99B0 /* BalanceLockType.swift */, - 2AC7BC832731A214001D99B0 /* BalanceLocks+Sort.swift */, 849A4EF3279A7AC600AB6709 /* AssetType.swift */, 849A4EF5279A7AEF00AB6709 /* StateminAssetExtras.swift */, 849A4EF7279ABBDD00AB6709 /* AssetBalance.swift */, @@ -8513,6 +8621,8 @@ 84BC7044289EFF44008A9758 /* TransactionDisplayCode.swift */, 84BC7046289EFFFA008A9758 /* ChainWalletDisplayAddress.swift */, 887AFC8628BC95F0002A0422 /* MetaAccountChainResponse.swift */, + 8831F0FF28C65B95009F7682 /* AssetLock.swift */, + 2AC7BC832731A214001D99B0 /* AssetLocks+Sort.swift */, ); path = Model; sourceTree = ""; @@ -8674,6 +8784,7 @@ 88E1E897289C024400C123A8 /* UIView+Create.swift */, 88A5317A28B9149600AF18F5 /* UIImage+DrawableIcon.swift */, 88A5317C28B9170100AF18F5 /* NSCollectionLayoutSection+create.swift */, + 88C7165528C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift */, ); path = UIKit; sourceTree = ""; @@ -8718,6 +8829,7 @@ 848F8B1C2863616E00204BC4 /* ChainsStore.swift */, 849FA21528A26CB500F83EAA /* CountdownTimerMediator.swift */, 84466B3228B65B5B00FA1E0D /* MetaAccountModel+Identicon.swift */, + 847A25BE28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift */, ); path = Helpers; sourceTree = ""; @@ -8747,6 +8859,8 @@ 840DC83F288090030039A054 /* Bool+Int.swift */, 844DAAE028AD106B008E11DA /* UInt+Serialization.swift */, 84D9C8F228ADA42F007FB23B /* Data+Chunk.swift */, + 880855EC28D062A9004255E7 /* Array+AddOrReplace.swift */, + 88BB219F28D34C660019C6B4 /* DataProviderChange+Identifier.swift */, ); path = Foundation; sourceTree = ""; @@ -9272,6 +9386,7 @@ 849842EA26587AFA006BBB9F /* View */ = { isa = PBXGroup; children = ( + 88E8CF5D28E3789600C90112 /* CrowdloanEmptyView.swift */, F4F69E272731B0B200214542 /* CrowdloanTableHeaderView.swift */, 84BB3CF7267D276D00676FFE /* CrowdloanTableViewCell.swift */, 8498430226592D29006BBB9F /* CrowdloanStatusSectionView.swift */, @@ -9385,7 +9500,6 @@ isa = PBXGroup; children = ( AEE1207B26565CDB003EE80C /* WalletNetworkOperationFactory+MinimalBalance.swift */, - 84B73ADB279D7C0F0071AE16 /* WalletNetworkFacade+BalanceLocks.swift */, 8490150224AB6C01008F705E /* WalletNetworkOperationFactory.swift */, 84C7435A251DC504009576C6 /* WalletNetworkOperationFactory+Protocol.swift */, 846AF83D2525B85100868F37 /* WalletNetworkFacade.swift */, @@ -9628,7 +9742,6 @@ 84B7C6FB289BFA79001A3566 /* AnalyticsRewardDetails */, 84B7C6FD289BFA79001A3566 /* ParaStkStakeConfirm */, 84B7C6FE289BFA79001A3566 /* ChainAddressDetails */, - 84B7C6FF289BFA79001A3566 /* BalanceLocks */, 84B7C701289BFA79001A3566 /* AssetsManage */, 84B7C703289BFA79001A3566 /* WalletHistoryFilter */, 84B7C705289BFA79001A3566 /* AccountManagement */, @@ -10220,14 +10333,6 @@ path = ChainAddressDetails; sourceTree = ""; }; - 84B7C6FF289BFA79001A3566 /* BalanceLocks */ = { - isa = PBXGroup; - children = ( - 84B7C700289BFA79001A3566 /* BalanceLocksTests.swift */, - ); - path = BalanceLocks; - sourceTree = ""; - }; 84B7C701289BFA79001A3566 /* AssetsManage */ = { isa = PBXGroup; children = ( @@ -10515,6 +10620,7 @@ 848CCB472832EF4400A1FD00 /* GeneralLocalStorageHandler.swift */, 84EF8D3D288FDA2100265346 /* WalletListLocalStorageSubscriber.swift */, 84EF8D3F288FDA7700265346 /* WalletListLocalStorageSubscriptionHandler.swift */, + 880855FB28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift */, ); path = Subscription; sourceTree = ""; @@ -11018,8 +11124,6 @@ children = ( 84EBC55A24F660F500459D15 /* SelectedAccountChanged.swift */, 840882AF2514024800177E20 /* SelectedConnectionChanged.swift */, - 843910B3253EE52100E3C217 /* WalletBalanceChanged.swift */, - 843910BA253F021E00E3C217 /* WalletStakingChanged.swift */, 84FD3DBA254104B600A234E3 /* WalletNewTransactionInserted.swift */, 84BEE22225646ABF00D05EB3 /* SelectedUsernameChanged.swift */, 8488ECDE258CE118004591CC /* PurchaseCompleted.swift */, @@ -11393,6 +11497,18 @@ path = ParaStkUnstakeConfirm; sourceTree = ""; }; + 880855EE28D099BD004255E7 /* CrowdloanService */ = { + isa = PBXGroup; + children = ( + 880855EF28D099F2004255E7 /* CrowdloanOnChainSyncService.swift */, + 880855F128D09A0B004255E7 /* CrowdloanContributionData.swift */, + 880855F328D09A26004255E7 /* RemoteCrowdloanContribution.swift */, + 880855F528D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift */, + 88A0C52028D49A090083A524 /* CrowdloanOffChainSyncService.swift */, + ); + path = CrowdloanService; + sourceTree = ""; + }; 8836AF4228AA290300A94EDD /* Currency */ = { isa = PBXGroup; children = ( @@ -11468,6 +11584,15 @@ path = CrowdloanList; sourceTree = ""; }; + 88C7165B28C8D37E0015D1E9 /* View */ = { + isa = PBXGroup; + children = ( + 88C7165728C8D3270015D1E9 /* LockCollectionViewCell.swift */, + 88C7165928C8D3450015D1E9 /* LocksHeaderView.swift */, + ); + path = View; + sourceTree = ""; + }; 88E1E894289C020D00C123A8 /* View */ = { isa = PBXGroup; children = ( @@ -12578,6 +12703,9 @@ 84CEAAF426D7ADF20021B881 /* KeystoreMigrator.swift */, 84CEAAF626D7B8010021B881 /* SettingsMigrator.swift */, 8457F90F26EB8288006803E1 /* StorageMigrator+Sync.swift */, + 882C29A928DC7B3D009CA4B6 /* SubstrateStorageMigrator.swift */, + 882C29AB28DC7B7F009CA4B6 /* SubstrateStorageVersion.swift */, + 882C29AD28DC7CB4009CA4B6 /* StorageMigrating+CheckVersion.swift */, ); path = Migration; sourceTree = ""; @@ -13424,6 +13552,7 @@ F40966B726B297D6008CD244 /* AnalyticsContainerViewController.swift in Sources */, F4CE0FC727344F600094CD8A /* AcalaContributionConfirmProtocols.swift in Sources */, 8490145224A93FD1008F705E /* FearlessLoadingViewPresenter.swift in Sources */, + 880855F828D09DA8004255E7 /* CrowdloanContributionDataMapper.swift in Sources */, 882AA13028AE64DC0093BC63 /* CrowdloanYourContributionsTotalCell.swift in Sources */, 849976B827B24BCB00B14A6C /* DAppPolkadotExtensionTransport.swift in Sources */, 844DBC71274E83F5009F8351 /* OnboardingMainBaseWireframe.swift in Sources */, @@ -13633,7 +13762,6 @@ 842898D1265A955A002D5D65 /* ImageViewModel.swift in Sources */, 8490145324A93FD1008F705E /* FearlessLoadingViewFactory.swift in Sources */, 8472976C260B1CAD009B86D0 /* InitiatedBondingConfirmInteractor.swift in Sources */, - 843910BB253F021E00E3C217 /* WalletStakingChanged.swift in Sources */, 84EBC55F24F71D6A00459D15 /* UITableViewCell+ReorderColor.swift in Sources */, F43F934B26D76E8E00A6B529 /* AnalyticsRewardsBaseView.swift in Sources */, 84D8F17124D856D300AF43E9 /* SNAddressType+ViewModel.swift in Sources */, @@ -13720,8 +13848,10 @@ 84D17ED628053D6D00F7BAFF /* DAppFavorite.swift in Sources */, 84EE2FB1289128E400A98816 /* WalletManageProtocols.swift in Sources */, 84364D55252FAD7100281F9A /* AssetDetailsConfigurator.swift in Sources */, + 88AC186528CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift in Sources */, 8460E711284AB99E002896E9 /* ParaStkHintsViewModelFactory.swift in Sources */, 2A66CFAF25D10EDF0006E4C1 /* PhishingItem.swift in Sources */, + 882C29AA28DC7B3D009CA4B6 /* SubstrateStorageMigrator.swift in Sources */, 8487583E27F070B300495306 /* ApplicationSettingsPresentable.swift in Sources */, 84FEF3E32807E8000042CBE7 /* DAppBrowserPage.swift in Sources */, AEA2C1C02681E9EC0069492E /* ValidatorSearchViewLayout.swift in Sources */, @@ -13805,10 +13935,10 @@ 84D6D7F827A7DE120094FC33 /* AssetListAccountCell.swift in Sources */, 84F2FEFF25E7ADE7008338D5 /* ValidatorPrefs.swift in Sources */, 84BE209E25E85CCA00B4748C /* ServiceCoordinator.swift in Sources */, - 84B73ADC279D7C100071AE16 /* WalletNetworkFacade+BalanceLocks.swift in Sources */, 84D1F31E260F585E0077DDFE /* AddAccount.swift in Sources */, 846CA77C27099DD90011124C /* WeaklyAnalyticsRewardSource.swift in Sources */, 84452F9D25D6768000F47EC5 /* RuntimeMetadataItem.swift in Sources */, + 84B28FC428C54441007A1006 /* OnChainTransferAmount.swift in Sources */, 84A3B8A82836E4A100DE2669 /* ParachainStakingCandidateMetadata.swift in Sources */, 849842FE26592C2B006BBB9F /* StatusSectionView.swift in Sources */, F42D125F26C1B14D00E59214 /* AnalyticsValidatorsViewModelFactory.swift in Sources */, @@ -13832,6 +13962,7 @@ 8401AE8D2641EF7B000B03E3 /* NetworkFeeConfirmView.swift in Sources */, 841AAC2F26F73E0C00F0A25E /* LocalStorageKeyFactory.swift in Sources */, 843C49DF24DF3CB300B71DDA /* AccountImportMetadata.swift in Sources */, + 88A0C52128D49A090083A524 /* CrowdloanOffChainSyncService.swift in Sources */, 8436E94226C840C8003D4EA7 /* RuntimeCoderEvents.swift in Sources */, 8461CC8A26BD2F07007460E4 /* RuntimeProviderPool.swift in Sources */, 8489A6DC27FE9F570040C066 /* StakingUnbondingItemViewModel.swift in Sources */, @@ -13851,6 +13982,7 @@ 84D8F15F24D8179000AF43E9 /* TitleWithSubtitleViewModel.swift in Sources */, AEE5FB0126415E2A002B8FDC /* StakingRebondSetupInteractor.swift in Sources */, 84C1B98624F5424700FE5470 /* ChainAccountViewModelFactory.swift in Sources */, + 847A25B928D7BB1F006AC9F5 /* BalancesTransferEvent.swift in Sources */, 84E258A52893D27E00DC8A51 /* AddressScanPresentable.swift in Sources */, AEE5FB1026457806002B8FDC /* StakingRewardDestSetupViewFactory.swift in Sources */, 849B563327A70D71007D5528 /* ExtrinsicProcessor+Fee.swift in Sources */, @@ -13865,6 +13997,7 @@ 84F43C0F25DF016600AEDA56 /* DispatchQueueHelper.swift in Sources */, 8490147824A94A37008F705E /* RootPresenterFactory.swift in Sources */, 849014C224AA87E4008F705E /* LocalAuthPresenter.swift in Sources */, + 88BB21A028D34C660019C6B4 /* DataProviderChange+Identifier.swift in Sources */, 8446F5F6281916D300B7A86C /* StakingRewardsHeaderCell.swift in Sources */, 84CA68D126BE99ED003B9453 /* RuntimeProviderFactory.swift in Sources */, 848F8B1928635A5600204BC4 /* TransferSetupPresenter.swift in Sources */, @@ -13908,6 +14041,7 @@ 8401620B25E144D50087A5F3 /* AmountInputAccessoryView.swift in Sources */, 84490E7427F2CE2C00941837 /* TransferMetadataContext.swift in Sources */, 8490151324AB8A3A008F705E /* WalletEmptyStateDataSource.swift in Sources */, + 88F19DE028D8D0F600F6E459 /* LoadableViewModelState+Addition.swift in Sources */, 84DF21AB25363C9F005454AE /* Chain+Info.swift in Sources */, 84A3B8A22836DA2600DE2669 /* LastAccountIdKeyWrapper.swift in Sources */, 84243095265B1888003E07EC /* CrowdloanMetadata.swift in Sources */, @@ -13977,6 +14111,7 @@ 2A84E87825D425750006FE9C /* AlertControllerFactory.swift in Sources */, 843461CB26E2590200DCE0CD /* SubscanHistoryOperationFactory.swift in Sources */, 840DFF5128940D0C001B11EA /* ChainAddressDetailsViewModel.swift in Sources */, + 880855FA28D0BAA2004255E7 /* CrowdloanContributionLocalSubscriptionFactory.swift in Sources */, 84CFF1F226526FBC00DB7CF7 /* StakingBondMoreConfirmationVC.swift in Sources */, 8446F5F82819235B00B7A86C /* AssetIconView+Style.swift in Sources */, 84786E1F25FA6C390089DFF7 /* CDStashItem+CoreDataCodable.swift in Sources */, @@ -14007,6 +14142,7 @@ 843E9B2F27C8B17F009C143A /* FileDownloadOperation.swift in Sources */, 8490152124ABC721008F705E /* WalletStaticImageViewModel.swift in Sources */, F408E9BE26B80FD30043CFE0 /* AnalyticsSectionHeader.swift in Sources */, + 88E8CF5E28E3789600C90112 /* CrowdloanEmptyView.swift in Sources */, 84C6801824D7053B00006BF5 /* BorderedSubtitleActionView.swift in Sources */, 84468A0B286663E500BCBE00 /* CrossChainTransferSetupPresenter.swift in Sources */, 84A2C90C24E192F50020D3B7 /* ShakeAnimator.swift in Sources */, @@ -14022,6 +14158,7 @@ 8463A72D25E3A8E1003B8160 /* ChainStorageDecodedItem.swift in Sources */, 84DD5F30263D84F300425ACF /* RuntimeConstantFetching.swift in Sources */, 84B64E412704569D00914E88 /* StakingLocalSubscriptionHandler.swift in Sources */, + 880855F428D09A26004255E7 /* RemoteCrowdloanContribution.swift in Sources */, 848FFE9525E6DF2200652AA5 /* PagedKeysRequest.swift in Sources */, 8428766B24ADF51D00D91AD8 /* UIViewController+Modal.swift in Sources */, 84468A09286662E000BCBE00 /* CrossChainTransferSetupInteractor.swift in Sources */, @@ -14043,6 +14180,7 @@ 842A736B27DB7A2E006EE1EA /* OperationDetailsViewModel.swift in Sources */, 8425EA9A25EA83FA00C307C9 /* ChainData+Value.swift in Sources */, 8887813E28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift in Sources */, + 88C017E628C60A65003B2D28 /* AssetLockMapper.swift in Sources */, AEA2C1BA2681E9C50069492E /* ValidatorSearchInteractor.swift in Sources */, 84585A2F251BFC8400390F7A /* TriangularedButton+Style.swift in Sources */, 84C6801A24D75E2A00006BF5 /* BorderedSubtitleActionView+Inspectable.swift in Sources */, @@ -14052,6 +14190,7 @@ F4F2296C260DBDCE00ACFDB8 /* StakingPayoutLabelTableCell.swift in Sources */, 844CB57826FA702700396E13 /* CrowdloansViewInfo.swift in Sources */, AEACD5F9265E94AB00A09892 /* StatusViewModel.swift in Sources */, + 847A25BF28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift in Sources */, 84CCBFBC2509709500180F4F /* UIBarButtonItem+Style.swift in Sources */, 8449660A25E15ECA00F2E9F5 /* RewardDestinationViewModel.swift in Sources */, F4223F102732D445003D8E4E /* AcalaStatementData.swift in Sources */, @@ -14090,6 +14229,7 @@ 849014DD24AA8F60008F705E /* MainTabBarViewFactory.swift in Sources */, AE7129C12608CAE7000AA3F5 /* NetworkStakingInfo.swift in Sources */, 849976C427B286AF00B14A6C /* MetamaskMessage.swift in Sources */, + 847A25BD28D7C0E7006AC9F5 /* TokenTransferedEvent.swift in Sources */, 8401F24F24E524900081D8F8 /* String+Helpers.swift in Sources */, F4113B3F260C77FF00DF4DBA /* StakingRewardPayoutsViewLayout.swift in Sources */, AEA0C8A8267B6B3200F9666F /* SelectedValidatorListPresenter.swift in Sources */, @@ -14098,6 +14238,7 @@ 8430AADC26022C58005B1066 /* NoStashState.swift in Sources */, 84F4A9182550331D000CF0A3 /* SecretSource.swift in Sources */, 84FD3DB12540C09800A234E3 /* TransactionHistoryMergeManager.swift in Sources */, + 88C7165A28C8D3450015D1E9 /* LocksHeaderView.swift in Sources */, 840B3D6E289A56BA00DA1DA9 /* ParitySignerScanWireframe.swift in Sources */, F48EB5462722BB7000AE15ED /* AcalaBonusService.swift in Sources */, 84466B4028B77B4500FA1E0D /* SignatureVerificationWrapper.swift in Sources */, @@ -14142,6 +14283,7 @@ AEA0C8C12681180900F9666F /* InitBondingCustomValidatorListWireframe.swift in Sources */, 849976C127B2823F00B14A6C /* DAppMetamaskBaseState.swift in Sources */, 842876A624AE049B00D91AD8 /* SelectableViewModelProtocol.swift in Sources */, + 8831F10028C65B95009F7682 /* AssetLock.swift in Sources */, 842B1806286506EE0014CC57 /* CrossChainTransferSetupProtocols.swift in Sources */, 84D1F2FB260F51770077DDFE /* SwitchAccount.swift in Sources */, 84466B3C28B7680300FA1E0D /* LedgerDiscoverInteractor.swift in Sources */, @@ -14300,6 +14442,7 @@ 84D2F19D2771E5610040C680 /* ExtrinsicBuilder+Signing.swift in Sources */, 8430AAFB260230C5005B1066 /* ValidatorState.swift in Sources */, 8424308D265B1814003E07EC /* CrowdloanOperationFactory.swift in Sources */, + 880855F628D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift in Sources */, 847999B82889510C00D1BAD2 /* TextInputViewDelegate.swift in Sources */, F4EF24D026BA7A4400F28B4E /* AnalyticsViewModelFactoryBase.swift in Sources */, 842A738427DDF55A006EE1EA /* OperationIdOptionsPresentable.swift in Sources */, @@ -14405,7 +14548,6 @@ 84CB2250270360AC0041C8C1 /* StakingLocalSubscriptionFactory.swift in Sources */, 8448221826B1624E007F4492 /* SelectValidatorsConfirmViewLayout.swift in Sources */, 8490145624A9404E008F705E /* AttributedStringDecorator.swift in Sources */, - 843910B4253EE52100E3C217 /* WalletBalanceChanged.swift in Sources */, D9046DBC27453D5C00C29F2E /* ParallelContributionResponse.swift in Sources */, 8499FED227BFA39300712589 /* DataChangesDiffCalculator.swift in Sources */, 887AFC8C28BCB314002A0422 /* PolkadotIconDetailsView.swift in Sources */, @@ -14548,6 +14690,7 @@ 84720730277C335000F593DD /* DAppListFlowLayout.swift in Sources */, AE2C84DF25EF98BA00986716 /* AnyValidatorInfoInteractor.swift in Sources */, 8490142E24A935FE008F705E /* LoadableViewProtocol.swift in Sources */, + 88CD321028E2137300542F0D /* CrowdloanContributionId.swift in Sources */, 84F2FF0725E7AF8F008338D5 /* EraValidatorInfo.swift in Sources */, 84E25BF627E9A51D00290BF1 /* LeaseParam.swift in Sources */, 843939902636F88F0087658D /* YourValidatorsModel.swift in Sources */, @@ -14561,6 +14704,7 @@ 8459A9CC2746A1E9000D6278 /* CrowdloanOffchainSubscriptionHandler.swift in Sources */, 84D331AF2519E8080078D044 /* TriangularedView+Style.swift in Sources */, 84C74365251E4D60009576C6 /* SigningWrapperProtocol.swift in Sources */, + 88AF35DE28C21D28003730DA /* LocksSubscription.swift in Sources */, 844CB56A26F9C57D00396E13 /* WalletLocalStorageSubscriber.swift in Sources */, 842A738A27DE14B3006EE1EA /* TransactionLocalStorageHandler.swift in Sources */, 8454C2652632B0EF00657DAD /* EventCodingPath.swift in Sources */, @@ -14669,7 +14813,7 @@ 8472C5B0265CF9C500E2481B /* StakingRewardDestConfirmWireframe.swift in Sources */, 84FB1F792527065A00E0242B /* HistoryConstants.swift in Sources */, 848F8B292864503A00204BC4 /* TransferSetupWireframe.swift in Sources */, - 84F18D4C27A1874000CA7554 /* OrmlAccountSubcriptionHandlingFactory.swift in Sources */, + 84F18D4C27A1874000CA7554 /* TokenSubscriptionFactory.swift in Sources */, 4448B591D4A193DBC9E2E3BF /* AccountCreateInteractor.swift in Sources */, 84DF21A92535AA8F005454AE /* ExistentialDepositInfoCommand.swift in Sources */, 84E6D57C262E2CE8000EA3F5 /* OperationCombiningService.swift in Sources */, @@ -14763,6 +14907,7 @@ 84DF21B12536DDC1005454AE /* TransferConfirmCommand.swift in Sources */, AEA0C8BC2681140700F9666F /* YourValidatorList+CustomList.swift in Sources */, 84E83AA428632AF50000B418 /* XcmPalletTransfer.swift in Sources */, + 880855FC28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift in Sources */, 8499FE6827BD1EA600712589 /* DistributedStorageFactory.swift in Sources */, 8428228A289B1E5C00163031 /* TableHeaderLayoutUpdatable.swift in Sources */, 84EE2FAF2891215200A98816 /* WalletManageTableViewCell.swift in Sources */, @@ -14776,6 +14921,7 @@ 84B018AC26E01A4100C75E28 /* StakingStateView.swift in Sources */, 842A737C27DCC489006EE1EA /* OperationDetailsTransferView.swift in Sources */, 8472C5B2265CF9C500E2481B /* StakingRewardDestConfirmInteractor.swift in Sources */, + 882C29AE28DC7CB4009CA4B6 /* StorageMigrating+CheckVersion.swift in Sources */, 846CA77A27099B1E0011124C /* StakingAnalyticsLocalSubscriptionFactory.swift in Sources */, 84CEEDE2284E3DCE0039364A /* AccountDetailsNavigationCell.swift in Sources */, 8466781127EB4078007935D3 /* StackNetworkFeeCell.swift in Sources */, @@ -14802,6 +14948,7 @@ 849A4EF6279A7AEF00AB6709 /* StateminAssetExtras.swift in Sources */, 849E17E627914394002D1744 /* NavigationBarSettings.swift in Sources */, 849E17F02791909C002D1744 /* DAppSettings.swift in Sources */, + 884048D428C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift in Sources */, 846A2C4D2529FBB700731018 /* NSPredicate+Filter.swift in Sources */, 84282292289BB45700163031 /* ParitySignerWallet.swift in Sources */, 84D17EDA28054C7500F7BAFF /* DAppLocalStorageSubscriber.swift in Sources */, @@ -14876,6 +15023,7 @@ 848B59C228BCC1E60009543C /* LedgerAddAccountConfirmationInteractor.swift in Sources */, 843461CF26E25AD400DCE0CD /* SubscanHistoryItem+Wallet.swift in Sources */, 882A5CED28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift in Sources */, + 88C7165428C894510015D1E9 /* CollectionViewDelegate.swift in Sources */, F458D3982642911B0055CB75 /* ControllerAccountViewModel.swift in Sources */, 84DAC198268D3DD9002D0DF4 /* SNAddressType.swift in Sources */, 84AC0B6A28C0D8CE00FA5B5D /* NoLedgerSupportCommand.swift in Sources */, @@ -15080,6 +15228,7 @@ 84B5DE59283F8B5400193ED3 /* CollatorSelectionViewModel.swift in Sources */, 84E4932727325D4E000534F2 /* AssetListViewModelFactory.swift in Sources */, 8401AEC02642A71D000B03E3 /* StakingRebondConfirmationViewModelFactory.swift in Sources */, + 880855ED28D062A9004255E7 /* Array+AddOrReplace.swift in Sources */, 8472C5AD265CF9C500E2481B /* StakingRewardDestConfirmViewModelFactory.swift in Sources */, 8422F2F328881C9B00C7B840 /* WatchOnlyWallet.swift in Sources */, 8446F5F228172BBC00B7A86C /* StakingUnbonHintView.swift in Sources */, @@ -15164,6 +15313,7 @@ 84D1ABE227E1E8060073C631 /* NewAmountInputView.swift in Sources */, 84100F3C26A60E4C00A5054E /* YourValidatorListWarningSectionView.swift in Sources */, 58F693958EF69F59D7C9760E /* StakingRewardPayoutsInteractor.swift in Sources */, + 88F19DDE28D8D0A100F6E459 /* Either.swift in Sources */, 50758C9BBB27AE5732FF78BA /* StakingRewardPayoutsViewController.swift in Sources */, AEF507AF262423FD0098574D /* HmacSigner.swift in Sources */, 3229E306230161AA99B14BDD /* StakingRewardPayoutsViewFactory.swift in Sources */, @@ -15211,6 +15361,7 @@ 6D47EAB127FAB7559A9FA107 /* StakingPayoutConfirmationViewController.swift in Sources */, F4EF24C826BA713300F28B4E /* AnalyticsStakeHeaderView.swift in Sources */, 849ABE772628103200011A2A /* ControllersListReducer.swift in Sources */, + 88C7165628C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift in Sources */, 840DFF532894189D001B11EA /* ChainAddressDetailsMeasurement.swift in Sources */, 9565BEB636E6D386B0C0FBE5 /* StakingPayoutConfirmationViewFactory.swift in Sources */, 6F0CFDAB9D0C35075BD74A77 /* WalletHistoryFilterProtocols.swift in Sources */, @@ -15236,6 +15387,7 @@ F4223ED127329767003D8E4E /* AcalaTransferRequest.swift in Sources */, 8473F4B8282BFFF8007CC55A /* StakingRelaychainInteractor+Subscription.swift in Sources */, 841E2E5027381B2A00F250C1 /* AccountInfoSubscriptionHandlingFactory.swift in Sources */, + 88AC186128CA3EE100892A9B /* LocksViewLayout.swift in Sources */, A32E1373E3671D518FFC3BC2 /* YourValidatorListViewController.swift in Sources */, 84D2F1A927744C280040C680 /* PolkadotExtensionError.swift in Sources */, 37E1E9782B9752BC50AF2476 /* YourValidatorListViewFactory.swift in Sources */, @@ -15281,8 +15433,10 @@ AEE5FB1C264A610C002B8FDC /* StakingRewardDestSetupLayout.swift in Sources */, 84350ADB28461E5B0031EF24 /* ParaStkYourCollatorsViewModelFactory.swift in Sources */, B7CF31A548C02AD7AAC16A8D /* StakingRedeemWireframe.swift in Sources */, + 88C7165828C8D3280015D1E9 /* LockCollectionViewCell.swift in Sources */, C0B0DDF638915E8259B1CD67 /* StakingRedeemPresenter.swift in Sources */, C4A4D40A08DAB4A71C21C1A8 /* StakingRedeemInteractor.swift in Sources */, + 88A6BD0128CA15710047E4C2 /* LocksViewInput.swift in Sources */, F409672B26B29C3B008CD244 /* RewardAnalyticsWidgetView.swift in Sources */, B1CCC5B7BF30F6ACA309B112 /* StakingRedeemViewController.swift in Sources */, C21129B2B8D8B33BCBD5843E /* StakingRedeemViewFactory.swift in Sources */, @@ -15327,6 +15481,7 @@ 84D9C8EF28AD97E7007FB23B /* SupportedLedgerApps.swift in Sources */, 8482F62A280C4B770006C3A0 /* DAppAuthSettingsViewModel.swift in Sources */, 840DC8422880AF440039A054 /* AssetSelectionViewController.swift in Sources */, + 880855F228D09A0B004255E7 /* CrowdloanContributionData.swift in Sources */, B51AD1836313CE26F369ED3F /* CustomValidatorListWireframe.swift in Sources */, D565DB5ED3B8B4D9BCFB4C21 /* CustomValidatorListPresenter.swift in Sources */, 8AEF593AFE8F59F7DC0A5753 /* CustomValidatorListInteractor.swift in Sources */, @@ -15370,6 +15525,7 @@ 8499FED827BFCABD00712589 /* NftModel+Identifier.swift in Sources */, 88421064289BBD9100306F2C /* Currency.swift in Sources */, 921E4891E85C0DC6FDD8A0D0 /* CrowdloanContributionConfirmInteractor.swift in Sources */, + 88A6BCFF28CA15400047E4C2 /* LocksBalanceViewModelFactory.swift in Sources */, 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */, 2793D406FD618A892D54EA84 /* CrowdloanContributionConfirmViewLayout.swift in Sources */, 840D92A3278EDB2E0007B979 /* DAppParsedCall.swift in Sources */, @@ -15396,6 +15552,7 @@ F4433D7A26C16D070002A91E /* AnalyticsValidatorsViewModel.swift in Sources */, 340AC2484415B10F247C135E /* AnalyticsValidatorsPresenter.swift in Sources */, C9A608AFCFF4030D63D1FB4F /* AnalyticsValidatorsInteractor.swift in Sources */, + 880855F028D099F2004255E7 /* CrowdloanOnChainSyncService.swift in Sources */, 8BF525D6B5DFB7CF6C03B015 /* AnalyticsValidatorsViewController.swift in Sources */, 237AD34CD1C2778834D7B330 /* AnalyticsValidatorsViewFactory.swift in Sources */, 843CE3A627D2098100436F4E /* NftDetailsLabel.swift in Sources */, @@ -15403,7 +15560,7 @@ 84216FD42827982800479375 /* SelectedRoundCollators.swift in Sources */, 849E17DC27909179002D1744 /* DAppSearchQueryTableViewCell.swift in Sources */, 76F74188F16A370D79033A12 /* AnalyticsRewardDetailsWireframe.swift in Sources */, - 2AC7BC842731A214001D99B0 /* BalanceLocks+Sort.swift in Sources */, + 2AC7BC842731A214001D99B0 /* AssetLocks+Sort.swift in Sources */, 848CCB4E2833CC4D00A1FD00 /* StakingMainStaticViewModel.swift in Sources */, F17C7FA0DB540A803558D1BB /* AnalyticsRewardDetailsPresenter.swift in Sources */, EB544E8D26ABEE4ADE2F939F /* AnalyticsRewardDetailsInteractor.swift in Sources */, @@ -15496,6 +15653,7 @@ DE03CA5AD7F1D0B80DFF13B6 /* DAppBrowserViewController.swift in Sources */, 70C0E48EE41B4C7229F5946C /* DAppBrowserViewLayout.swift in Sources */, FDE2CA45061C620567AC329C /* DAppBrowserViewFactory.swift in Sources */, + 882C29AC28DC7B7F009CA4B6 /* SubstrateStorageVersion.swift in Sources */, 84BC7045289EFF44008A9758 /* TransactionDisplayCode.swift in Sources */, 1D1DC32EFF13F41677A084B7 /* DAppOperationConfirmProtocols.swift in Sources */, 841E5561282E9CC700C8438F /* ParaStkStateMachineProtocols.swift in Sources */, @@ -15522,6 +15680,7 @@ 9BADFCBF3AF5186094DB8D67 /* DAppTxDetailsInteractor.swift in Sources */, B409644ED1E20062A3EA0316 /* DAppTxDetailsViewController.swift in Sources */, DAD46B2B29A446C19A6ABF2D /* DAppTxDetailsViewLayout.swift in Sources */, + 88AC186328CA3F0000892A9B /* GenericCollectionViewLayout.swift in Sources */, A97F32D057BFEFBCC478A09C /* DAppTxDetailsViewFactory.swift in Sources */, D567BAAF620EDB9F4975C800 /* DAppAuthConfirmProtocols.swift in Sources */, 84C342092831645800156569 /* EraCountdownDisplay.swift in Sources */, @@ -15721,6 +15880,7 @@ 09A6D92CE47636723DFC91F4 /* MessageSheetViewFactory.swift in Sources */, 4BC33C8DE172AE573AEEDA4F /* WalletsListProtocols.swift in Sources */, 049DA9A36A72CB6F8401769C /* WalletsListWireframe.swift in Sources */, + 847A25BB28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift in Sources */, 1812D5012A1765CB38D32A4A /* WalletsListPresenter.swift in Sources */, A265CC9857E951EB71E5E831 /* WalletsListInteractor.swift in Sources */, 28B4C94DBAF461CBF18B1B63 /* WalletsListViewController.swift in Sources */, @@ -15816,6 +15976,11 @@ B317AB093D99677D292121C4 /* YourWalletsViewController.swift in Sources */, E8C54C2441B78248B6067204 /* YourWalletsViewLayout.swift in Sources */, 964DE461970AF6B89D0968C5 /* YourWalletsViewFactory.swift in Sources */, + 9E40464B7687006B1EE75C72 /* LocksProtocols.swift in Sources */, + 30413A3C5ADB96B7D663F94D /* LocksWireframe.swift in Sources */, + BE301A0F2286CCEF6A02D341 /* LocksPresenter.swift in Sources */, + CE4C1344F03A5132C601A594 /* LocksViewController.swift in Sources */, + 8A23DD1F4146639EA2F7AEF6 /* LocksViewFactory.swift in Sources */, C0A7710415B9C9BA496320E7 /* ParaStkYieldBoostSetupProtocols.swift in Sources */, 7E5ACF8DDF17C054E6E1B3D5 /* ParaStkYieldBoostSetupWireframe.swift in Sources */, 7C0135CA49EF6B535030643E /* ParaStkYieldBoostSetupPresenter.swift in Sources */, @@ -15877,6 +16042,7 @@ 84B7C72E289BFA79001A3566 /* CustomValidatorListTestDataGenerator.swift in Sources */, 84B7C746289BFA79001A3566 /* WalletHistoryFilterTests.swift in Sources */, 843EC7A82701F63600C7DC7E /* PriceProviderFactoryStub.swift in Sources */, + 8890E51628DDC98C001D3994 /* SubstrateStorageMigrationTests.swift in Sources */, 84B7C70E289BFA79001A3566 /* SettingsTests.swift in Sources */, 84563D0924F46B7F0055591D /* ManagedAccountItemMapperTests.swift in Sources */, 845B822726EFFE0200D25C72 /* MetaAccountMapperTests.swift in Sources */, @@ -15916,7 +16082,6 @@ 84B7C745289BFA79001A3566 /* AssetsManageTests.swift in Sources */, 84B7C728289BFA79001A3566 /* YourValidatorListTests.swift in Sources */, 84B7C743289BFA79001A3566 /* AnalyticsRewardDetailsTests.swift in Sources */, - 84B7C744289BFA79001A3566 /* BalanceLocksTests.swift in Sources */, 84B7C72F289BFA79001A3566 /* CustomValidatorListComposerTests.swift in Sources */, 84B7C717289BFA79001A3566 /* DAppAuthConfirmTests.swift in Sources */, 84F4387F25D9D61300AEDA56 /* SubstrateStorageTestFacade.swift in Sources */, @@ -16634,9 +16799,10 @@ 843910CA253F7E6500E3C217 /* SubstrateDataModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 88787F0328DB3A7B00B115AB /* SubstrateDataModel2.xcdatamodel */, 843910CB253F7E6500E3C217 /* SubstrateDataModel.xcdatamodel */, ); - currentVersion = 843910CB253F7E6500E3C217 /* SubstrateDataModel.xcdatamodel */; + currentVersion = 88787F0328DB3A7B00B115AB /* SubstrateDataModel2.xcdatamodel */; path = SubstrateDataModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/novawallet/Assets.xcassets/iconLock.imageset/iconLock.pdf b/novawallet/Assets.xcassets/iconLock.imageset/iconLock.pdf index af52f11cb8..a0a792c201 100644 Binary files a/novawallet/Assets.xcassets/iconLock.imageset/iconLock.pdf and b/novawallet/Assets.xcassets/iconLock.imageset/iconLock.pdf differ diff --git a/novawallet/Assets.xcassets/iconTransferable.imageset/Contents.json b/novawallet/Assets.xcassets/iconTransferable.imageset/Contents.json new file mode 100644 index 0000000000..bfbeb098c2 --- /dev/null +++ b/novawallet/Assets.xcassets/iconTransferable.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconTransferable.imageset/Icon.pdf b/novawallet/Assets.xcassets/iconTransferable.imageset/Icon.pdf new file mode 100644 index 0000000000..59c3f7ff79 Binary files /dev/null and b/novawallet/Assets.xcassets/iconTransferable.imageset/Icon.pdf differ diff --git a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift new file mode 100644 index 0000000000..ec59dcedf4 --- /dev/null +++ b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift @@ -0,0 +1,225 @@ +import SubstrateSdk +import RobinHood + +protocol CrowdloanContributionLocalSubscriptionFactoryProtocol { + func getCrowdloanContributionDataProvider( + for accountId: AccountId, + chain: ChainModel + ) -> StreamableProvider? + + func getAllLocalCrowdloanContributionDataProvider() -> StreamableProvider? +} + +final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, + CrowdloanContributionLocalSubscriptionFactoryProtocol { + let operationFactory: CrowdloanOperationFactoryProtocol + let paraIdOperationFactory: ParaIdOperationFactoryProtocol + let eventCenter: EventCenterProtocol + + init( + operationFactory: CrowdloanOperationFactoryProtocol, + operationManager: OperationManagerProtocol, + chainRegistry: ChainRegistryProtocol, + storageFacade: StorageFacadeProtocol, + paraIdOperationFactory: ParaIdOperationFactoryProtocol, + eventCenter: EventCenterProtocol, + logger: LoggerProtocol + ) { + self.operationFactory = operationFactory + self.paraIdOperationFactory = paraIdOperationFactory + self.eventCenter = eventCenter + + super.init( + chainRegistry: chainRegistry, + storageFacade: storageFacade, + operationManager: operationManager, + logger: logger + ) + } + + func getCrowdloanContributionDataProvider( + for accountId: AccountId, + chain: ChainModel + ) -> StreamableProvider? { + let cacheKey = "crowdloanContributions-\(accountId.toHex())-\(chain.chainId)" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let offchainSources: [ExternalContributionSourceProtocol] = [ + ParallelContributionSource(), + AcalaContributionSource( + paraIdOperationFactory: paraIdOperationFactory, + acalaChainId: KnowChainId.acala + ) + ] + + let onChainSyncService = createOnChainSyncService(chainId: chain.chainId, accountId: accountId) + let offChainSyncServices = createOffChainSyncServices( + from: offchainSources, + chain: chain, + accountId: accountId + ) + + let syncServices = [onChainSyncService] + offChainSyncServices + + let source = CrowdloanContributionStreamableSource( + syncServices: syncServices, + chainId: chain.chainId, + accountId: accountId, + eventCenter: eventCenter + ) + + let crowdloansFilter = NSPredicate.crowdloanContribution( + for: chain.chainId, + accountId: accountId + ) + + let mapper = CrowdloanContributionDataMapper() + let repository = storageFacade.createRepository( + filter: crowdloansFilter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + + let observable = CoreDataContextObservable( + service: storageFacade.databaseService, + mapper: AnyCoreDataMapper(mapper), + predicate: { entity in + accountId.toHex() == entity.chainAccountId && + chain.chainId == entity.chainId + } + ) + + observable.start { [weak self] error in + if let error = error { + self?.logger.error("Did receive error: \(error)") + } + } + + let provider = StreamableProvider( + source: AnyStreamableSource(source), + repository: AnyDataProviderRepository(repository), + observable: AnyDataProviderRepositoryObservable(observable), + operationManager: operationManager + ) + + saveProvider(provider, for: cacheKey) + + return provider + } + + private func createOnChainSyncService(chainId: ChainModel.Id, accountId: AccountId) -> SyncServiceProtocol { + let mapper = CrowdloanContributionDataMapper() + let onChainFilter = NSPredicate.crowdloanContribution( + for: chainId, + accountId: accountId, + source: nil + ) + let onChainCrowdloansRepository = storageFacade.createRepository( + filter: onChainFilter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + return CrowdloanOnChainSyncService( + operationFactory: operationFactory, + chainRegistry: chainRegistry, + repository: AnyDataProviderRepository(onChainCrowdloansRepository), + accountId: accountId, + chainId: chainId, + operationManager: operationManager, + logger: logger + ) + } + + private func createOffChainSyncServices( + from sources: [ExternalContributionSourceProtocol], + chain: ChainModel, + accountId: AccountId + ) -> [SyncServiceProtocol] { + let mapper = CrowdloanContributionDataMapper() + + return sources.map { source in + let chainFilter = NSPredicate.crowdloanContribution( + for: chain.chainId, + accountId: accountId, + source: source.sourceName + ) + let serviceRepository = storageFacade.createRepository( + filter: chainFilter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + return CrowdloanOffChainSyncService( + source: source, + chain: chain, + accountId: accountId, + operationManager: operationManager, + repository: AnyDataProviderRepository(serviceRepository), + logger: logger + ) + } + } + + func getAllLocalCrowdloanContributionDataProvider() -> StreamableProvider? { + let cacheKey = "all-crowdloanContributions" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let source = EmptyStreamableSource() + let mapper = CrowdloanContributionDataMapper() + let repository = storageFacade.createRepository( + filter: nil, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + + let observable = CoreDataContextObservable( + service: storageFacade.databaseService, + mapper: AnyCoreDataMapper(mapper), + predicate: { _ in + true + } + ) + + observable.start { [weak self] error in + if let error = error { + self?.logger.error("Did receive error: \(error)") + } + } + + let provider = StreamableProvider( + source: AnyStreamableSource(source), + repository: AnyDataProviderRepository(repository), + observable: AnyDataProviderRepositoryObservable(observable), + operationManager: operationManager + ) + + saveProvider(provider, for: cacheKey) + + return provider + } +} + +extension CrowdloanContributionLocalSubscriptionFactory { + static let operationManager = OperationManagerFacade.sharedManager + + static let shared = CrowdloanContributionLocalSubscriptionFactory( + operationFactory: CrowdloanOperationFactory( + requestOperationFactory: StorageRequestFactory( + remoteFactory: StorageKeyFactory(), + operationManager: operationManager + ), + operationManager: operationManager + ), + operationManager: operationManager, + chainRegistry: ChainRegistryFacade.sharedRegistry, + storageFacade: SubstrateDataStorageFacade.shared, + paraIdOperationFactory: ParaIdOperationFactory.shared, + eventCenter: EventCenter.shared, + logger: Logger.shared + ) +} diff --git a/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift new file mode 100644 index 0000000000..e2fcf6197a --- /dev/null +++ b/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift @@ -0,0 +1,120 @@ +import Foundation +import RobinHood + +protocol CrowdloanContributionLocalSubscriptionHandler: AnyObject { + func handleCrowdloans( + result: Result<[DataProviderChange], Error>, + accountId: AccountId, + chain: ChainModel + ) + + func handleAllCrowdloans(result: Result<[DataProviderChange], Error>) +} + +protocol CrowdloansLocalStorageSubscriber: AnyObject { + var crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol { get } + var crowdloansLocalSubscriptionHandler: CrowdloanContributionLocalSubscriptionHandler { get } + + func subscribeToCrowdloansProvider( + for account: AccountId, + chain: ChainModel + ) -> StreamableProvider? + + func subscribeToAllCrowdloansProvider() -> StreamableProvider? +} + +extension CrowdloansLocalStorageSubscriber { + func subscribeToCrowdloansProvider( + for accountId: AccountId, + chain: ChainModel + ) -> StreamableProvider? { + guard let provider = crowdloansLocalSubscriptionFactory.getCrowdloanContributionDataProvider( + for: accountId, + chain: chain + ) else { + return nil + } + + let updateClosure = { [weak self] (changes: [DataProviderChange]) in + self?.crowdloansLocalSubscriptionHandler.handleCrowdloans( + result: .success(changes), + accountId: accountId, + chain: chain + ) + return + } + + let failureClosure = { [weak self] (error: Error) in + self?.crowdloansLocalSubscriptionHandler.handleCrowdloans( + result: .failure(error), + accountId: accountId, + chain: chain + ) + return + } + + let options = StreamableProviderObserverOptions( + alwaysNotifyOnRefresh: true, + waitsInProgressSyncOnAdd: false, + initialSize: 0, + refreshWhenEmpty: false + ) + + provider.addObserver( + self, + deliverOn: .main, + executing: updateClosure, + failing: failureClosure, + options: options + ) + + return provider + } + + func subscribeToAllCrowdloansProvider() -> StreamableProvider? { + guard let provider = crowdloansLocalSubscriptionFactory.getAllLocalCrowdloanContributionDataProvider() else { + return nil + } + + let updateClosure = { [weak self] (changes: [DataProviderChange]) in + self?.crowdloansLocalSubscriptionHandler.handleAllCrowdloans(result: .success(changes)) + return + } + + let failureClosure = { [weak self] (error: Error) in + self?.crowdloansLocalSubscriptionHandler.handleAllCrowdloans(result: .failure(error)) + return + } + + let options = StreamableProviderObserverOptions( + alwaysNotifyOnRefresh: true, + waitsInProgressSyncOnAdd: false, + initialSize: 0, + refreshWhenEmpty: false + ) + + provider.addObserver( + self, + deliverOn: .main, + executing: updateClosure, + failing: failureClosure, + options: options + ) + + return provider + } +} + +extension CrowdloansLocalStorageSubscriber where Self: CrowdloanContributionLocalSubscriptionHandler { + var crowdloansLocalSubscriptionHandler: CrowdloanContributionLocalSubscriptionHandler { self } +} + +extension CrowdloanContributionLocalSubscriptionHandler { + func handleCrowdloans( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId, + chain _: ChainModel + ) {} + + func handleAllCrowdloans(result _: Result<[DataProviderChange], Error>) {} +} diff --git a/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift index d49c371aba..abc1ac66d5 100644 --- a/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift +++ b/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift @@ -22,6 +22,16 @@ protocol WalletLocalStorageSubscriber where Self: AnyObject { ) -> StreamableProvider? func subscribeAllBalancesProvider() -> StreamableProvider? + + func subscribeToAllLocksProvider( + for accountId: AccountId + ) -> StreamableProvider? + + func subscribeToLocksProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) -> StreamableProvider? } extension WalletLocalStorageSubscriber { @@ -201,6 +211,96 @@ extension WalletLocalStorageSubscriber { return provider } + + func subscribeToAllLocksProvider( + for accountId: AccountId + ) -> StreamableProvider? { + guard let locksProvider = try? walletLocalSubscriptionFactory.getLocksProvider(for: accountId) else { + return nil + } + + let updateClosure = { [weak self] (changes: [DataProviderChange]) in + self?.walletLocalSubscriptionHandler.handleAccountLocks(result: .success(changes), accountId: accountId) + return + } + + let failureClosure = { [weak self] (error: Error) in + self?.walletLocalSubscriptionHandler.handleAccountLocks( + result: .failure(error), + accountId: accountId + ) + return + } + + let options = StreamableProviderObserverOptions( + alwaysNotifyOnRefresh: false, + waitsInProgressSyncOnAdd: false, + initialSize: 0, + refreshWhenEmpty: false + ) + + locksProvider.addObserver( + self, + deliverOn: .main, + executing: updateClosure, + failing: failureClosure, + options: options + ) + + return locksProvider + } + + func subscribeToLocksProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) -> StreamableProvider? { + guard + let locksProvider = try? walletLocalSubscriptionFactory.getLocksProvider( + for: accountId, + chainId: chainId, + assetId: assetId + ) else { + return nil + } + + let updateClosure = { [weak self] (changes: [DataProviderChange]) in + self?.walletLocalSubscriptionHandler.handleAccountLocks( + result: .success(changes), + accountId: accountId, + chainId: chainId, + assetId: assetId + ) + return + } + + let failureClosure = { [weak self] (error: Error) in + self?.walletLocalSubscriptionHandler.handleAccountLocks( + result: .failure(error), + accountId: accountId, + chainId: chainId, + assetId: assetId + ) + return + } + + let options = StreamableProviderObserverOptions( + alwaysNotifyOnRefresh: false, + waitsInProgressSyncOnAdd: false, + initialSize: 0, + refreshWhenEmpty: false + ) + + locksProvider.addObserver( + self, + deliverOn: .main, + executing: updateClosure, + failing: failureClosure, + options: options + ) + + return locksProvider + } } extension WalletLocalStorageSubscriber where Self: WalletLocalSubscriptionHandler { diff --git a/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift b/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift index 3abf747bb3..2bff23fe88 100644 --- a/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift +++ b/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift @@ -21,6 +21,18 @@ protocol WalletLocalSubscriptionHandler { ) func handleAllBalances(result: Result<[DataProviderChange], Error>) + + func handleAccountLocks( + result: Result<[DataProviderChange], Error>, + accountId: AccountId + ) + + func handleAccountLocks( + result: Result<[DataProviderChange], Error>, + accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) } extension WalletLocalSubscriptionHandler { @@ -43,4 +55,16 @@ extension WalletLocalSubscriptionHandler { ) {} func handleAllBalances(result _: Result<[DataProviderChange], Error>) {} + + func handleAccountLocks( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId + ) {} + + func handleAccountLocks( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId, + chainId _: ChainModel.Id, + assetId _: AssetModel.Id + ) {} } diff --git a/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift index e6372d18d5..a5283834a9 100644 --- a/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift @@ -16,6 +16,14 @@ protocol WalletLocalSubscriptionFactoryProtocol { func getAccountBalanceProvider(for accountId: AccountId) throws -> StreamableProvider func getAllBalancesProvider() throws -> StreamableProvider + + func getLocksProvider(for accountId: AccountId) throws -> StreamableProvider + + func getLocksProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) throws -> StreamableProvider } final class WalletLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, @@ -172,4 +180,85 @@ final class WalletLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, return provider } + + func getLocksProvider(for accountId: AccountId) throws -> StreamableProvider { + let cacheKey = "locks-\(accountId.toHex())" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let filter = NSPredicate.assetLock(for: accountId) + + let provider = createAssetLocksProvider(for: filter) { entity in + accountId.toHex() == entity.chainAccountId + } + + saveProvider(provider, for: cacheKey) + + return provider + } + + func getLocksProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) throws -> StreamableProvider { + let cacheKey = "locks-\(accountId.toHex())-\(chainId)-\(assetId)" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let filter = NSPredicate.assetLock( + for: accountId, + chainAssetId: ChainAssetId(chainId: chainId, assetId: assetId) + ) + + let provider = createAssetLocksProvider(for: filter) { entity in + accountId.toHex() == entity.chainAccountId && + chainId == entity.chainId && + assetId == entity.assetId + } + + saveProvider(provider, for: cacheKey) + + return provider + } + + private func createAssetLocksProvider( + for repositoryFilter: NSPredicate, + observingFilter: @escaping (CDAssetLock) -> Bool + ) -> StreamableProvider { + let source = EmptyStreamableSource() + + let mapper = AssetLockMapper() + + let repository = storageFacade.createRepository( + filter: repositoryFilter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + + let observable = CoreDataContextObservable( + service: storageFacade.databaseService, + mapper: AnyCoreDataMapper(mapper), + predicate: { entity in + observingFilter(entity) + } + ) + + observable.start { [weak self] error in + if let error = error { + self?.logger.error("Did receive error: \(error)") + } + } + + return StreamableProvider( + source: AnyStreamableSource(source), + repository: AnyDataProviderRepository(repository), + observable: AnyDataProviderRepositoryObservable(observable), + operationManager: operationManager + ) + } } diff --git a/novawallet/Common/EventCenter/EventVisitor.swift b/novawallet/Common/EventCenter/EventVisitor.swift index eb7895a57e..d14af67485 100644 --- a/novawallet/Common/EventCenter/EventVisitor.swift +++ b/novawallet/Common/EventCenter/EventVisitor.swift @@ -5,8 +5,6 @@ protocol EventVisitorProtocol: AnyObject { func processSelectedAccountChanged(event: SelectedAccountChanged) func processSelectedUsernameChanged(event: SelectedUsernameChanged) func processSelectedConnectionChanged(event: SelectedConnectionChanged) - func processBalanceChanged(event: WalletBalanceChanged) - func processStakingChanged(event: WalletStakingInfoChanged) func processNewTransaction(event: WalletNewTransactionInserted) func processPurchaseCompletion(event: PurchaseCompleted) func processTypeRegistryPrepared(event: TypeRegistryPrepared) @@ -34,8 +32,6 @@ extension EventVisitorProtocol { func processChainAccountChanged(event _: ChainAccountChanged) {} func processSelectedAccountChanged(event _: SelectedAccountChanged) {} func processSelectedConnectionChanged(event _: SelectedConnectionChanged) {} - func processBalanceChanged(event _: WalletBalanceChanged) {} - func processStakingChanged(event _: WalletStakingInfoChanged) {} func processNewTransaction(event _: WalletNewTransactionInserted) {} func processSelectedUsernameChanged(event _: SelectedUsernameChanged) {} func processPurchaseCompletion(event _: PurchaseCompleted) {} diff --git a/novawallet/Common/EventCenter/Events/WalletBalanceChanged.swift b/novawallet/Common/EventCenter/Events/WalletBalanceChanged.swift deleted file mode 100644 index e4b2e5babc..0000000000 --- a/novawallet/Common/EventCenter/Events/WalletBalanceChanged.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -struct WalletBalanceChanged: EventProtocol { - func accept(visitor: EventVisitorProtocol) { - visitor.processBalanceChanged(event: self) - } -} diff --git a/novawallet/Common/EventCenter/Events/WalletStakingChanged.swift b/novawallet/Common/EventCenter/Events/WalletStakingChanged.swift deleted file mode 100644 index ea20543b3f..0000000000 --- a/novawallet/Common/EventCenter/Events/WalletStakingChanged.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -struct WalletStakingInfoChanged: EventProtocol { - func accept(visitor: EventVisitorProtocol) { - visitor.processStakingChanged(event: self) - } -} diff --git a/novawallet/Common/Extension/Foundation/Array+AddOrReplace.swift b/novawallet/Common/Extension/Foundation/Array+AddOrReplace.swift new file mode 100644 index 0000000000..ed6375d7b6 --- /dev/null +++ b/novawallet/Common/Extension/Foundation/Array+AddOrReplace.swift @@ -0,0 +1,11 @@ +import RobinHood + +extension Array where Element: Identifiable { + mutating func addOrReplaceSingle(_ element: Element) { + if let index = firstIndex(where: { $0.identifier == element.identifier }) { + self[index] = element + } else { + append(element) + } + } +} diff --git a/novawallet/Common/Extension/Foundation/DataProviderChange+Identifier.swift b/novawallet/Common/Extension/Foundation/DataProviderChange+Identifier.swift new file mode 100644 index 0000000000..923f5b8332 --- /dev/null +++ b/novawallet/Common/Extension/Foundation/DataProviderChange+Identifier.swift @@ -0,0 +1,12 @@ +import RobinHood + +extension DataProviderChange where T: Identifiable { + var identifier: String { + switch self { + case let .insert(newItem), let .update(newItem): + return newItem.identifier + case let .delete(deletedIdentifier): + return deletedIdentifier + } + } +} diff --git a/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift b/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift index 3633470af4..793a0d6d32 100644 --- a/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift +++ b/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift @@ -204,6 +204,39 @@ extension NSPredicate { ) } + static func assetLock( + for accountId: AccountId + ) -> NSPredicate { + NSPredicate( + format: "%K == %@", + #keyPath(CDAssetLock.chainAccountId), + accountId.toHex() + ) + } + + static func assetLock( + for accountId: AccountId, + chainAssetId: ChainAssetId + ) -> NSPredicate { + let accountPredicate = assetLock(for: accountId) + + let chainIdPredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDAssetLock.chainId), + chainAssetId.chainId + ) + + let assetIdPredicate = NSPredicate( + format: "%K == %d", + #keyPath(CDAssetLock.assetId), + chainAssetId.assetId + ) + + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + accountPredicate, chainIdPredicate, assetIdPredicate + ]) + } + static func nfts(for chainId: ChainModel.Id, ownerId: AccountId) -> NSPredicate { let chainPredicate = NSPredicate(format: "%K == %@", #keyPath(CDNft.chainId), chainId) let ownerPredicate = NSPredicate(format: "%K == %@", #keyPath(CDNft.ownerId), ownerId.toHex()) @@ -245,4 +278,35 @@ extension NSPredicate { static func filterAuthorizedDApps(by metaId: String) -> NSPredicate { NSPredicate(format: "%K == %@", #keyPath(CDDAppSettings.metaId), metaId) } + + static func crowdloanContribution( + for chainId: ChainModel.Id, + accountId: AccountId, + source: String? + ) -> NSPredicate { + let accountChainPredicate = crowdloanContribution(for: chainId, accountId: accountId) + let sourcePredicate = source.map { + NSPredicate(format: "%K == %@", #keyPath(CDCrowdloanContribution.source), $0) + } ?? NSPredicate(format: "%K = nil", #keyPath(CDCrowdloanContribution.source)) + + return NSCompoundPredicate(andPredicateWithSubpredicates: [accountChainPredicate, sourcePredicate]) + } + + static func crowdloanContribution( + for chainId: ChainModel.Id, + accountId: AccountId + ) -> NSPredicate { + let chainPredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDCrowdloanContribution.chainId), + chainId + ) + let accountPredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDCrowdloanContribution.chainAccountId), + accountId.toHex() + ) + + return NSCompoundPredicate(andPredicateWithSubpredicates: [chainPredicate, accountPredicate]) + } } diff --git a/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift b/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift index ccf4840a38..e55674ee7e 100644 --- a/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift +++ b/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift @@ -3,6 +3,7 @@ import UIKit extension NSCollectionLayoutSection { struct Settings { let estimatedRowHeight: CGFloat + let absoluteHeaderHeight: CGFloat? let estimatedHeaderHeight: CGFloat let sectionContentInsets: NSDirectionalEdgeInsets let sectionInterGroupSpacing: CGFloat @@ -28,7 +29,10 @@ extension NSCollectionLayoutSection { let headerSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), - heightDimension: .estimated(settings.estimatedHeaderHeight) + heightDimension: + settings.absoluteHeaderHeight.map { + .absolute($0) + } ?? .estimated(settings.estimatedHeaderHeight) ) let section = NSCollectionLayoutSection(group: group) section.contentInsets = settings.sectionContentInsets diff --git a/novawallet/Common/Extension/UIKit/UICollectionViewDiffableDataSource+apply.swift b/novawallet/Common/Extension/UIKit/UICollectionViewDiffableDataSource+apply.swift new file mode 100644 index 0000000000..266262718e --- /dev/null +++ b/novawallet/Common/Extension/UIKit/UICollectionViewDiffableDataSource+apply.swift @@ -0,0 +1,13 @@ +import UIKit + +extension UICollectionViewDiffableDataSource where SectionIdentifierType: SectionProtocol { + func apply(_ viewModel: [SectionIdentifierType]) where SectionIdentifierType.CellModel == ItemIdentifierType { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections(viewModel) + viewModel.forEach { section in + snapshot.appendItems(section.cells, toSection: section) + } + + apply(snapshot) + } +} diff --git a/novawallet/Common/Helpers/AccountIdCodingWrapper.swift b/novawallet/Common/Helpers/AccountIdCodingWrapper.swift new file mode 100644 index 0000000000..df750f9b42 --- /dev/null +++ b/novawallet/Common/Helpers/AccountIdCodingWrapper.swift @@ -0,0 +1,16 @@ +import Foundation +import SubstrateSdk + +struct AccountIdCodingWrapper: Decodable { + let wrappedValue: AccountId + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if let rawAccountId = try? container.decode(AccountId.self) { + wrappedValue = rawAccountId + } else { + wrappedValue = try container.decode(BytesCodable.self).wrappedValue + } + } +} diff --git a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift index 0cae4c5749..975752eab1 100644 --- a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift +++ b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift @@ -13,6 +13,11 @@ protocol SubstrateRepositoryFactoryProtocol { func createTxRepository() -> AnyDataProviderRepository func createPhishingRepository() -> AnyDataProviderRepository + func createAssetLocksRepository( + for accountId: AccountId, + chainAssetId: ChainAssetId + ) -> AnyDataProviderRepository + func createChainAddressTxRepository( for address: AccountAddress, chainId: ChainModel.Id @@ -35,6 +40,17 @@ protocol SubstrateRepositoryFactoryProtocol { func createPhishingSitesRepositoryWithPredicate( _ filter: NSPredicate ) -> AnyDataProviderRepository + + func createCrowdloanContributionRepository( + accountId: AccountId, + chainId: ChainModel.Id, + source: String? + ) -> AnyDataProviderRepository + + func createCrowdloanContributionRepository( + accountId: AccountId, + chainId: ChainModel.Id + ) -> AnyDataProviderRepository } final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { @@ -181,4 +197,59 @@ final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { return AnyDataProviderRepository(repository) } + + func createAssetLocksRepository( + for accountId: AccountId, + chainAssetId: ChainAssetId + ) -> AnyDataProviderRepository { + createAssetLocksRepository(.assetLock(for: accountId, chainAssetId: chainAssetId)) + } + + private func createAssetLocksRepository(_ filter: NSPredicate) -> AnyDataProviderRepository { + let mapper = AssetLockMapper() + let repository = storageFacade.createRepository( + filter: filter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + return AnyDataProviderRepository(repository) + } + + func createCrowdloanContributionRepository( + accountId: AccountId, + chainId: ChainModel.Id, + source: String? + ) -> AnyDataProviderRepository { + let filter = NSPredicate.crowdloanContribution( + for: chainId, + accountId: accountId, + source: source + ) + + return createCrowdloanContributionRepository(for: filter) + } + + func createCrowdloanContributionRepository( + accountId: AccountId, + chainId: ChainModel.Id + ) -> AnyDataProviderRepository { + let filter = NSPredicate.crowdloanContribution( + for: chainId, + accountId: accountId + ) + + return createCrowdloanContributionRepository(for: filter) + } + + private func createCrowdloanContributionRepository( + for filter: NSPredicate + ) -> AnyDataProviderRepository { + let mapper = CrowdloanContributionDataMapper() + let repository = storageFacade.createRepository( + filter: filter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + return AnyDataProviderRepository(repository) + } } diff --git a/novawallet/Common/Migration/StorageMigrating+CheckVersion.swift b/novawallet/Common/Migration/StorageMigrating+CheckVersion.swift new file mode 100644 index 0000000000..a368ed647c --- /dev/null +++ b/novawallet/Common/Migration/StorageMigrating+CheckVersion.swift @@ -0,0 +1,64 @@ +import CoreData + +extension StorageMigrating { + typealias Version = CaseIterable & RawRepresentable & Equatable + + func checkIfMigrationNeeded( + to version: T, + storeURL: URL, + fileManager: FileManager, + modelDirectory: String + ) -> Bool where T: Version, T.RawValue == String { + let storageExists = fileManager.fileExists(atPath: storeURL.path) + + guard storageExists else { + return false + } + + guard let metadata = NSPersistentStoreCoordinator.metadata(at: storeURL) else { + return false + } + + let compatibleVersion = T.allCases.first { + let model = createManagedObjectModel(forResource: $0.rawValue, modelDirectory: modelDirectory) + return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + } + + return compatibleVersion != version + } + + func compatibleVersionForStoreMetadata( + _ metadata: [String: Any], + modelDirectory: String + ) -> T? where T: Version, T.RawValue == String { + let compatibleVersion = T.allCases.first { + let model = createManagedObjectModel(forResource: $0.rawValue, modelDirectory: modelDirectory) + return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + } + + return compatibleVersion + } + + func createManagedObjectModel(forResource resource: String, modelDirectory: String) -> NSManagedObjectModel { + let bundle = Bundle.main + let omoURL = bundle.url( + forResource: resource, + withExtension: "omo", + subdirectory: modelDirectory + ) + + let momURL = bundle.url( + forResource: resource, + withExtension: "mom", + subdirectory: modelDirectory + ) + + guard + let modelURL = omoURL ?? momURL, + let model = NSManagedObjectModel(contentsOf: modelURL) else { + fatalError("Unable to load model in bundle for resource \(resource)") + } + + return model + } +} diff --git a/novawallet/Common/Migration/StorageMigrator+Sync.swift b/novawallet/Common/Migration/StorageMigrator+Sync.swift index 42a0218772..bb63b7a46c 100644 --- a/novawallet/Common/Migration/StorageMigrator+Sync.swift +++ b/novawallet/Common/Migration/StorageMigrator+Sync.swift @@ -8,6 +8,18 @@ extension UserStorageMigrator: Migrating { performMigration() - Logger.shared.info("Db migration completed") + Logger.shared.info("User storage migration was completed") + } +} + +extension SubstrateStorageMigrator: Migrating { + func migrate() throws { + guard requiresMigration() else { + return + } + + performMigration() + + Logger.shared.info("Substrate storage migration was completed") } } diff --git a/novawallet/Common/Migration/StorageMigrator.swift b/novawallet/Common/Migration/StorageMigrator.swift index b53addffb8..3bbb603c61 100644 --- a/novawallet/Common/Migration/StorageMigrator.swift +++ b/novawallet/Common/Migration/StorageMigrator.swift @@ -141,51 +141,20 @@ final class UserStorageMigrator { } private func checkIfMigrationNeeded(to version: UserStorageVersion) -> Bool { - let storageExists = fileManager.fileExists(atPath: storeURL.path) - - guard storageExists else { - return false - } - - guard let metadata = NSPersistentStoreCoordinator.metadata(at: storeURL) else { - return false - } - - let compatibleVersion = compatibleVersionForStoreMetadata(metadata) - - return compatibleVersion != version + checkIfMigrationNeeded( + to: version, + storeURL: storeURL, + fileManager: fileManager, + modelDirectory: modelDirectory + ) } private func compatibleVersionForStoreMetadata(_ metadata: [String: Any]) -> UserStorageVersion? { - let compatibleVersion = UserStorageVersion.allCases.first { - let model = createManagedObjectModel(forResource: $0.rawValue) - return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) - } - - return compatibleVersion + compatibleVersionForStoreMetadata(metadata, modelDirectory: modelDirectory) } private func createManagedObjectModel(forResource resource: String) -> NSManagedObjectModel { - let bundle = Bundle.main - let omoURL = bundle.url( - forResource: resource, - withExtension: "omo", - subdirectory: modelDirectory - ) - - let momURL = bundle.url( - forResource: resource, - withExtension: "mom", - subdirectory: modelDirectory - ) - - guard - let modelURL = omoURL ?? momURL, - let model = NSManagedObjectModel(contentsOf: modelURL) else { - fatalError("Unable to load model in bundle for resource \(resource)") - } - - return model + createManagedObjectModel(forResource: resource, modelDirectory: modelDirectory) } private func createMapping( diff --git a/novawallet/Common/Migration/SubstrateStorageMigrator.swift b/novawallet/Common/Migration/SubstrateStorageMigrator.swift new file mode 100644 index 0000000000..c8414a12fc --- /dev/null +++ b/novawallet/Common/Migration/SubstrateStorageMigrator.swift @@ -0,0 +1,69 @@ +import Foundation +import CoreData + +final class SubstrateStorageMigrator { + let modelDirectory: String + let model: SubstrateStorageVersion + let storeURL: URL + let fileManager: FileManager + + init( + storeURL: URL, + modelDirectory: String, + model: SubstrateStorageVersion, + fileManager: FileManager + ) { + self.storeURL = storeURL + self.model = model + self.modelDirectory = modelDirectory + self.fileManager = fileManager + } +} + +// MARK: - StorageMigrating + +extension SubstrateStorageMigrator: StorageMigrating { + func requiresMigration() -> Bool { + checkIfMigrationNeeded( + to: SubstrateStorageVersion.current, + storeURL: storeURL, + fileManager: fileManager, + modelDirectory: modelDirectory + ) + } + + func performMigration() { + let destinationVersion = SubstrateStorageVersion.current + + let mom = createManagedObjectModel( + forResource: destinationVersion.rawValue, + modelDirectory: modelDirectory + ) + + let psc = NSPersistentStoreCoordinator(managedObjectModel: mom) + let options = [ + NSMigratePersistentStoresAutomaticallyOption: true, + NSInferMappingModelAutomaticallyOption: true + ] + do { + try psc.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: options + ) + } catch { + fatalError("Failed to add persistent store: \(error)") + } + } + + func migrate(_ completion: @escaping () -> Void) { + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + self?.performMigration() + + DispatchQueue.main.async { + completion() + } + } + } +} diff --git a/novawallet/Common/Migration/SubstrateStorageVersion.swift b/novawallet/Common/Migration/SubstrateStorageVersion.swift new file mode 100644 index 0000000000..70e23a87f5 --- /dev/null +++ b/novawallet/Common/Migration/SubstrateStorageVersion.swift @@ -0,0 +1,17 @@ +enum SubstrateStorageVersion: String, CaseIterable { + case version1 = "SubstrateDataModel" + case version2 = "SubstrateDataModel2" + + static var current: SubstrateStorageVersion { + allCases.last! + } + + var nextVersion: SubstrateStorageVersion? { + switch self { + case .version1: + return .version2 + case .version2: + return nil + } + } +} diff --git a/novawallet/Common/Model/AmountInputResult.swift b/novawallet/Common/Model/AmountInputResult.swift index 326a424bac..58e95c76f3 100644 --- a/novawallet/Common/Model/AmountInputResult.swift +++ b/novawallet/Common/Model/AmountInputResult.swift @@ -12,4 +12,13 @@ enum AmountInputResult { return value } } + + var isMax: Bool { + switch self { + case let .rate(value): + return value == 1 + case .absolute: + return false + } + } } diff --git a/novawallet/Common/Model/AssetBalance.swift b/novawallet/Common/Model/AssetBalance.swift index eb87b6325e..6e3171623f 100644 --- a/novawallet/Common/Model/AssetBalance.swift +++ b/novawallet/Common/Model/AssetBalance.swift @@ -11,6 +11,7 @@ struct AssetBalance: Equatable { var totalInPlank: BigUInt { freeInPlank + reservedInPlank } var transferable: BigUInt { freeInPlank > frozenInPlank ? freeInPlank - frozenInPlank : 0 } + var locked: BigUInt { frozenInPlank + reservedInPlank } } extension AssetBalance: Identifiable { diff --git a/novawallet/Common/Model/AssetLock.swift b/novawallet/Common/Model/AssetLock.swift new file mode 100644 index 0000000000..b0fa19f029 --- /dev/null +++ b/novawallet/Common/Model/AssetLock.swift @@ -0,0 +1,52 @@ +import BigInt +import RobinHood + +struct AssetLock: Equatable { + let chainAssetId: ChainAssetId + let accountId: AccountId + let type: Data + let amount: BigUInt + + var lockType: LockType? { + guard let typeString = displayId else { + return nil + } + return LockType(rawValue: typeString.lowercased()) + } + + var displayId: String? { + String(data: type, encoding: .utf8)?.trimmingCharacters(in: .whitespaces) + } +} + +extension AssetLock: Identifiable { + static func createIdentifier( + for chainAssetId: ChainAssetId, + accountId: AccountId, + type: Data + ) -> String { + let data = [ + chainAssetId.stringValue, + accountId.toHex(), + type.toUTF8String()! + ].joined(separator: "-").data(using: .utf8)! + return data.sha256().toHex() + } + + var identifier: String { + Self.createIdentifier(for: chainAssetId, accountId: accountId, type: type) + } +} + +extension AssetLock: CustomDebugStringConvertible { + var debugDescription: String { + [ + "ChainAsset: \(chainAssetId.stringValue)", + "AccountId: \(accountId.toHex())", + "Type: \(type.toUTF8String() ?? "")", + "Amount: \(amount)" + ].joined(separator: "\n") + } +} + +extension AssetLock: Codable {} diff --git a/novawallet/Common/Model/BalanceLocks+Sort.swift b/novawallet/Common/Model/AssetLocks+Sort.swift similarity index 54% rename from novawallet/Common/Model/BalanceLocks+Sort.swift rename to novawallet/Common/Model/AssetLocks+Sort.swift index b2f562f197..58424c44d4 100644 --- a/novawallet/Common/Model/BalanceLocks+Sort.swift +++ b/novawallet/Common/Model/AssetLocks+Sort.swift @@ -1,19 +1,19 @@ import Foundation -typealias BalanceLocks = [BalanceLock] +typealias AssetLocks = [AssetLock] -extension BalanceLocks { - func mainLocks() -> BalanceLocks { +extension AssetLocks { + func mainLocks() -> AssetLocks { LockType.locksOrder.compactMap { lockType in self.first(where: { lock in - lock.displayId == lockType.rawValue + lock.lockType == lockType }) } } - func auxLocks() -> BalanceLocks { + func auxLocks() -> AssetLocks { compactMap { lock in - guard LockType(rawValue: lock.displayId ?? "") != nil else { + guard lock.lockType != nil else { return lock } diff --git a/novawallet/Common/Model/AssetStorageInfo.swift b/novawallet/Common/Model/AssetStorageInfo.swift index 24ca5d049b..f93b1d4164 100644 --- a/novawallet/Common/Model/AssetStorageInfo.swift +++ b/novawallet/Common/Model/AssetStorageInfo.swift @@ -6,10 +6,18 @@ enum AssetStorageInfoError: Error { case unexpectedTypeExtras } +struct OrmlTokenStorageInfo { + let currencyId: JSON + let currencyData: Data + let module: String + let existentialDeposit: BigUInt + let canTransferAll: Bool +} + enum AssetStorageInfo { - case native + case native(canTransferAll: Bool) case statemine(extras: StatemineAssetExtras) - case orml(currencyId: JSON, currencyData: Data, module: String, existentialDeposit: BigUInt) + case orml(info: OrmlTokenStorageInfo) } extension AssetStorageInfo { @@ -25,31 +33,9 @@ extension AssetStorageInfo { throw AssetStorageInfoError.unexpectedTypeExtras } - let rawCurrencyId = try Data(hexString: extras.currencyIdScale) - - let decoder = try codingFactory.createDecoder(from: rawCurrencyId) - let currencyId = try decoder.read(type: extras.currencyIdType) + let info = try createOrmlStorageInfo(from: extras, codingFactory: codingFactory) - let moduleName: String - - let tokensTransfer = CallCodingPath.tokensTransfer - if codingFactory.metadata.getCall( - from: tokensTransfer.moduleName, - with: tokensTransfer.callName - ) != nil { - moduleName = tokensTransfer.moduleName - } else { - moduleName = CallCodingPath.currenciesTransfer.moduleName - } - - let existentialDeposit = BigUInt(extras.existentialDeposit) ?? 0 - - return .orml( - currencyId: currencyId, - currencyData: rawCurrencyId, - module: moduleName, - existentialDeposit: existentialDeposit - ) + return .orml(info: info) case .statemine: guard let extras = try asset.typeExtras?.map(to: StatemineAssetExtras.self) else { throw AssetStorageInfoError.unexpectedTypeExtras @@ -57,7 +43,53 @@ extension AssetStorageInfo { return .statemine(extras: extras) case .none: - return .native + let call = CallCodingPath.transferAll + let canTransferAll = codingFactory.metadata.getCall( + from: call.moduleName, + with: call.callName + ) != nil + return .native(canTransferAll: canTransferAll) + } + } + + private static func createOrmlStorageInfo( + from extras: OrmlTokenExtras, + codingFactory: RuntimeCoderFactoryProtocol + ) throws -> OrmlTokenStorageInfo { + let rawCurrencyId = try Data(hexString: extras.currencyIdScale) + + let decoder = try codingFactory.createDecoder(from: rawCurrencyId) + let currencyId = try decoder.read(type: extras.currencyIdType) + + let moduleName: String + + let tokensTransfer = CallCodingPath.tokensTransfer + let transferAllPath: CallCodingPath + + if codingFactory.metadata.getCall( + from: tokensTransfer.moduleName, + with: tokensTransfer.callName + ) != nil { + moduleName = tokensTransfer.moduleName + transferAllPath = CallCodingPath.tokensTransferAll + } else { + moduleName = CallCodingPath.currenciesTransfer.moduleName + transferAllPath = CallCodingPath.currenciesTransferAll } + + let existentialDeposit = BigUInt(extras.existentialDeposit) ?? 0 + + let canTransferAll = codingFactory.metadata.getCall( + from: transferAllPath.moduleName, + with: transferAllPath.callName + ) != nil + + return OrmlTokenStorageInfo( + currencyId: currencyId, + currencyData: rawCurrencyId, + module: moduleName, + existentialDeposit: existentialDeposit, + canTransferAll: canTransferAll + ) } } diff --git a/novawallet/Common/Model/ChainRegistry/ChainModel.swift b/novawallet/Common/Model/ChainRegistry/ChainModel.swift index 79d3c80d18..4abad4b479 100644 --- a/novawallet/Common/Model/ChainRegistry/ChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/ChainModel.swift @@ -95,7 +95,7 @@ struct ChainModel: Equatable, Codable, Hashable { addressPrefix = remoteModel.addressPrefix types = remoteModel.types icon = remoteModel.icon - options = remoteModel.options + options = remoteModel.options?.compactMap { ChainOptions(rawValue: $0) } externalApi = remoteModel.externalApi explorers = remoteModel.explorers additional = remoteModel.additional diff --git a/novawallet/Common/Model/ChainRegistry/RemoteChainModel.swift b/novawallet/Common/Model/ChainRegistry/RemoteChainModel.swift index 40767fb482..0fad648b4f 100644 --- a/novawallet/Common/Model/ChainRegistry/RemoteChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/RemoteChainModel.swift @@ -11,7 +11,7 @@ struct RemoteChainModel: Equatable, Codable, Hashable { let addressPrefix: UInt16 let types: ChainModel.TypesSettings? let icon: URL - let options: [ChainOptions]? + let options: [String]? let externalApi: ChainModel.ExternalApiSet? let explorers: [ChainModel.Explorer]? let additional: JSON? diff --git a/novawallet/Common/Network/Wallet/WalletNetworkFacade+BalanceLocks.swift b/novawallet/Common/Network/Wallet/WalletNetworkFacade+BalanceLocks.swift deleted file mode 100644 index 25e56409eb..0000000000 --- a/novawallet/Common/Network/Wallet/WalletNetworkFacade+BalanceLocks.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Foundation -import RobinHood -import SubstrateSdk - -extension WalletNetworkFacade { - func createBalanceLocksFetchOperation( - for accountId: AccountId, - asset: AssetModel, - chainId: ChainModel.Id, - chainFormat: ChainFormat - ) -> CompoundOperationWrapper { - if let rawType = asset.type { - switch AssetType(rawValue: rawType) { - case .none, .statemine: - return CompoundOperationWrapper.createWithResult(nil) - case .orml: - return createOrmlBalanceLocksWrapper( - for: accountId, - asset: asset, - chainId: chainId - ) - } - } else { - return createNativeBalanceLocksWrapper( - for: accountId, - chainId: chainId, - chainFormat: chainFormat - ) - } - } - - private func createNativeBalanceLocksWrapper( - for accountId: AccountId, - chainId: ChainModel.Id, - chainFormat: ChainFormat - ) -> CompoundOperationWrapper { - let operationManager = OperationManagerFacade.sharedManager - - let requestFactory = StorageRequestFactory( - remoteFactory: StorageKeyFactory(), - operationManager: operationManager - ) - - guard let connection = chainRegistry.getConnection(for: chainId) else { - return CompoundOperationWrapper.createWithError(ChainRegistryError.connectionUnavailable) - } - - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { - return CompoundOperationWrapper.createWithError(ChainRegistryError.runtimeMetadaUnavailable) - } - - let coderFactoryOperation = runtimeService.fetchCoderFactoryOperation() - - let wrapper: CompoundOperationWrapper<[StorageResponse]> - - switch chainFormat { - case .substrate: - wrapper = requestFactory.queryItems( - engine: connection, - keyParams: { [accountId] }, - factory: { try coderFactoryOperation.extractNoCancellableResultData() }, - storagePath: StorageCodingPath.balanceLocks - ) - case .ethereum: - wrapper = requestFactory.queryItems( - engine: connection, - keyParams: { [accountId.map { StringScaleMapper(value: $0) }] }, - factory: { try coderFactoryOperation.extractNoCancellableResultData() }, - storagePath: StorageCodingPath.balanceLocks - ) - } - - let mapOperation = ClosureOperation { - try wrapper.targetOperation.extractNoCancellableResultData().first?.value - } - - wrapper.allOperations.forEach { $0.addDependency(coderFactoryOperation) } - - let dependencies = [coderFactoryOperation] + wrapper.allOperations - - dependencies.forEach { mapOperation.addDependency($0) } - - return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: dependencies) - } - - private func createOrmlBalanceLocksWrapper( - for accountId: AccountId, - asset: AssetModel, - chainId: ChainModel.Id - ) -> CompoundOperationWrapper { - guard - let extras = try? asset.typeExtras?.map(to: OrmlTokenExtras.self), - let currencyId = try? Data(hexString: extras.currencyIdScale) else { - return CompoundOperationWrapper.createWithResult(nil) - } - - let operationManager = OperationManagerFacade.sharedManager - - let storageKeyFactory = StorageKeyFactory() - let requestFactory = StorageRequestFactory( - remoteFactory: StorageKeyFactory(), - operationManager: operationManager - ) - - guard let connection = chainRegistry.getConnection(for: chainId) else { - return CompoundOperationWrapper.createWithError(ChainRegistryError.connectionUnavailable) - } - - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { - return CompoundOperationWrapper.createWithError(ChainRegistryError.runtimeMetadaUnavailable) - } - - let coderFactoryOperation = runtimeService.fetchCoderFactoryOperation() - - let storagePath = StorageCodingPath.ormlTokenLocks - let keyEncodingOperation = DoubleMapKeyEncodingOperation( - path: storagePath, - storageKeyFactory: storageKeyFactory, - keyParams1: [accountId], - keyParams2: [currencyId], - param1Encoder: nil, - param2Encoder: { $0 } - ) - - keyEncodingOperation.configurationBlock = { - do { - keyEncodingOperation.codingFactory = try coderFactoryOperation - .extractNoCancellableResultData() - } catch { - keyEncodingOperation.result = .failure(error) - } - } - - let wrapper: CompoundOperationWrapper<[StorageResponse]> = requestFactory.queryItems( - engine: connection, - keys: { try keyEncodingOperation.extractNoCancellableResultData() }, - factory: { try coderFactoryOperation.extractNoCancellableResultData() }, - storagePath: storagePath - ) - - let mapOperation = ClosureOperation { - try wrapper.targetOperation.extractNoCancellableResultData().first?.value - } - - keyEncodingOperation.addDependency(coderFactoryOperation) - wrapper.addDependency(operations: [keyEncodingOperation]) - - let dependencies = [coderFactoryOperation, keyEncodingOperation] + wrapper.allOperations - - dependencies.forEach { mapOperation.addDependency($0) } - - return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: dependencies) - } -} diff --git a/novawallet/Common/Network/Wallet/WalletNetworkFacade+Storage.swift b/novawallet/Common/Network/Wallet/WalletNetworkFacade+Storage.swift index d31e2eb2ba..8a7da6ef2f 100644 --- a/novawallet/Common/Network/Wallet/WalletNetworkFacade+Storage.swift +++ b/novawallet/Common/Network/Wallet/WalletNetworkFacade+Storage.swift @@ -2,6 +2,7 @@ import Foundation import CommonWallet import RobinHood import SubstrateSdk +import BigInt extension WalletNetworkFacade { func fetchBalanceInfoForAsset( @@ -36,21 +37,30 @@ extension WalletNetworkFacade { options: RepositoryFetchOptions() ) - let balanceLocksWrapper: CompoundOperationWrapper<[BalanceLock]?> = - createBalanceLocksFetchOperation( - for: selectedAccount.accountId, - asset: remoteAsset, - chainId: chain.chainId, - chainFormat: chain.chainFormat - ) + let locksRepository = repositoryFactory.createAssetLocksRepository( + for: selectedAccount.accountId, + chainAssetId: ChainAssetId(chainId: chain.chainId, assetId: remoteAsset.assetId) + ) + + let contributionsRepository = repositoryFactory.createCrowdloanContributionRepository( + accountId: selectedAccount.accountId, + chainId: chain.chainId + ) + + let balanceLocksOperation = locksRepository.fetchAllOperation(with: RepositoryFetchOptions()) + + let crowdloanContributionsOperation = contributionsRepository.fetchAllOperation( + with: RepositoryFetchOptions() + ) let mappingOperation = createBalanceMappingOperation( asset: asset, dependingOn: balanceOperation, - balanceLocksWrapper: balanceLocksWrapper + balanceLocksOperation: balanceLocksOperation, + crowdloanContributionsOperation: crowdloanContributionsOperation ) - let storageOperations = [balanceOperation] + balanceLocksWrapper.allOperations + let storageOperations = [balanceOperation, balanceLocksOperation, crowdloanContributionsOperation] storageOperations.forEach { storageOperation in storageOperation.addDependency(codingFactoryOperation) @@ -82,7 +92,8 @@ extension WalletNetworkFacade { private func createBalanceMappingOperation( asset: WalletAsset, dependingOn balanceOperation: BaseOperation, - balanceLocksWrapper: CompoundOperationWrapper<[BalanceLock]?> + balanceLocksOperation: BaseOperation<[AssetLock]>, + crowdloanContributionsOperation: BaseOperation<[CrowdloanContributionData]> ) -> BaseOperation { ClosureOperation { let maybeAssetBalance = try balanceOperation.extractNoCancellableResultData() @@ -90,10 +101,17 @@ extension WalletNetworkFacade { if let assetBalance = maybeAssetBalance { context = context.byChangingAssetBalance(assetBalance, precision: asset.precision) + } + + let balanceLocks = try balanceLocksOperation.extractNoCancellableResultData() + context = context.byChangingBalanceLocks(balanceLocks) + + let contributions = try crowdloanContributionsOperation.extractNoCancellableResultData() + + let contributionsInPlank = contributions.reduce(BigUInt(0)) { $0 + $1.amount } - if let balanceLocks = try? balanceLocksWrapper.targetOperation.extractNoCancellableResultData() { - context = context.byChangingBalanceLocks(balanceLocks) - } + if let contributionsDecimal = Decimal.fromSubstrateAmount(contributionsInPlank, precision: asset.precision) { + context = context.byChangingCrowdloans(contributionsDecimal) } let balance = BalanceData( diff --git a/novawallet/Common/Services/BaseSyncService.swift b/novawallet/Common/Services/BaseSyncService.swift index e1d5efc2ed..48a9b020f0 100644 --- a/novawallet/Common/Services/BaseSyncService.swift +++ b/novawallet/Common/Services/BaseSyncService.swift @@ -2,6 +2,12 @@ import Foundation import RobinHood import SubstrateSdk +protocol SyncServiceProtocol { + func syncUp() + func stopSyncUp() + func setup() +} + class BaseSyncService { let retryStrategy: ReconnectionStrategyProtocol let logger: LoggerProtocol? @@ -124,3 +130,21 @@ extension BaseSyncService: SchedulerDelegate { performSyncUp() } } + +extension BaseSyncService: SyncServiceProtocol { + func syncUp() { + mutex.lock() + + defer { + mutex.unlock() + } + + guard isActive, !isSyncing else { + return + } + + isSyncing = true + + performSyncUp() + } +} diff --git a/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift b/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift index 29e9ab4181..d633ee479c 100644 --- a/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift +++ b/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift @@ -1,5 +1,7 @@ import Foundation -final class ChainRegistryFacade { +typealias ChainRegistryLazyClosure = () -> ChainRegistryProtocol + +enum ChainRegistryFacade { static let sharedRegistry: ChainRegistryProtocol = ChainRegistryFactory.createDefaultRegistry() } diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionData.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionData.swift new file mode 100644 index 0000000000..0a809aa4c0 --- /dev/null +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionData.swift @@ -0,0 +1,46 @@ +import BigInt +import RobinHood + +struct CrowdloanContributionData { + let accountId: AccountId + let chainId: ChainModel.Id + let paraId: ParaId + let source: String? + let amount: BigUInt + + var type: SourceType { + if let source = source, !source.isEmpty { + return .offChain + } else { + return .onChain + } + } + + enum SourceType: String { + case onChain + case offChain + } +} + +extension CrowdloanContributionData: Identifiable { + var identifier: String { + Self.createIdentifier(for: chainId, accountId: accountId, paraId: paraId, source: source) + } + + static func createIdentifier( + for chainId: ChainModel.Id, + accountId: AccountId, + paraId: ParaId, + source: String? + ) -> String { + let data = [ + chainId, + accountId.toHex(), + paraId.toHex(), + source + ].compactMap { $0 } + .joined(separator: "-") + .data(using: .utf8)! + return data.sha256().toHex() + } +} diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift new file mode 100644 index 0000000000..91d6c51289 --- /dev/null +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift @@ -0,0 +1,72 @@ +import Foundation +import RobinHood + +final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { + typealias Model = CrowdloanContributionData + typealias CommitNotificationBlock = ((Result?) -> Void) + + let syncServices: [SyncServiceProtocol] + let chainId: ChainModel.Id + let accountId: AccountId + let eventCenter: EventCenterProtocol + + init( + syncServices: [SyncServiceProtocol], + chainId: ChainModel.Id, + accountId: AccountId, + eventCenter: EventCenterProtocol + ) { + self.syncServices = syncServices + self.eventCenter = eventCenter + self.chainId = chainId + self.accountId = accountId + + self.eventCenter.add(observer: self) + + syncServices.forEach { + $0.setup() + } + } + + func fetchHistory( + runningIn queue: DispatchQueue?, + commitNotificationBlock: CommitNotificationBlock? + ) { + guard let closure = commitNotificationBlock else { + return + } + + let result: Result = Result.success(0) + + dispatchInQueueWhenPossible(queue) { + closure(result) + } + } + + func refresh( + runningIn queue: DispatchQueue?, + commitNotificationBlock: CommitNotificationBlock? + ) { + syncServices.forEach { + $0.syncUp() + } + + guard let closure = commitNotificationBlock else { + return + } + + let result: Result = Result.success(0) + dispatchInQueueWhenPossible(queue) { + closure(result) + } + } +} + +extension CrowdloanContributionStreamableSource: EventVisitorProtocol { + func processAssetBalanceChanged(event: AssetBalanceChanged) { + guard event.accountId == accountId, event.chainAssetId.chainId == chainId else { + return + } + refresh(runningIn: nil, commitNotificationBlock: nil) + } +} diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanOffChainSyncService.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanOffChainSyncService.swift new file mode 100644 index 0000000000..1bfced4173 --- /dev/null +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanOffChainSyncService.swift @@ -0,0 +1,118 @@ +import RobinHood + +final class CrowdloanOffChainSyncService: BaseSyncService { + private let source: ExternalContributionSourceProtocol + private let operationManager: OperationManagerProtocol + private let repository: AnyDataProviderRepository + private var syncOperationWrapper: CompoundOperationWrapper? + private let chain: ChainModel + private let accountId: AccountId + + init( + source: ExternalContributionSourceProtocol, + chain: ChainModel, + accountId: AccountId, + operationManager: OperationManagerProtocol, + repository: AnyDataProviderRepository, + logger: LoggerProtocol? + ) { + self.source = source + self.operationManager = operationManager + self.repository = repository + self.chain = chain + self.accountId = accountId + + super.init(logger: logger) + } + + private func contributionsFetchOperation( + accountId: AccountId, + chain: ChainModel + ) -> CompoundOperationWrapper<[ExternalContribution]> { + source.getContributions(accountId: accountId, chain: chain) + } + + private func createChangesOperationWrapper( + dependingOn contributionsOperation: CompoundOperationWrapper<[ExternalContribution]>, + chainId: ChainModel.Id, + accountId: AccountId + ) -> BaseOperation<[DataProviderChange]?> { + let changesOperation = ClosureOperation<[DataProviderChange]?> { + let contributions = try contributionsOperation.targetOperation.extractNoCancellableResultData() + + let remoteModels: [CrowdloanContributionData] = contributions.compactMap { + CrowdloanContributionData( + accountId: accountId, + chainId: chainId, + paraId: $0.paraId, + source: $0.source, + amount: $0.amount + ) + } + + return remoteModels.map(DataProviderChange.update) + } + + changesOperation.addDependency(contributionsOperation.targetOperation) + + return changesOperation + } + + private func createSaveOperation( + dependingOn operation: BaseOperation<[DataProviderChange]?> + ) -> BaseOperation { + let replaceOperation = repository.replaceOperation { + guard let changes = try operation.extractNoCancellableResultData() else { + return [] + } + return changes.compactMap(\.item) + } + + replaceOperation.addDependency(operation) + return replaceOperation + } + + override func performSyncUp() { + let contributionsFetchOperation = contributionsFetchOperation( + accountId: accountId, + chain: chain + ) + + let changesWrapper = createChangesOperationWrapper( + dependingOn: contributionsFetchOperation, + chainId: chain.chainId, + accountId: accountId + ) + let saveOperation = createSaveOperation(dependingOn: changesWrapper) + + saveOperation.completionBlock = { + guard !saveOperation.isCancelled else { + return + } + + do { + try saveOperation.extractNoCancellableResultData() + self.syncOperationWrapper = nil + self.complete(nil) + } catch { + self.syncOperationWrapper = nil + self.complete(error) + } + } + + let operations = contributionsFetchOperation.allOperations + [changesWrapper] + + let syncWrapper = CompoundOperationWrapper( + targetOperation: saveOperation, + dependencies: operations + ) + + syncOperationWrapper = syncWrapper + operationManager.enqueue(operations: syncWrapper.allOperations, in: .transient) + } + + override func stopSyncUp() { + syncOperationWrapper?.cancel() + syncOperationWrapper = nil + } +} diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanOnChainSyncService.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanOnChainSyncService.swift new file mode 100644 index 0000000000..319c66498e --- /dev/null +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanOnChainSyncService.swift @@ -0,0 +1,179 @@ +import SubstrateSdk +import RobinHood + +final class CrowdloanOnChainSyncService: BaseSyncService { + private let operationFactory: CrowdloanOperationFactoryProtocol + private let chainRegistry: ChainRegistryProtocol + private let operationManager: OperationManagerProtocol + private let accountId: AccountId + private let chainId: ChainModel.Id + private let repository: AnyDataProviderRepository + private var syncOperationWrapper: CompoundOperationWrapper? + + init( + operationFactory: CrowdloanOperationFactoryProtocol, + chainRegistry: ChainRegistryProtocol, + repository: AnyDataProviderRepository, + accountId: AccountId, + chainId: ChainModel.Id, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol? + ) { + self.operationFactory = operationFactory + self.chainRegistry = chainRegistry + self.repository = repository + self.accountId = accountId + self.chainId = chainId + self.operationManager = operationManager + + super.init(logger: logger) + } + + private func contributionsFetchOperation( + dependingOn fetchCrowdloansOperation: CompoundOperationWrapper<[Crowdloan]>, + connection: ChainConnection, + runtimeService: RuntimeProviderProtocol, + accountId: AccountId + ) -> BaseOperation<[RemoteCrowdloanContribution]> { + let contributionsOperation: BaseOperation<[RemoteCrowdloanContribution]> = + OperationCombiningService(operationManager: operationManager) { [weak self] in + guard let self = self else { + return [] + } + + let crowdloans = try fetchCrowdloansOperation.targetOperation.extractNoCancellableResultData() + + return crowdloans.map { crowdloan in + let fetchOperation = self.operationFactory.fetchContributionOperation( + connection: connection, + runtimeService: runtimeService, + accountId: accountId, + index: crowdloan.fundInfo.index + ) + + let mapOperation = ClosureOperation { + let contributionResponse = try fetchOperation.targetOperation.extractNoCancellableResultData() + + return RemoteCrowdloanContribution( + crowdloan: crowdloan, + contribution: contributionResponse.contribution + ) + } + + mapOperation.addDependency(fetchOperation.targetOperation) + + return CompoundOperationWrapper( + targetOperation: mapOperation, + dependencies: fetchOperation.allOperations + ) + } + }.longrunOperation() + + contributionsOperation.addDependency(fetchCrowdloansOperation.targetOperation) + + return contributionsOperation + } + + private func createChangesOperationWrapper( + dependingOn contributionsOperation: BaseOperation<[RemoteCrowdloanContribution]>, + chainId: ChainModel.Id, + accountId: AccountId + ) -> BaseOperation<[DataProviderChange]?> { + let changesOperation = ClosureOperation<[DataProviderChange]?> { + let contributions = try contributionsOperation + .extractNoCancellableResultData() + + let remoteModels: [CrowdloanContributionData] = contributions.compactMap { + guard let contribution = $0.contribution else { + return nil + } + return CrowdloanContributionData( + accountId: accountId, + chainId: chainId, + paraId: $0.crowdloan.paraId, + source: nil, + amount: contribution.balance + ) + } + + return remoteModels.map(DataProviderChange.update) + } + + changesOperation.addDependency(contributionsOperation) + + return changesOperation + } + + private func createSaveOperation( + dependingOn operation: BaseOperation<[DataProviderChange]?> + ) -> BaseOperation { + let replaceOperation = repository.replaceOperation { + guard let changes = try operation.extractNoCancellableResultData() else { + return [] + } + return changes.compactMap(\.item) + } + + replaceOperation.addDependency(operation) + return replaceOperation + } + + override func performSyncUp() { + guard let connection = chainRegistry.getConnection(for: chainId) else { + logger?.error("Connection for chainId: \(chainId) is unavailable") + complete(ChainRegistryError.connectionUnavailable) + return + } + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + logger?.error("Runtime metadata for chainId: \(chainId) is unavailable") + complete(ChainRegistryError.runtimeMetadaUnavailable) + return + } + + let fetchCrowdloansOperation = operationFactory.fetchCrowdloansOperation( + connection: connection, + runtimeService: runtimeService + ) + let contributionsFetchOperation = contributionsFetchOperation( + dependingOn: fetchCrowdloansOperation, + connection: connection, + runtimeService: runtimeService, + accountId: accountId + ) + let changesWrapper = createChangesOperationWrapper( + dependingOn: contributionsFetchOperation, + chainId: chainId, + accountId: accountId + ) + let saveOperation = createSaveOperation(dependingOn: changesWrapper) + + saveOperation.completionBlock = { + guard !saveOperation.isCancelled else { + return + } + + do { + try saveOperation.extractNoCancellableResultData() + self.syncOperationWrapper = nil + self.complete(nil) + } catch { + self.syncOperationWrapper = nil + self.complete(error) + } + } + + let operations = fetchCrowdloansOperation.allOperations + [contributionsFetchOperation, changesWrapper] + + let syncWrapper = CompoundOperationWrapper( + targetOperation: saveOperation, + dependencies: operations + ) + syncOperationWrapper = syncWrapper + operationManager.enqueue(operations: syncWrapper.allOperations, in: .transient) + } + + override func stopSyncUp() { + syncOperationWrapper?.cancel() + syncOperationWrapper = nil + } +} diff --git a/novawallet/Common/Services/CrowdloanService/RemoteCrowdloanContribution.swift b/novawallet/Common/Services/CrowdloanService/RemoteCrowdloanContribution.swift new file mode 100644 index 0000000000..a04bfdf35f --- /dev/null +++ b/novawallet/Common/Services/CrowdloanService/RemoteCrowdloanContribution.swift @@ -0,0 +1,4 @@ +struct RemoteCrowdloanContribution { + let crowdloan: Crowdloan + let contribution: CrowdloanContribution? +} diff --git a/novawallet/Common/Services/RemoteSubscription/AccountInfoSubscriptionHandlingFactory.swift b/novawallet/Common/Services/RemoteSubscription/AccountInfoSubscriptionHandlingFactory.swift index 9a911a52fb..4789a9f790 100644 --- a/novawallet/Common/Services/RemoteSubscription/AccountInfoSubscriptionHandlingFactory.swift +++ b/novawallet/Common/Services/RemoteSubscription/AccountInfoSubscriptionHandlingFactory.swift @@ -2,27 +2,18 @@ import Foundation import RobinHood final class AccountInfoSubscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol { - let chainAssetId: ChainAssetId - let accountId: AccountId - let chainRegistry: ChainRegistryProtocol - let assetRepository: AnyDataProviderRepository - let eventCenter: EventCenterProtocol - let transactionSubscription: TransactionSubscription? + let accountLocalStorageKey: String + let locksLocalStorageKey: String + let factory: NativeTokenSubscriptionFactoryProtocol init( - chainAssetId: ChainAssetId, - accountId: AccountId, - chainRegistry: ChainRegistryProtocol, - assetRepository: AnyDataProviderRepository, - transactionSubscription: TransactionSubscription?, - eventCenter: EventCenterProtocol + accountLocalStorageKey: String, + locksLocalStorageKey: String, + factory: NativeTokenSubscriptionFactoryProtocol ) { - self.chainAssetId = chainAssetId - self.accountId = accountId - self.chainRegistry = chainRegistry - self.assetRepository = assetRepository - self.transactionSubscription = transactionSubscription - self.eventCenter = eventCenter + self.accountLocalStorageKey = accountLocalStorageKey + self.locksLocalStorageKey = locksLocalStorageKey + self.factory = factory } func createHandler( @@ -32,18 +23,20 @@ final class AccountInfoSubscriptionHandlingFactory: RemoteSubscriptionHandlingFa operationManager: OperationManagerProtocol, logger: LoggerProtocol ) -> StorageChildSubscribing { - AccountInfoSubscription( - chainAssetId: chainAssetId, - accountId: accountId, - chainRegistry: chainRegistry, - assetRepository: assetRepository, - transactionSubscription: transactionSubscription, - remoteStorageKey: remoteStorageKey, - localStorageKey: localStorageKey, - storage: storage, - operationManager: operationManager, - logger: logger, - eventCenter: eventCenter - ) + if locksLocalStorageKey == localStorageKey { + return factory.createBalanceLocksSubscription( + remoteStorageKey: remoteStorageKey, + operationManager: operationManager, + logger: logger + ) + } else { + return factory.createAccountInfoSubscription( + remoteStorageKey: remoteStorageKey, + localStorageKey: localStorageKey, + storage: storage, + operationManager: operationManager, + logger: logger + ) + } } } diff --git a/novawallet/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift b/novawallet/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift index 2ebb980bcd..5a80517701 100644 --- a/novawallet/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift +++ b/novawallet/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift @@ -114,16 +114,22 @@ final class AccountInfoUpdatingService { logger: logger ) + let chainAssetId = ChainAssetId(chainId: chain.chainId, assetId: asset.assetId) let assetBalanceMapper = AssetBalanceMapper() let assetRepository = storageFacade.createRepository(mapper: AnyCoreDataMapper(assetBalanceMapper)) + let locksRepository = repositoryFactory.createAssetLocksRepository( + for: accountId, + chainAssetId: chainAssetId + ) - let subscriptionHandlingFactory = AccountInfoSubscriptionHandlingFactory( - chainAssetId: ChainAssetId(chainId: chain.chainId, assetId: asset.assetId), + let subscriptionHandlingFactory = TokenSubscriptionFactory( + chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, assetRepository: AnyDataProviderRepository(assetRepository), - transactionSubscription: transactionSubscription, - eventCenter: eventCenter + locksRepository: AnyDataProviderRepository(locksRepository), + eventCenter: eventCenter, + transactionSubscription: transactionSubscription ) let maybeSubscriptionId = remoteSubscriptionService.attachToAccountInfo( diff --git a/novawallet/Common/Services/RemoteSubscription/AssetsUpdatingService.swift b/novawallet/Common/Services/RemoteSubscription/AssetsUpdatingService.swift index 2076258a2c..3e01baf07f 100644 --- a/novawallet/Common/Services/RemoteSubscription/AssetsUpdatingService.swift +++ b/novawallet/Common/Services/RemoteSubscription/AssetsUpdatingService.swift @@ -144,9 +144,10 @@ final class AssetsUpdatingService { let assetRepository = repositoryFactory.createAssetBalanceRepository() let chainItemRepository = repositoryFactory.createChainStorageItemRepository() + let chainAssetId = ChainAssetId(chainId: chainId, assetId: asset.assetId) let assetBalanceUpdater = AssetsBalanceUpdater( - chainAssetId: ChainAssetId(chainId: chainId, assetId: asset.assetId), + chainAssetId: chainAssetId, accountId: accountId, extras: assetExtras, chainRegistry: chainRegistry, @@ -185,12 +186,15 @@ final class AssetsUpdatingService { return nil } + let chainAssetId = ChainAssetId(chainId: chainId, assetId: asset.assetId) let assetsRepository = repositoryFactory.createAssetBalanceRepository() - let subscriptionHandlingFactory = OrmlAccountSubscriptionHandlingFactory( - chainAssetId: ChainAssetId(chainId: chainId, assetId: asset.assetId), + let locksRepository = repositoryFactory.createAssetLocksRepository(for: accountId, chainAssetId: chainAssetId) + let subscriptionHandlingFactory = TokenSubscriptionFactory( + chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, assetRepository: assetsRepository, + locksRepository: locksRepository, eventCenter: eventCenter, transactionSubscription: transactionSubscription ) diff --git a/novawallet/Common/Services/RemoteSubscription/OrmlAccountSubcriptionHandlingFactory.swift b/novawallet/Common/Services/RemoteSubscription/OrmlAccountSubcriptionHandlingFactory.swift deleted file mode 100644 index ed2e2fcc2d..0000000000 --- a/novawallet/Common/Services/RemoteSubscription/OrmlAccountSubcriptionHandlingFactory.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation -import RobinHood - -final class OrmlAccountSubscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol { - let chainAssetId: ChainAssetId - let accountId: AccountId - let chainRegistry: ChainRegistryProtocol - let assetRepository: AnyDataProviderRepository - let eventCenter: EventCenterProtocol - let transactionSubscription: TransactionSubscription? - - init( - chainAssetId: ChainAssetId, - accountId: AccountId, - chainRegistry: ChainRegistryProtocol, - assetRepository: AnyDataProviderRepository, - eventCenter: EventCenterProtocol, - transactionSubscription: TransactionSubscription? - ) { - self.chainAssetId = chainAssetId - self.accountId = accountId - self.chainRegistry = chainRegistry - self.assetRepository = assetRepository - self.eventCenter = eventCenter - self.transactionSubscription = transactionSubscription - } - - func createHandler( - remoteStorageKey: Data, - localStorageKey: String, - storage: AnyDataProviderRepository, - operationManager: OperationManagerProtocol, - logger: LoggerProtocol - ) -> StorageChildSubscribing { - OrmlAccountSubscription( - chainAssetId: chainAssetId, - accountId: accountId, - chainRegistry: chainRegistry, - assetRepository: assetRepository, - remoteStorageKey: remoteStorageKey, - localStorageKey: localStorageKey, - storage: storage, - operationManager: operationManager, - logger: logger, - eventCenter: eventCenter, - transactionSubscription: transactionSubscription - ) - } -} diff --git a/novawallet/Common/Services/RemoteSubscription/OrmlTokenSubscriptionHandlingFactory.swift b/novawallet/Common/Services/RemoteSubscription/OrmlTokenSubscriptionHandlingFactory.swift new file mode 100644 index 0000000000..224307ef4d --- /dev/null +++ b/novawallet/Common/Services/RemoteSubscription/OrmlTokenSubscriptionHandlingFactory.swift @@ -0,0 +1,41 @@ +import RobinHood + +final class OrmlTokenSubscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol { + let accountLocalStorageKey: String + let locksLocalStorageKey: String + let factory: OrmlTokenSubscriptionFactoryProtocol + + init( + accountLocalStorageKey: String, + locksLocalStorageKey: String, + factory: OrmlTokenSubscriptionFactoryProtocol + ) { + self.accountLocalStorageKey = accountLocalStorageKey + self.locksLocalStorageKey = locksLocalStorageKey + self.factory = factory + } + + func createHandler( + remoteStorageKey: Data, + localStorageKey: String, + storage: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing { + if locksLocalStorageKey == localStorageKey { + return factory.createOrmLocksSubscription( + remoteStorageKey: remoteStorageKey, + operationManager: operationManager, + logger: logger + ) + } else { + return factory.createOrmlAccountSubscription( + remoteStorageKey: remoteStorageKey, + localStorageKey: localStorageKey, + storage: storage, + operationManager: operationManager, + logger: logger + ) + } + } +} diff --git a/novawallet/Common/Services/RemoteSubscription/TokenSubscriptionFactory.swift b/novawallet/Common/Services/RemoteSubscription/TokenSubscriptionFactory.swift new file mode 100644 index 0000000000..295fc61c0a --- /dev/null +++ b/novawallet/Common/Services/RemoteSubscription/TokenSubscriptionFactory.swift @@ -0,0 +1,145 @@ +import Foundation +import RobinHood +import SubstrateSdk + +protocol OrmlTokenSubscriptionFactoryProtocol { + func createOrmlAccountSubscription( + remoteStorageKey: Data, + localStorageKey: String, + storage: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing + + func createOrmLocksSubscription( + remoteStorageKey: Data, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing +} + +protocol NativeTokenSubscriptionFactoryProtocol { + func createAccountInfoSubscription( + remoteStorageKey: Data, + localStorageKey: String, + storage: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing + + func createBalanceLocksSubscription( + remoteStorageKey: Data, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing +} + +// MARK: - OrmlTokenSubscriptionFactoryProtocol + +final class TokenSubscriptionFactory: OrmlTokenSubscriptionFactoryProtocol { + let chainAssetId: ChainAssetId + let accountId: AccountId + let chainRegistry: ChainRegistryProtocol + let assetRepository: AnyDataProviderRepository + let eventCenter: EventCenterProtocol + let transactionSubscription: TransactionSubscription? + let locksRepository: AnyDataProviderRepository + + init( + chainAssetId: ChainAssetId, + accountId: AccountId, + chainRegistry: ChainRegistryProtocol, + assetRepository: AnyDataProviderRepository, + locksRepository: AnyDataProviderRepository, + eventCenter: EventCenterProtocol, + transactionSubscription: TransactionSubscription? + ) { + self.chainAssetId = chainAssetId + self.accountId = accountId + self.chainRegistry = chainRegistry + self.assetRepository = assetRepository + self.locksRepository = locksRepository + self.eventCenter = eventCenter + self.transactionSubscription = transactionSubscription + } + + func createOrmlAccountSubscription( + remoteStorageKey: Data, + localStorageKey: String, + storage: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing { + OrmlAccountSubscription( + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + assetRepository: assetRepository, + remoteStorageKey: remoteStorageKey, + localStorageKey: localStorageKey, + storage: storage, + operationManager: operationManager, + logger: logger, + eventCenter: eventCenter, + transactionSubscription: transactionSubscription + ) + } + + func createOrmLocksSubscription( + remoteStorageKey: Data, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing { + OrmLocksSubscription( + remoteStorageKey: remoteStorageKey, + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + repository: locksRepository, + operationManager: operationManager, + logger: logger + ) + } +} + +// MARK: - NativeTokenSubscriptionFactoryProtocol + +extension TokenSubscriptionFactory: NativeTokenSubscriptionFactoryProtocol { + func createAccountInfoSubscription( + remoteStorageKey: Data, + localStorageKey: String, + storage: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing { + AccountInfoSubscription( + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + assetRepository: assetRepository, + transactionSubscription: transactionSubscription, + remoteStorageKey: remoteStorageKey, + localStorageKey: localStorageKey, + storage: storage, + operationManager: operationManager, + logger: logger, + eventCenter: eventCenter + ) + } + + func createBalanceLocksSubscription( + remoteStorageKey: Data, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing { + BalanceLocksSubscription( + remoteStorageKey: remoteStorageKey, + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + repository: locksRepository, + operationManager: operationManager, + logger: logger + ) + } +} diff --git a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift index 72a7daeddf..dc9b37391f 100644 --- a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift +++ b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift @@ -1,5 +1,6 @@ import Foundation import SubstrateSdk +import RobinHood protocol WalletRemoteSubscriptionServiceProtocol { // swiftlint:disable:next function_parameter_count @@ -9,7 +10,7 @@ protocol WalletRemoteSubscriptionServiceProtocol { chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol? + subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol? ) -> UUID? func detachFromAccountInfo( @@ -48,7 +49,7 @@ protocol WalletRemoteSubscriptionServiceProtocol { chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol? + subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol? ) -> UUID? // swiftlint:disable:next function_parameter_count @@ -70,46 +71,69 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol? + subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol? ) -> UUID? { do { let storagePath = StorageCodingPath.account - let localKey = try LocalStorageKeyFactory().createFromStoragePath( + let storageKeyFactory = LocalStorageKeyFactory() + let accountLocalKey = try storageKeyFactory.createFromStoragePath( storagePath, accountId: accountId, chainId: chainId ) + let locksStoragePath = StorageCodingPath.balanceLocks + let locksLocalKey = try storageKeyFactory.createFromStoragePath( + locksStoragePath, + encodableElement: accountId, + chainId: chainId + ) + + let accountRequest: SubscriptionRequestProtocol + let locksRequest: SubscriptionRequestProtocol switch chainFormat { case .substrate: - let request = MapSubscriptionRequest( + accountRequest = MapSubscriptionRequest( storagePath: storagePath, - localKey: localKey + localKey: accountLocalKey ) { accountId } - - return attachToSubscription( - with: [request], - chainId: chainId, - cacheKey: localKey, - queue: queue, - closure: closure, - subscriptionHandlingFactory: subscriptionHandlingFactory + locksRequest = MapSubscriptionRequest( + storagePath: .balanceLocks, + localKey: locksLocalKey, + keyParamClosure: { + accountId + } ) case .ethereum: - let request = MapSubscriptionRequest( + accountRequest = MapSubscriptionRequest( storagePath: storagePath, - localKey: localKey + localKey: accountLocalKey ) { accountId.map { StringScaleMapper(value: $0) } } - return attachToSubscription( - with: [request], - chainId: chainId, - cacheKey: localKey, - queue: queue, - closure: closure, - subscriptionHandlingFactory: subscriptionHandlingFactory + locksRequest = MapSubscriptionRequest( + storagePath: .balanceLocks, + localKey: locksLocalKey, + keyParamClosure: { accountId.map { StringScaleMapper(value: $0) } } ) } + + let handlingFactory = subscriptionHandlingFactory.map { + AccountInfoSubscriptionHandlingFactory( + accountLocalStorageKey: accountLocalKey, + locksLocalStorageKey: locksLocalKey, + factory: $0 + ) + } + + return attachToSubscription( + with: [accountRequest, locksRequest], + chainId: chainId, + cacheKey: accountLocalKey + locksLocalKey, + queue: queue, + closure: closure, + subscriptionHandlingFactory: handlingFactory + ) + } catch { callbackClosureIfProvided(closure, queue: queue, result: .failure(error)) return nil @@ -125,12 +149,21 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu ) { do { let storagePath = StorageCodingPath.account - let localKey = try LocalStorageKeyFactory().createFromStoragePath( + let storageKeyFactory = LocalStorageKeyFactory() + let accountLocalKey = try storageKeyFactory.createFromStoragePath( storagePath, accountId: accountId, chainId: chainId ) + let locksStoragePath = StorageCodingPath.balanceLocks + let locksLocalKey = try storageKeyFactory.createFromStoragePath( + locksStoragePath, + encodableElement: accountId, + chainId: chainId + ) + + let localKey = accountLocalKey + locksLocalKey detachFromSubscription(localKey, subscriptionId: subscriptionId, queue: queue, closure: closure) } catch { callbackClosureIfProvided(closure, queue: queue, result: .failure(error)) @@ -194,7 +227,6 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu closure: closure, subscriptionHandlingFactory: handlingFactory ) - } catch { callbackClosureIfProvided(closure, queue: queue, result: .failure(error)) @@ -234,32 +266,52 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol? + subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol? ) -> UUID? { do { - let storagePath = StorageCodingPath.ormlTokenAccount + let storageKeyFactory = LocalStorageKeyFactory() + let accountLocalKey = try storageKeyFactory.createFromStoragePath( + .ormlTokenAccount, + encodableElement: accountId + currencyId, + chainId: chainId + ) - let localKey = try LocalStorageKeyFactory().createFromStoragePath( - storagePath, + let accountRequest = DoubleMapSubscriptionRequest( + storagePath: .ormlTokenAccount, + localKey: accountLocalKey, + keyParamClosure: { (accountId, currencyId) }, + param1Encoder: nil, + param2Encoder: { $0 } + ) + let locksLocalKey = try storageKeyFactory.createFromStoragePath( + .ormlTokenLocks, encodableElement: accountId + currencyId, chainId: chainId ) - let request = DoubleMapSubscriptionRequest( - storagePath: storagePath, - localKey: localKey, + let locksRequest = DoubleMapSubscriptionRequest( + storagePath: .ormlTokenLocks, + localKey: locksLocalKey, keyParamClosure: { (accountId, currencyId) }, param1Encoder: nil, param2Encoder: { $0 } ) + let handlingFactory = subscriptionHandlingFactory.map { + OrmlTokenSubscriptionHandlingFactory( + accountLocalStorageKey: accountLocalKey, + locksLocalStorageKey: locksLocalKey, + factory: $0 + ) + } + return attachToSubscription( - with: [request], + with: [accountRequest, locksRequest], chainId: chainId, - cacheKey: localKey, + cacheKey: accountLocalKey + locksLocalKey, queue: queue, closure: closure, - subscriptionHandlingFactory: subscriptionHandlingFactory + subscriptionHandlingFactory: handlingFactory ) } catch { @@ -278,12 +330,18 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu closure: RemoteSubscriptionClosure? ) { do { - let storagePath = StorageCodingPath.ormlTokenAccount - let localKey = try LocalStorageKeyFactory().createFromStoragePath( - storagePath, + let storageKeyFactory = LocalStorageKeyFactory() + let accountLocalKey = try storageKeyFactory.createFromStoragePath( + .ormlTokenAccount, + encodableElement: accountId + currencyId, + chainId: chainId + ) + let locksLocalKey = try storageKeyFactory.createFromStoragePath( + .ormlTokenLocks, encodableElement: accountId + currencyId, chainId: chainId ) + let localKey = accountLocalKey + locksLocalKey detachFromSubscription(localKey, subscriptionId: subscriptionId, queue: queue, closure: closure) diff --git a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift index ce4d0944f2..460ba43e06 100644 --- a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift +++ b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift @@ -81,11 +81,13 @@ final class WalletRemoteSubscriptionWrapper { completion: RemoteSubscriptionClosure? ) -> UUID? { let assetsRepository = repositoryFactory.createAssetBalanceRepository() - let subscriptionHandlingFactory = OrmlAccountSubscriptionHandlingFactory( + let locksRepository = repositoryFactory.createAssetLocksRepository(for: accountId, chainAssetId: chainAssetId) + let subscriptionHandlingFactory = TokenSubscriptionFactory( chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, assetRepository: assetsRepository, + locksRepository: locksRepository, eventCenter: eventCenter, transactionSubscription: nil ) @@ -107,14 +109,16 @@ final class WalletRemoteSubscriptionWrapper { completion: RemoteSubscriptionClosure? ) -> UUID? { let assetRepository = repositoryFactory.createAssetBalanceRepository() + let locksRepository = repositoryFactory.createAssetLocksRepository(for: accountId, chainAssetId: chainAssetId) - let subscriptionHandlingFactory = AccountInfoSubscriptionHandlingFactory( + let subscriptionHandlingFactory = TokenSubscriptionFactory( chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, assetRepository: assetRepository, - transactionSubscription: nil, - eventCenter: eventCenter + locksRepository: locksRepository, + eventCenter: eventCenter, + transactionSubscription: nil ) return remoteSubscriptionService.attachToAccountInfo( @@ -150,9 +154,9 @@ extension WalletRemoteSubscriptionWrapper: WalletRemoteSubscriptionWrapperProtoc chainAssetId: chainAsset.chainAssetId, completion: completion ) - case let .orml(_, currencyData, _, _): + case let .orml(info): return subscribeOrml( - using: currencyData, + using: info.currencyData, accountId: accountId, chainAssetId: chainAsset.chainAssetId, completion: completion @@ -185,11 +189,11 @@ extension WalletRemoteSubscriptionWrapper: WalletRemoteSubscriptionWrapperProtoc queue: .main, closure: completion ) - case let .orml(_, currencyData, _, _): + case let .orml(info): remoteSubscriptionService.detachFromOrmlToken( for: subscriptionId, accountId: accountId, - currencyId: currencyData, + currencyId: info.currencyData, chainId: chainAssetId.chainId, queue: .main, closure: completion diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift index 3d86384b6c..6d7cfe0b54 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift @@ -175,8 +175,6 @@ final class AccountInfoSubscription: BaseStorageChildSubscription { let maybeItem = try? changesWrapper.targetOperation.extractNoCancellableResultData() if maybeItem != nil { - self?.eventCenter.notify(with: WalletBalanceChanged()) - let assetBalanceChangeEvent = AssetBalanceChanged( chainAssetId: chainAssetId, accountId: accountId, diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/AssetsBalanceUpdater.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/AssetsBalanceUpdater.swift index 1fa5ed58a1..d8079299f1 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/AssetsBalanceUpdater.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/AssetsBalanceUpdater.swift @@ -128,8 +128,6 @@ final class AssetsBalanceUpdater { let maybeItem = try? changesWrapper.targetOperation.extractNoCancellableResultData() if maybeItem != nil { - self?.eventCenter.notify(with: WalletBalanceChanged()) - let assetBalanceChangeEvent = AssetBalanceChanged( chainAssetId: chainAssetId, accountId: accountId, diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift new file mode 100644 index 0000000000..1f1ce045f9 --- /dev/null +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift @@ -0,0 +1,31 @@ +import Foundation +import BigInt +import SubstrateSdk + +extension ExtrinsicProcessor { + func matchBalancesTransferAmount( + from eventRecords: [EventRecord], + metadata: RuntimeMetadataProtocol, + context: RuntimeJsonContext + ) throws -> BigUInt? { + try eventRecords.first { record in + metadata.createEventCodingPath(from: record.event) == .balancesTransfer + }.map { eventRecord in + try eventRecord.event.params.map(to: BalancesTransferEvent.self, with: context.toRawContext()) + }?.amount + } + + func matchOrmlTransferAmount( + from eventRecords: [EventRecord], + metadata: RuntimeMetadataProtocol, + context: RuntimeJsonContext + ) throws -> BigUInt? { + let eventPaths: [EventCodingPath] = [.tokensTransfer, .currenciesTransferred] + return try eventRecords.first { record in + let isTransferAll = metadata.createEventCodingPath(from: record.event).map { eventPaths.contains($0) } + return isTransferAll == true + }.map { eventRecord in + try eventRecord.event.params.map(to: TokenTransferedEvent.self, with: context.toRawContext()) + }?.amount + } +} diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift index f971a53660..44b825cb80 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift @@ -56,7 +56,14 @@ extension ExtrinsicProcessor { with: context.toRawContext() ).accountId - let result = try parseOrmlExtrinsic(extrinsic, sender: maybeSender, context: context) + let eventRecords = eventRecords.filter { $0.extrinsicIndex == extrinsicIndex } + let result = try parseOrmlExtrinsic( + extrinsic, + eventRecords: eventRecords, + metadata: metadata, + sender: maybeSender, + context: context + ) guard result.callPath.isTokensTransfer, result.isAccountMatched, let sender = maybeSender else { return nil @@ -131,19 +138,41 @@ extension ExtrinsicProcessor { private func parseOrmlExtrinsic( _ extrinsic: Extrinsic, + eventRecords: [EventRecord], + metadata: RuntimeMetadataProtocol, sender: AccountId?, context: RuntimeJsonContext ) throws -> OrmlParsingResult { - let call = try extrinsic.call.map( - to: RuntimeCall.self, - with: context.toRawContext() - ) - let callAccountId = call.args.dest.accountId - let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) - let isAccountMatched = accountId == sender || accountId == callAccountId - let currencyId = call.args.currencyId + if + let call = try? extrinsic.call.map( + to: RuntimeCall.self, + with: context.toRawContext() + ) { + let callAccountId = call.args.dest.accountId + let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + let isAccountMatched = accountId == sender || accountId == callAccountId + let currencyId = call.args.currencyId + + return (callPath, isAccountMatched, callAccountId, currencyId, call.args.amount) + } else { + let call = try extrinsic.call.map( + to: RuntimeCall.self, + with: context.toRawContext() + ) + + let callAccountId = call.args.dest.accountId + let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + let isAccountMatched = accountId == sender || accountId == callAccountId + let currencyId = call.args.currencyId - return (callPath, isAccountMatched, callAccountId, currencyId, call.args.amount) + let amount = try? matchOrmlTransferAmount( + from: eventRecords, + metadata: metadata, + context: context + ) + + return (callPath, isAccountMatched, callAccountId, currencyId, amount ?? 0) + } } private func parseEthereumTransact( @@ -361,7 +390,14 @@ extension ExtrinsicProcessor { with: context.toRawContext() ).accountId - let result = try parseBalancesExtrinsic(extrinsic, sender: maybeSender, context: context) + let extrinsicEventRecords = eventRecords.filter { $0.extrinsicIndex == extrinsicIndex } + let result = try parseBalancesExtrinsic( + extrinsic, + eventRecords: extrinsicEventRecords, + metadata: metadata, + sender: maybeSender, + context: context + ) guard result.callPath.isBalancesTransfer, @@ -408,18 +444,35 @@ extension ExtrinsicProcessor { private func parseBalancesExtrinsic( _ extrinsic: Extrinsic, + eventRecords: [EventRecord], + metadata: RuntimeMetadataProtocol, sender: AccountId?, context: RuntimeJsonContext ) throws -> BalancesParsingResult { - let call = try extrinsic.call.map( + if let call = try? extrinsic.call.map( to: RuntimeCall.self, with: context.toRawContext() - ) - let callAccountId = call.args.dest.accountId - let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) - let isAccountMatched = accountId == sender || accountId == callAccountId + ) { + let callAccountId = call.args.dest.accountId + let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + let isAccountMatched = accountId == sender || accountId == callAccountId + + return (callPath, isAccountMatched, callAccountId, call.args.value) + } else { + let call = try extrinsic.call.map(to: RuntimeCall.self, with: context.toRawContext()) + + let callAccountId = call.args.dest.accountId + let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + let isAccountMatched = accountId == sender || accountId == callAccountId - return (callPath, isAccountMatched, callAccountId, call.args.value) + let amount = try? matchBalancesTransferAmount( + from: eventRecords, + metadata: metadata, + context: context + ) + + return (callPath, isAccountMatched, callAccountId, amount ?? 0) + } } func matchExtrinsic( diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/LocksSubscription.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/LocksSubscription.swift new file mode 100644 index 0000000000..3f3601245b --- /dev/null +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/LocksSubscription.swift @@ -0,0 +1,182 @@ +import RobinHood + +final class OrmLocksSubscription: LocksSubscription { + init( + remoteStorageKey: Data, + chainAssetId: ChainAssetId, + accountId: AccountId, + chainRegistry: ChainRegistryProtocol, + repository: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) { + super.init( + storageCodingPath: .ormlTokenLocks, + remoteStorageKey: remoteStorageKey, + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + repository: repository, + operationManager: operationManager, + logger: logger + ) + } +} + +final class BalanceLocksSubscription: LocksSubscription { + init( + remoteStorageKey: Data, + chainAssetId: ChainAssetId, + accountId: AccountId, + chainRegistry: ChainRegistryProtocol, + repository: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) { + super.init( + storageCodingPath: .balanceLocks, + remoteStorageKey: remoteStorageKey, + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + repository: repository, + operationManager: operationManager, + logger: logger + ) + } +} + +class LocksSubscription: StorageChildSubscribing { + var remoteStorageKey: Data + + let chainAssetId: ChainAssetId + let accountId: AccountId + let chainRegistry: ChainRegistryProtocol + let repository: AnyDataProviderRepository + let operationManager: OperationManagerProtocol + let storageCodingPath: StorageCodingPath + let logger: LoggerProtocol + + init( + storageCodingPath: StorageCodingPath, + remoteStorageKey: Data, + chainAssetId: ChainAssetId, + accountId: AccountId, + chainRegistry: ChainRegistryProtocol, + repository: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) { + self.remoteStorageKey = remoteStorageKey + self.chainAssetId = chainAssetId + self.accountId = accountId + self.chainRegistry = chainRegistry + self.repository = repository + self.operationManager = operationManager + self.storageCodingPath = storageCodingPath + self.logger = logger + } + + func processUpdate(_ data: Data?, blockHash _: Data?) { + guard let data = data else { + return + } + logger.debug("Did receive locks update") + + let decodingWrapper = createDecodingOperationWrapper( + data: data, + chainAssetId: chainAssetId + ) + let changesWrapper = createChangesOperationWrapper( + dependingOn: decodingWrapper, + chainAssetId: chainAssetId, + accountId: accountId + ) + + let saveOperation = createSaveOperation(dependingOn: changesWrapper) + + changesWrapper.addDependency(wrapper: decodingWrapper) + saveOperation.addDependency(changesWrapper.targetOperation) + + let operations = decodingWrapper.allOperations + changesWrapper.allOperations + [saveOperation] + + operationManager.enqueue(operations: operations, in: .transient) + } + + private func createDecodingOperationWrapper( + data: Data, + chainAssetId: ChainAssetId + ) -> CompoundOperationWrapper<[BalanceLock]?> { + guard let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAssetId.chainId) else { + logger.error("Runtime metadata unavailable for chain: \(chainAssetId.chainId)") + return CompoundOperationWrapper.createWithError( + ChainRegistryError.runtimeMetadaUnavailable + ) + } + + let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + let decodingOperation = StorageFallbackDecodingOperation<[BalanceLock]>( + path: storageCodingPath, + data: data + ) + + decodingOperation.configurationBlock = { [weak self] in + do { + decodingOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + } catch { + self?.logger.error("Error occur while decoding data: \(error.localizedDescription)") + decodingOperation.result = .failure(error) + } + } + + decodingOperation.addDependency(codingFactoryOperation) + + return CompoundOperationWrapper( + targetOperation: decodingOperation, + dependencies: [codingFactoryOperation] + ) + } + + private func createChangesOperationWrapper( + dependingOn decodingWrapper: CompoundOperationWrapper<[BalanceLock]?>, + chainAssetId: ChainAssetId, + accountId: AccountId + ) -> CompoundOperationWrapper<[DataProviderChange]?> { + let fetchOperation = repository.fetchAllOperation(with: .init()) + + let changesOperation = ClosureOperation<[DataProviderChange]?> { + let locks = try decodingWrapper + .targetOperation + .extractNoCancellableResultData() ?? [] + + let remoteModels = locks.map { + AssetLock( + chainAssetId: chainAssetId, + accountId: accountId, + type: $0.identifier, + amount: $0.amount + ) + } + + return remoteModels.map(DataProviderChange.update) + } + + changesOperation.addDependency(fetchOperation) + + return CompoundOperationWrapper(targetOperation: changesOperation, dependencies: [fetchOperation]) + } + + private func createSaveOperation( + dependingOn operation: CompoundOperationWrapper<[DataProviderChange]?> + ) -> BaseOperation { + let replaceOperation = repository.replaceOperation { + guard let changes = try operation.targetOperation.extractNoCancellableResultData() else { + return [] + } + return changes.compactMap(\.item) + } + + replaceOperation.addDependency(operation.targetOperation) + return replaceOperation + } +} diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/OrmlAccountSubscription.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/OrmlAccountSubscription.swift index fffb7ea471..1e22d1d5fa 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/OrmlAccountSubscription.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/OrmlAccountSubscription.swift @@ -172,8 +172,6 @@ final class OrmlAccountSubscription: BaseStorageChildSubscription { let maybeItem = try? changesWrapper.targetOperation.extractNoCancellableResultData() if maybeItem != nil { - self?.eventCenter.notify(with: WalletBalanceChanged()) - let assetBalanceChangeEvent = AssetBalanceChanged( chainAssetId: chainAssetId, accountId: accountId, diff --git a/novawallet/Common/Storage/EntityToModel/AssetLockMapper.swift b/novawallet/Common/Storage/EntityToModel/AssetLockMapper.swift new file mode 100644 index 0000000000..f08c3469e1 --- /dev/null +++ b/novawallet/Common/Storage/EntityToModel/AssetLockMapper.swift @@ -0,0 +1,41 @@ +import Foundation +import RobinHood +import CoreData +import BigInt + +final class AssetLockMapper { + var entityIdentifierFieldName: String { #keyPath(CDAssetLock.identifier) } + + typealias DataProviderModel = AssetLock + typealias CoreDataEntity = CDAssetLock +} + +extension AssetLockMapper: CoreDataMapperProtocol { + func populate( + entity: CoreDataEntity, + from model: DataProviderModel, + using _: NSManagedObjectContext + ) throws { + entity.identifier = model.identifier + entity.chainAccountId = model.accountId.toHex() + entity.chainId = model.chainAssetId.chainId + entity.assetId = Int32(bitPattern: model.chainAssetId.assetId) + entity.amount = String(model.amount) + entity.type = model.type.toUTF8String()! + } + + func transform(entity: CoreDataEntity) throws -> DataProviderModel { + let accountId = try Data(hexString: entity.chainAccountId!) + let amount = entity.amount.map { BigUInt($0) ?? 0 } ?? 0 + let chainAssetId = ChainAssetId( + chainId: entity.chainId!, + assetId: UInt32(bitPattern: entity.assetId) + ) + return .init( + chainAssetId: chainAssetId, + accountId: accountId, + type: entity.type!.data(using: .utf8)!, + amount: amount + ) + } +} diff --git a/novawallet/Common/Storage/EntityToModel/CrowdloanContributionDataMapper.swift b/novawallet/Common/Storage/EntityToModel/CrowdloanContributionDataMapper.swift new file mode 100644 index 0000000000..b3737bf704 --- /dev/null +++ b/novawallet/Common/Storage/EntityToModel/CrowdloanContributionDataMapper.swift @@ -0,0 +1,40 @@ +import Foundation +import RobinHood +import CoreData +import BigInt + +final class CrowdloanContributionDataMapper { + var entityIdentifierFieldName: String { #keyPath(CDCrowdloanContribution.identifier) } + + typealias DataProviderModel = CrowdloanContributionData + typealias CoreDataEntity = CDCrowdloanContribution +} + +extension CrowdloanContributionDataMapper: CoreDataMapperProtocol { + func populate( + entity: CoreDataEntity, + from model: DataProviderModel, + using _: NSManagedObjectContext + ) throws { + entity.identifier = model.identifier + entity.chainId = model.chainId + entity.paraId = Int32(model.paraId) + entity.source = model.source + entity.chainAccountId = model.accountId.toHex() + entity.amount = String(model.amount) + } + + func transform(entity: CoreDataEntity) throws -> DataProviderModel { + let accountId = try Data(hexString: entity.chainAccountId!) + let amount = entity.amount.map { BigUInt($0) ?? 0 } ?? 0 + let paraId = UInt32(entity.paraId) + + return .init( + accountId: accountId, + chainId: entity.chainId!, + paraId: paraId, + source: entity.source, + amount: amount + ) + } +} diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000000..16a19db870 --- /dev/null +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + SubstrateDataModel2.xcdatamodel + + diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents index 7936e89864..91c21cef57 100644 --- a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel2.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel2.xcdatamodel/contents new file mode 100644 index 0000000000..27afd0afe8 --- /dev/null +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel2.xcdatamodel/contents @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift index af091c75e4..2f131715ef 100644 --- a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift +++ b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift @@ -1,25 +1,53 @@ import RobinHood import CoreData +enum SubstrateStorageParams { + static let databaseName = "SubstrateDataModel.sqlite" + static let modelDirectory: String = "SubstrateDataModel.momd" + static let modelVersion: SubstrateStorageVersion = .version2 + + static let storageDirectoryURL: URL = { + let baseURL = FileManager.default.urls( + for: .documentDirectory, + in: .userDomainMask + ).first?.appendingPathComponent("CoreData") + + return baseURL! + }() + + static var storageURL: URL { + storageDirectoryURL.appendingPathComponent(databaseName) + } +} + class SubstrateDataStorageFacade: StorageFacadeProtocol { static let shared = SubstrateDataStorageFacade() let databaseService: CoreDataServiceProtocol private init() { - let modelName = "SubstrateDataModel" - let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd") - let databaseName = "\(modelName).sqlite" + let databaseName = SubstrateStorageParams.databaseName + let modelName = SubstrateStorageParams.modelVersion.rawValue + let bundle = Bundle.main - let baseURL = FileManager.default.urls( - for: .documentDirectory, - in: .userDomainMask - ).first?.appendingPathComponent("CoreData") + let omoURL = bundle.url( + forResource: modelName, + withExtension: "omo", + subdirectory: SubstrateStorageParams.modelDirectory + ) + + let momURL = bundle.url( + forResource: modelName, + withExtension: "mom", + subdirectory: SubstrateStorageParams.modelDirectory + ) + + let modelURL = omoURL ?? momURL let persistentSettings = CoreDataPersistentSettings( - databaseDirectory: baseURL!, + databaseDirectory: SubstrateStorageParams.storageDirectoryURL, databaseName: databaseName, - incompatibleModelStrategy: .removeStore + incompatibleModelStrategy: .ignore ) let configuration = CoreDataServiceConfiguration( diff --git a/novawallet/Common/Substrate/Calls/Common/OrmlTokenTransfer.swift b/novawallet/Common/Substrate/Calls/Common/OrmlTokenTransfer.swift index 391639623f..b7a2a63a54 100644 --- a/novawallet/Common/Substrate/Calls/Common/OrmlTokenTransfer.swift +++ b/novawallet/Common/Substrate/Calls/Common/OrmlTokenTransfer.swift @@ -13,3 +13,15 @@ struct OrmlTokenTransfer: Codable { let currencyId: JSON @StringCodable var amount: BigUInt } + +struct OrmlTokenTransferAll: Codable { + enum CodingKeys: String, CodingKey { + case dest + case currencyId = "currency_id" + case keepAlive = "keep_alive" + } + + let dest: MultiAddress + let currencyId: JSON + let keepAlive: Bool +} diff --git a/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift b/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift index 8c6766d9b8..d69ecc6cec 100644 --- a/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift +++ b/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift @@ -9,6 +9,8 @@ protocol SubstrateCallFactoryProtocol { amount: BigUInt ) -> RuntimeCall + func nativeTransferAll(to receiver: AccountId) -> RuntimeCall + func assetsTransfer( to receiver: AccountId, extras: StatemineAssetExtras, @@ -22,6 +24,12 @@ protocol SubstrateCallFactoryProtocol { amount: BigUInt ) -> RuntimeCall + func ormlTransferAll( + in moduleName: String, + currencyId: JSON, + receiverId: AccountId + ) -> RuntimeCall + func bond( amount: BigUInt, controller: String, @@ -141,6 +149,11 @@ final class SubstrateCallFactory: SubstrateCallFactoryProtocol { return RuntimeCall(moduleName: "Balances", callName: "transfer", args: args) } + func nativeTransferAll(to receiver: AccountId) -> RuntimeCall { + let args = TransferAllCall(dest: .accoundId(receiver), keepAlive: false) + return RuntimeCall(moduleName: "Balances", callName: "transfer_all", args: args) + } + func ormlTransfer( in moduleName: String, currencyId: JSON, @@ -156,6 +169,20 @@ final class SubstrateCallFactory: SubstrateCallFactoryProtocol { return RuntimeCall(moduleName: moduleName, callName: "transfer", args: args) } + func ormlTransferAll( + in moduleName: String, + currencyId: JSON, + receiverId: AccountId + ) -> RuntimeCall { + let args = OrmlTokenTransferAll( + dest: .accoundId(receiverId), + currencyId: currencyId, + keepAlive: false + ) + + return RuntimeCall(moduleName: moduleName, callName: "transfer_all", args: args) + } + func setPayee(for destination: RewardDestinationArg) -> RuntimeCall { let args = SetPayeeCall(payee: destination) return RuntimeCall(moduleName: "Staking", callName: "set_payee", args: args) diff --git a/novawallet/Common/Substrate/Calls/Common/TransferCall.swift b/novawallet/Common/Substrate/Calls/Common/TransferCall.swift index 2b0e250f1a..93c4239352 100644 --- a/novawallet/Common/Substrate/Calls/Common/TransferCall.swift +++ b/novawallet/Common/Substrate/Calls/Common/TransferCall.swift @@ -6,3 +6,13 @@ struct TransferCall: Codable { let dest: MultiAddress @StringCodable var value: BigUInt } + +struct TransferAllCall: Codable { + enum CodingKeys: String, CodingKey { + case dest + case keepAlive = "keep_alive" + } + + let dest: MultiAddress + let keepAlive: Bool +} diff --git a/novawallet/Common/Substrate/Types/BalanceLock.swift b/novawallet/Common/Substrate/Types/BalanceLock.swift index dae4e81cdd..7d3cd30b0c 100644 --- a/novawallet/Common/Substrate/Types/BalanceLock.swift +++ b/novawallet/Common/Substrate/Types/BalanceLock.swift @@ -19,3 +19,5 @@ struct BalanceLock: Codable, Equatable { )?.trimmingCharacters(in: .whitespaces) } } + +typealias BalanceLocks = [BalanceLock] diff --git a/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift b/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift new file mode 100644 index 0000000000..f84e6b3ef7 --- /dev/null +++ b/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift @@ -0,0 +1,19 @@ +import Foundation +import SubstrateSdk +import BigInt + +struct BalancesTransferEvent: Decodable { + let sender: AccountId + let receiver: AccountId + let amount: BigUInt + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + sender = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue + + receiver = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue + + amount = try unkeyedContainer.decode(StringScaleMapper.self).value + } +} diff --git a/novawallet/Common/Substrate/Types/EventCodingPath.swift b/novawallet/Common/Substrate/Types/EventCodingPath.swift index 02dc28267d..2c1b73cbeb 100644 --- a/novawallet/Common/Substrate/Types/EventCodingPath.swift +++ b/novawallet/Common/Substrate/Types/EventCodingPath.swift @@ -32,6 +32,18 @@ extension EventCodingPath { EventCodingPath(moduleName: "Balances", eventName: "Withdraw") } + static var balancesTransfer: EventCodingPath { + EventCodingPath(moduleName: "Balances", eventName: "Transfer") + } + + static var tokensTransfer: EventCodingPath { + EventCodingPath(moduleName: "Tokens", eventName: "Transfer") + } + + static var currenciesTransferred: EventCodingPath { + EventCodingPath(moduleName: "Currencies", eventName: "Transferred") + } + static var ethereumExecuted: EventCodingPath { EventCodingPath(moduleName: "Ethereum", eventName: "Executed") } diff --git a/novawallet/Common/Substrate/Types/TokenTransferedEvent.swift b/novawallet/Common/Substrate/Types/TokenTransferedEvent.swift new file mode 100644 index 0000000000..4fbdc0f257 --- /dev/null +++ b/novawallet/Common/Substrate/Types/TokenTransferedEvent.swift @@ -0,0 +1,22 @@ +import Foundation +import SubstrateSdk +import BigInt + +struct TokenTransferedEvent: Decodable { + let currencyId: JSON + let sender: AccountId + let receiver: AccountId + let amount: BigUInt + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + currencyId = try unkeyedContainer.decode(JSON.self) + + sender = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue + + receiver = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue + + amount = try unkeyedContainer.decode(StringScaleMapper.self).value + } +} diff --git a/novawallet/Common/View/ErrorView/ErrorStateView.swift b/novawallet/Common/View/ErrorView/ErrorStateView.swift index 9f182e376b..8fa3764cee 100644 --- a/novawallet/Common/View/ErrorView/ErrorStateView.swift +++ b/novawallet/Common/View/ErrorView/ErrorStateView.swift @@ -25,6 +25,8 @@ class ErrorStateView: UIView { return button }() + lazy var stackView = UIStackView(arrangedSubviews: [iconImageView, errorDescriptionLabel, retryButton]) + var locale = Locale.current { didSet { if locale != oldValue { @@ -47,13 +49,16 @@ class ErrorStateView: UIView { } private func setupLayout() { - let stackView = UIStackView(arrangedSubviews: [iconImageView, errorDescriptionLabel, retryButton]) stackView.axis = .vertical stackView.spacing = 16 stackView.alignment = .center addSubview(stackView) - stackView.snp.makeConstraints { $0.center.equalToSuperview() } + stackView.snp.makeConstraints { + $0.center.equalToSuperview() + $0.leading.top.greaterThanOrEqualToSuperview() + $0.trailing.bottom.lessThanOrEqualToSuperview() + } } private func applyLocalization() { diff --git a/novawallet/Common/View/IconDetailsView.swift b/novawallet/Common/View/IconDetailsView.swift index 011ac69d51..7ff1e3a6c5 100644 --- a/novawallet/Common/View/IconDetailsView.swift +++ b/novawallet/Common/View/IconDetailsView.swift @@ -21,6 +21,16 @@ class IconDetailsView: UIView { return label }() + var hidesIcon: Bool { + get { + imageView.isHidden + } + + set { + imageView.isHidden = newValue + } + } + var mode: Mode = .iconDetails { didSet { applyLayout() diff --git a/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift b/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift index 30c17ffa5f..fe84754aef 100644 --- a/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift +++ b/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift @@ -8,6 +8,9 @@ struct ModalInfoFactory { static let headerHeight: CGFloat = 40.0 static let footerHeight: CGFloat = 0.0 + typealias LockSortingViewModel = (value: Decimal, viewModel: LocalizableResource) + typealias LocksSortingViewModel = [LockSortingViewModel] + static func createParaStkRewardDetails( for maxReward: Decimal, avgReward: Decimal, @@ -238,76 +241,66 @@ struct ModalInfoFactory { priceFormatter: LocalizableResource, precision: Int16 ) -> [LocalizableResource] { - print(balanceContext.toContext()) - let staticModels: [LocalizableResource] = [ - LocalizableResource { locale in - let title = R.string.localizable - .walletBalanceReserved(preferredLanguages: locale.rLanguages) - - let amountString = amountFormatter.value(for: locale).stringFromDecimal(balanceContext.reserved) ?? "" - - let formatter = priceFormatter.value(for: locale) - - let price = balanceContext.reserved * balanceContext.price - let priceString = balanceContext.price == 0.0 ? nil : formatter.stringFromDecimal(price) + let reserved: LocksSortingViewModel = createReservedViewModel( + balanceContext: balanceContext, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter + ) - let balance = BalanceViewModel( - amount: amountString, - price: priceString - ) + let crowdloans = createCrowdloansViewModel( + balanceContext: balanceContext, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter + ) - return StakingAmountViewModel(title: title, balance: balance) - } - ] + let balanceLockKnownModels: LocksSortingViewModel = createLockViewModel( + from: balanceContext.balanceLocks.mainLocks(), + balanceContext: balanceContext, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter, + precision: precision + ) - let balanceLockKnownModels: [LocalizableResource] = - createLockViewModel( - from: balanceContext.balanceLocks.mainLocks(), - balanceContext: balanceContext, - amountFormatter: amountFormatter, - priceFormatter: priceFormatter, - precision: precision - ) - - let balanceLockUnknownModels: [LocalizableResource] = - createLockViewModel( - from: balanceContext.balanceLocks.auxLocks(), - balanceContext: balanceContext, - amountFormatter: amountFormatter, - priceFormatter: priceFormatter, - precision: precision - ) + let balanceLockUnknownModels: LocksSortingViewModel = createLockViewModel( + from: balanceContext.balanceLocks.auxLocks(), + balanceContext: balanceContext, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter, + precision: precision + ) - return balanceLockKnownModels + balanceLockUnknownModels + staticModels + return (balanceLockKnownModels + balanceLockUnknownModels + crowdloans + reserved) + .sorted { viewModel1, viewModel2 in + viewModel1.value >= viewModel2.value + }.map(\.viewModel) } private static func createLockViewModel( - from locks: BalanceLocks, + from locks: AssetLocks, balanceContext: BalanceContext, amountFormatter: LocalizableResource, priceFormatter: LocalizableResource, precision: Int16 - ) -> [LocalizableResource] { + ) -> LocksSortingViewModel { locks.map { lock in - LocalizableResource { locale in + let lockAmount = Decimal.fromSubstrateAmount( + lock.amount, + precision: precision + ) ?? 0.0 + + let price = lockAmount * balanceContext.price + + let viewModel = LocalizableResource { locale in let formatter = priceFormatter.value(for: locale) let amountFormatter = amountFormatter.value(for: locale) let title: String = { - guard let mainTitle = LockType(rawValue: lock.displayId ?? "")? - .displayType - .value(for: locale) else { + guard let mainTitle = lock.lockType?.displayType.value(for: locale) else { return lock.displayId?.capitalized ?? "" } return mainTitle }() - let lockAmount = Decimal.fromSubstrateAmount( - lock.amount, - precision: precision - ) ?? 0.0 - let price = lockAmount * balanceContext.price - let priceString = balanceContext.price == 0.0 ? nil : formatter.stringFromDecimal(price) let amountString = amountFormatter.stringFromDecimal(lockAmount) ?? "" @@ -316,10 +309,76 @@ struct ModalInfoFactory { price: priceString ) - return StakingAmountViewModel( - title: title, balance: balance - ) + return StakingAmountViewModel(title: title, balance: balance) } + + return (price, viewModel) } } + + private static func createCrowdloansViewModel( + balanceContext: BalanceContext, + amountFormatter: LocalizableResource, + priceFormatter: LocalizableResource + ) -> LocksSortingViewModel { + guard balanceContext.crowdloans > 0 else { + return [] + } + + let title = LocalizableResource { locale in + R.string.localizable.walletAccountLocksCrowdloans(preferredLanguages: locale.rLanguages) + } + + return createLockFieldViewModel( + amount: balanceContext.crowdloans, + price: balanceContext.price, + localizedTitle: title, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter + ) + } + + private static func createReservedViewModel( + balanceContext: BalanceContext, + amountFormatter: LocalizableResource, + priceFormatter: LocalizableResource + ) -> LocksSortingViewModel { + let title = LocalizableResource { locale in + R.string.localizable.walletBalanceReserved(preferredLanguages: locale.rLanguages) + } + + return createLockFieldViewModel( + amount: balanceContext.reserved, + price: balanceContext.price, + localizedTitle: title, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter + ) + } + + private static func createLockFieldViewModel( + amount: Decimal, + price: Decimal, + localizedTitle: LocalizableResource, + amountFormatter: LocalizableResource, + priceFormatter: LocalizableResource + ) -> LocksSortingViewModel { + let totalPrice = amount * price + + let viewModel = LocalizableResource { locale in + let formatter = priceFormatter.value(for: locale) + let amountFormatter = amountFormatter.value(for: locale) + + let title = localizedTitle.value(for: locale) + + let priceString = totalPrice == 0.0 ? nil : formatter.stringFromDecimal(totalPrice) + let amountString = amountFormatter.stringFromDecimal(amount) ?? "" + + let balance = BalanceViewModel(amount: amountString, price: priceString) + + return StakingAmountViewModel(title: title, balance: balance) + } + + return [LockSortingViewModel(value: totalPrice, viewModel: viewModel)] + } } diff --git a/novawallet/Modules/AssetList/AssetListInteractor.swift b/novawallet/Modules/AssetList/AssetListInteractor.swift index 5a80b0a874..1ba5ae43c4 100644 --- a/novawallet/Modules/AssetList/AssetListInteractor.swift +++ b/novawallet/Modules/AssetList/AssetListInteractor.swift @@ -22,11 +22,15 @@ final class AssetListInteractor: AssetListBaseInteractor { private var nftSubscription: StreamableProvider? private var nftChainIds: Set? + private var assetLocksSubscriptions: [AccountId: StreamableProvider] = [:] + private var locks: [ChainAssetId: [AssetLock]] = [:] + init( selectedWalletSettings: SelectedWalletSettings, chainRegistry: ChainRegistryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, nftLocalSubscriptionFactory: NftLocalSubscriptionFactoryProtocol, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, eventCenter: EventCenterProtocol, settingsManager: SettingsManagerProtocol, @@ -41,6 +45,7 @@ final class AssetListInteractor: AssetListBaseInteractor { selectedWalletSettings: selectedWalletSettings, chainRegistry: chainRegistry, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + crowdloansLocalSubscriptionFactory: crowdloansLocalSubscriptionFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, currencyManager: currencyManager, logger: logger @@ -50,6 +55,8 @@ final class AssetListInteractor: AssetListBaseInteractor { private func resetWallet() { clearAccountSubscriptions() clearNftSubscription() + clearLocksSubscription() + clearCrowdloansSubscription() guard let selectedMetaAccount = selectedWalletSettings.value else { return @@ -66,8 +73,15 @@ final class AssetListInteractor: AssetListBaseInteractor { presenter?.didReceiveChainModelChanges(changes) updateAccountInfoSubscription(from: changes) - setupNftSubscription(from: Array(availableChains.values)) + updateLocksSubscription(from: changes) + updateCrowdloansSubscription(from: Array(availableChains.values)) + } + + private func clearLocksSubscription() { + assetLocksSubscriptions.values.forEach { $0.removeObserver(self) } + assetLocksSubscriptions = [:] + locks = [:] } private func providerWalletInfo() { @@ -102,6 +116,7 @@ final class AssetListInteractor: AssetListBaseInteractor { updateConnectionStatus(from: allChanges) setupNftSubscription(from: Array(availableChains.values)) + updateLocksSubscription(from: allChanges) } private func updateConnectionStatus(from changes: [DataProviderChange]) { @@ -142,6 +157,28 @@ final class AssetListInteractor: AssetListBaseInteractor { eventCenter.add(observer: self, dispatchIn: .main) } + + private func updateLocksSubscription(from changes: [DataProviderChange]) { + guard let selectedMetaAccount = selectedWalletSettings.value else { + return + } + + assetLocksSubscriptions = changes.reduce( + intitial: assetLocksSubscriptions, + selectedMetaAccount: selectedMetaAccount + ) { [weak self] in + self?.subscribeToAllLocksProvider(for: $0) + } + } + + override func handleAccountLocks(result: Result<[DataProviderChange], Error>, accountId: AccountId) { + switch result { + case let .success(changes): + handleAccountLocksChanges(changes, accountId: accountId) + case let .failure(error): + presenter?.didReceiveLocks(result: .failure(error)) + } + } } extension AssetListInteractor: AssetListInteractorInputProtocol { @@ -173,6 +210,45 @@ extension AssetListInteractor: NftLocalStorageSubscriber, NftLocalSubscriptionHa } } +extension AssetListInteractor { + private func handleAccountLocksChanges( + _ changes: [DataProviderChange], + accountId: AccountId + ) { + locks = changes.reduce( + into: locks + ) { accum, change in + switch change { + case let .insert(lock), let .update(lock): + let groupIdentifier = AssetBalance.createIdentifier( + for: lock.chainAssetId, + accountId: lock.accountId + ) + guard + let assetBalanceId = assetBalanceIdMapping[groupIdentifier], + assetBalanceId.accountId == accountId else { + return + } + + let chainAssetId = ChainAssetId( + chainId: assetBalanceId.chainId, + assetId: assetBalanceId.assetId + ) + + var items = accum[chainAssetId] ?? [] + items.addOrReplaceSingle(lock) + accum[chainAssetId] = items + case let .delete(deletedIdentifier): + for chainLocks in accum { + accum[chainLocks.key] = chainLocks.value.filter { $0.identifier != deletedIdentifier } + } + } + } + + presenter?.didReceiveLocks(result: .success(Array(locks.values.flatMap { $0 }))) + } +} + extension AssetListInteractor: ConnectionStateSubscription { func didReceive(state: WebSocketEngine.State, for chainId: ChainModel.Id) { presenter?.didReceive(state: state, for: chainId) diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index 081b2ab7ba..e7a1caafa6 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -20,6 +20,7 @@ final class AssetListPresenter: AssetListBasePresenter { private var name: String? private var hidesZeroBalances: Bool? private(set) var connectionStates: [ChainModel.Id: WebSocketEngine.State] = [:] + private(set) var locksResult: Result<[AssetLock], Error>? private var scheduler: SchedulerProtocol? @@ -54,6 +55,7 @@ final class AssetListPresenter: AssetListBasePresenter { walletIdenticon: walletIdenticon, walletType: walletType, prices: nil, + locks: nil, locale: selectedLocale ) @@ -69,79 +71,154 @@ final class AssetListPresenter: AssetListBasePresenter { ) } + typealias SuccessAssetListAssetAccountPrice = AssetListAssetAccountPrice + typealias FailedAssetListAssetAccountPrice = AssetListAssetAccountPrice + private func createAssetAccountPrice( + chainAssetId: ChainAssetId, + priceData: PriceData + ) -> Either? { + let chainId = chainAssetId.chainId + let assetId = chainAssetId.assetId + + guard let chain = allChains[chainId], + let asset = chain.assets.first(where: { $0.assetId == assetId }) else { + return nil + } + + guard case let .success(assetBalance) = balances[chainAssetId] else { + return .right( + AssetListAssetAccountPrice( + assetInfo: asset.displayInfo, + balance: 0, + price: priceData + ) + ) + } + + return .left( + AssetListAssetAccountPrice( + assetInfo: asset.displayInfo, + balance: assetBalance.totalInPlank, + price: priceData + )) + } + + private func createAssetAccountPriceLock( + chainAssetId: ChainAssetId, + priceData: PriceData + ) -> AssetListAssetAccountPrice? { + let chainId = chainAssetId.chainId + let assetId = chainAssetId.assetId + + guard let chain = allChains[chainId], + let asset = chain.assets.first(where: { $0.assetId == assetId }) else { + return nil + } + + guard case let .success(assetBalance) = balances[chainAssetId], assetBalance.locked > 0 else { + return nil + } + + return AssetListAssetAccountPrice( + assetInfo: asset.displayInfo, + balance: assetBalance.locked, + price: priceData + ) + } + private func provideHeaderViewModel( with priceMapping: [ChainAssetId: PriceData], walletIdenticon: Data?, walletType: MetaAccountModelType, name: String ) { - let priceState: LoadableViewModelState<[AssetListAssetAccountPrice]> = priceMapping.reduce( - LoadableViewModelState.loaded(value: []) - ) { result, keyValue in - let chainAssetId = keyValue.key - let chainId = chainAssetId.chainId - let assetId = chainAssetId.assetId - switch result { - case .loading: - return .loading - case let .cached(items): - guard - let chain = allChains[chainId], - let asset = chain.assets.first(where: { $0.assetId == assetId }) else { - return .cached(value: items) - } + let crowdloans = crowdloansModel(prices: priceMapping) + let totalValue = createHeaderPriceState(from: priceMapping, crowdloans: crowdloans) + let totalLocks = createHeaderLockState(from: priceMapping, crowdloans: crowdloans) - let totalBalance: BigUInt + let viewModel = viewModelFactory.createHeaderViewModel( + from: name, + walletIdenticon: walletIdenticon, + walletType: walletType, + prices: totalValue, + locks: totalLocks, + locale: selectedLocale + ) - if case let .success(assetBalance) = balanceResults[chainAssetId] { - totalBalance = assetBalance - } else { - totalBalance = 0 - } + view?.didReceiveHeader(viewModel: viewModel) + } - let newItem = AssetListAssetAccountPrice( - assetInfo: asset.displayInfo, - balance: totalBalance, - price: keyValue.value - ) + private func createHeaderPriceState( + from priceMapping: [ChainAssetId: PriceData], + crowdloans: [AssetListAssetAccountPrice] + ) -> LoadableViewModelState<[AssetListAssetAccountPrice]> { + var priceState: LoadableViewModelState<[AssetListAssetAccountPrice]> = .loaded(value: []) - return .cached(value: items + [newItem]) + for (chainAssetId, priceData) in priceMapping { + switch priceState { + case .loading: + priceState = .loading + case let .cached(items): + guard let newItem = createAssetAccountPrice( + chainAssetId: chainAssetId, + priceData: priceData + ) else { + priceState = .cached(value: items) + continue + } + priceState = .cached(value: items + [newItem.value]) case let .loaded(items): - guard - let chain = allChains[chainId], - let asset = chain.assets.first(where: { $0.assetId == assetId }) else { - return .cached(value: items) + guard let newItem = createAssetAccountPrice( + chainAssetId: chainAssetId, + priceData: priceData + ) else { + priceState = .cached(value: items) + continue } - if case let .success(assetBalance) = balanceResults[chainAssetId] { - let newItem = AssetListAssetAccountPrice( - assetInfo: asset.displayInfo, - balance: assetBalance, - price: keyValue.value - ) + switch newItem { + case let .left(item): + priceState = .loaded(value: items + [item]) + case let .right(item): + priceState = .cached(value: items + [item]) + } + } + } - return .loaded(value: items + [newItem]) - } else { - let newItem = AssetListAssetAccountPrice( - assetInfo: asset.displayInfo, - balance: 0, - price: keyValue.value - ) + return priceState + crowdloans + } - return .cached(value: items + [newItem]) - } + private func createHeaderLockState( + from priceMapping: [ChainAssetId: PriceData], + crowdloans: [AssetListAssetAccountPrice] + ) -> [AssetListAssetAccountPrice]? { + guard checkNonZeroLocks() else { + return nil + } + + let locks: [AssetListAssetAccountPrice] = priceMapping.reduce(into: []) { accum, keyValue in + if let lock = createAssetAccountPriceLock(chainAssetId: keyValue.key, priceData: keyValue.value) { + accum.append(lock) } } - let viewModel = viewModelFactory.createHeaderViewModel( - from: name, - walletIdenticon: walletIdenticon, - walletType: walletType, - prices: priceState, - locale: selectedLocale - ) + return locks + crowdloans + } - view?.didReceiveHeader(viewModel: viewModel) + private func checkNonZeroLocks() -> Bool { + let locks = balances.map { (try? $0.value.get())?.locked ?? 0 } + + if locks.contains(where: { $0 > 0 }) { + return true + } + + let crowdloanContributions = (try? crowdloansResult?.get()) ?? [:] + + if crowdloanContributions.contains(where: { $0.value.contains(where: { $0.amount > 0 }) }) { + return true + } + + return false } private func calculateNftBalance(for chainAsset: ChainAsset) -> BigUInt { @@ -166,10 +243,12 @@ final class AssetListPresenter: AssetListBasePresenter { } let maybePrices = try? priceResult?.get() + let maybeCrowdloans = try? crowdloansResult?.get() let viewModels: [AssetListGroupViewModel] = groups.allItems.compactMap { groupModel in createGroupViewModel( from: groupModel, maybePrices: maybePrices, + maybeCrowdloans: maybeCrowdloans, hidesZeroBalances: hidesZeroBalances ) } @@ -181,9 +260,40 @@ final class AssetListPresenter: AssetListBasePresenter { } } + private func crowdloansModel(prices: [ChainAssetId: PriceData]) -> [AssetListAssetAccountPrice] { + switch crowdloansResult { + case .failure, .none: + return [] + case let .success(crowdloans): + return crowdloans.compactMap { chainId, chainCrowdloans in + guard let chain = allChains[chainId] else { + return nil + } + guard let asset = chain.utilityAsset() else { + return nil + } + let chainAssetId = ChainAssetId(chainId: chainId, assetId: asset.assetId) + let price = prices[chainAssetId] ?? .zero() + + let contributedAmount = chainCrowdloans.reduce(0) { $0 + $1.amount } + + guard contributedAmount > 0 else { + return nil + } + + return AssetListAssetAccountPrice( + assetInfo: asset.displayInfo, + balance: contributedAmount, + price: price + ) + } + } + } + private func createGroupViewModel( from groupModel: AssetListGroupModel, maybePrices: [ChainAssetId: PriceData]?, + maybeCrowdloans _: [ChainModel.Id: [CrowdloanContributionData]]?, hidesZeroBalances: Bool ) -> AssetListGroupViewModel? { let chain = groupModel.chain @@ -280,6 +390,11 @@ final class AssetListPresenter: AssetListBasePresenter { wireframe.showAssetDetails(from: view, chain: chain, asset: asset) } + override func resetStorages() { + super.resetStorages() + locksResult = nil + } + // MARK: Interactor Output overridings override func didReceivePrices(result: Result<[ChainAssetId: PriceData], Error>?) { @@ -296,11 +411,17 @@ final class AssetListPresenter: AssetListBasePresenter { updateAssetsView() } - override func didReceiveBalance(results: [ChainAssetId: Result]) { + override func didReceiveBalance(results: [ChainAssetId: Result]) { super.didReceiveBalance(results: results) updateAssetsView() } + + override func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { + super.didReceiveCrowdloans(result: result) + + updateAssetsView() + } } extension AssetListPresenter: AssetListPresenterProtocol { @@ -337,6 +458,26 @@ extension AssetListPresenter: AssetListPresenterProtocol { wireframe.showAssetsSearch(from: view, initState: initState, delegate: self) } + + func didTapTotalBalance() { + guard + checkNonZeroLocks(), + let priceResult = priceResult, + let prices = try? priceResult.get(), + let locks = try? locksResult?.get(), + let crowdloans = try? crowdloansResult?.get() else { + return + } + + wireframe.showBalanceBreakdown( + from: view, + prices: prices, + balances: balances.values.compactMap { try? $0.get() }, + chains: allChains, + locks: locks, + crowdloans: crowdloans + ) + } } extension AssetListPresenter: AssetListInteractorOutputProtocol { @@ -382,6 +523,12 @@ extension AssetListPresenter: AssetListInteractorOutputProtocol { updateAssetsView() } + + func didReceiveLocks(result: Result<[AssetLock], Error>) { + locksResult = result + + updateHeaderView() + } } extension AssetListPresenter: Localizable { diff --git a/novawallet/Modules/AssetList/AssetListProtocols.swift b/novawallet/Modules/AssetList/AssetListProtocols.swift index 661e22ba5f..4aee0b3c7c 100644 --- a/novawallet/Modules/AssetList/AssetListProtocols.swift +++ b/novawallet/Modules/AssetList/AssetListProtocols.swift @@ -18,6 +18,7 @@ protocol AssetListPresenterProtocol: AnyObject { func refresh() func presentSettings() func presentSearch() + func didTapTotalBalance() } protocol AssetListInteractorInputProtocol: AssetListBaseInteractorInputProtocol { @@ -32,6 +33,7 @@ protocol AssetListInteractorOutputProtocol: AssetListBaseInteractorOutputProtoco func didReceive(state: WebSocketEngine.State, for chainId: ChainModel.Id) func didChange(name: String) func didReceive(hidesZeroBalances: Bool) + func didReceiveLocks(result: Result<[AssetLock], Error>) } protocol AssetListWireframeProtocol: AnyObject, WalletSwitchPresentable { @@ -45,4 +47,13 @@ protocol AssetListWireframeProtocol: AnyObject, WalletSwitchPresentable { ) func showNfts(from view: AssetListViewProtocol?) + + func showBalanceBreakdown( + from view: AssetListViewProtocol?, + prices: [ChainAssetId: PriceData], + balances: [AssetBalance], + chains: [ChainModel.Id: ChainModel], + locks: [AssetLock], + crowdloans: [ChainModel.Id: [CrowdloanContributionData]] + ) } diff --git a/novawallet/Modules/AssetList/AssetListViewController.swift b/novawallet/Modules/AssetList/AssetListViewController.swift index d5cdc01252..aea837afc2 100644 --- a/novawallet/Modules/AssetList/AssetListViewController.swift +++ b/novawallet/Modules/AssetList/AssetListViewController.swift @@ -93,7 +93,8 @@ extension AssetListViewController: UICollectionViewDelegateFlowLayout { sizeForItemAt indexPath: IndexPath ) -> CGSize { let cellType = AssetListFlowLayout.CellType(indexPath: indexPath) - return CGSize(width: collectionView.frame.width, height: cellType.height) + let cellHeight = rootView.collectionViewLayout.cellHeight(for: cellType) + return CGSize(width: collectionView.bounds.width, height: cellHeight) } func collectionView( @@ -119,7 +120,7 @@ extension AssetListViewController: UICollectionViewDelegateFlowLayout { let cellType = AssetListFlowLayout.CellType(indexPath: indexPath) switch cellType { - case .account, .totalBalance, .settings, .emptyState: + case .account, .settings, .emptyState: break case .asset: if let groupIndex = AssetListFlowLayout.SectionType.assetsGroupIndexFromSection( @@ -130,6 +131,8 @@ extension AssetListViewController: UICollectionViewDelegateFlowLayout { } case .yourNfts: presenter.selectNfts() + case .totalBalance: + presenter.didTapTotalBalance() } } @@ -348,6 +351,11 @@ extension AssetListViewController: AssetListViewProtocol { headerViewModel = viewModel rootView.collectionView.reloadData() + + let cellHeight = viewModel.locksAmount == nil ? + AssetListMeasurement.totalBalanceHeight : AssetListMeasurement.totalBalanceWithLocksHeight + + rootView.collectionViewLayout.updateTotalBalanceHeight(cellHeight) } func didReceiveGroups(state: AssetListGroupState) { diff --git a/novawallet/Modules/AssetList/AssetListViewFactory.swift b/novawallet/Modules/AssetList/AssetListViewFactory.swift index 099c9d2a57..b3a82fac9d 100644 --- a/novawallet/Modules/AssetList/AssetListViewFactory.swift +++ b/novawallet/Modules/AssetList/AssetListViewFactory.swift @@ -7,18 +7,28 @@ struct AssetListViewFactory { guard let currencyManager = CurrencyManager.shared else { return nil } + let interactor = AssetListInteractor( selectedWalletSettings: SelectedWalletSettings.shared, chainRegistry: ChainRegistryFacade.sharedRegistry, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, nftLocalSubscriptionFactory: NftLocalSubscriptionFactory.shared, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, eventCenter: EventCenter.shared, settingsManager: SettingsManager.shared, - currencyManager: currencyManager + currencyManager: currencyManager, + logger: Logger.shared + ) + + let walletUpdater = WalletDetailsUpdater( + eventCenter: EventCenter.shared, + crowdloansLocalSubscriptionFactory: interactor.crowdloansLocalSubscriptionFactory, + walletLocalSubscriptionFactory: interactor.walletLocalSubscriptionFactory, + walletSettings: interactor.selectedWalletSettings ) - let wireframe = AssetListWireframe(walletUpdater: WalletDetailsUpdater.shared) + let wireframe = AssetListWireframe(walletUpdater: walletUpdater) let nftDownloadService = NftFileDownloadService( cacheBasePath: ApplicationConfig.shared.fileCachePath, diff --git a/novawallet/Modules/AssetList/AssetListViewLayout.swift b/novawallet/Modules/AssetList/AssetListViewLayout.swift index 0983d5d723..f927aa2660 100644 --- a/novawallet/Modules/AssetList/AssetListViewLayout.swift +++ b/novawallet/Modules/AssetList/AssetListViewLayout.swift @@ -3,8 +3,10 @@ import UIKit final class AssetListViewLayout: UIView { let backgroundView = MultigradientView.background - let collectionView: UICollectionView = { - let flowLayout = AssetListFlowLayout() + let collectionViewLayout = AssetListFlowLayout() + + lazy var collectionView: UICollectionView = { + let flowLayout = collectionViewLayout flowLayout.scrollDirection = .vertical flowLayout.minimumLineSpacing = 0 flowLayout.minimumInteritemSpacing = 0 diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index 3c6b70701b..d8fb169529 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import SoraUI final class AssetListWireframe: AssetListWireframeProtocol { let walletUpdater: WalletDetailsUpdating @@ -21,7 +22,8 @@ final class AssetListWireframe: AssetListWireframeProtocol { try? context.createAssetDetails(for: assetId, in: navigationController) - walletUpdater.context = context + let chainAsset = ChainAsset(chain: chain, asset: asset) + walletUpdater.setup(context: context, chainAsset: chainAsset) } func showAssetsManage(from view: AssetListViewProtocol?) { @@ -59,4 +61,30 @@ final class AssetListWireframe: AssetListWireframeProtocol { nftListView.controller.hidesBottomBarWhenPushed = true view?.controller.navigationController?.pushViewController(nftListView.controller, animated: true) } + + func showBalanceBreakdown( + from view: AssetListViewProtocol?, + prices: [ChainAssetId: PriceData], + balances: [AssetBalance], + chains: [ChainModel.Id: ChainModel], + locks: [AssetLock], + crowdloans: [ChainModel.Id: [CrowdloanContributionData]] + ) { + guard let viewController = LocksViewFactory.createView(input: + .init( + prices: prices, + balances: balances, + chains: chains, + locks: locks, + crowdloans: crowdloans + )) else { + return + } + + let factory = ModalSheetPresentationFactory(configuration: ModalSheetPresentationConfiguration.fearless) + viewController.controller.modalTransitioningFactory = factory + viewController.controller.modalPresentationStyle = .custom + + view?.controller.present(viewController.controller, animated: true) + } } diff --git a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift index 2dc29843bf..4f27e749c2 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift @@ -4,17 +4,23 @@ import SubstrateSdk import SoraKeystore import BigInt -class AssetListBaseInteractor { +class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { weak var basePresenter: AssetListBaseInteractorOutputProtocol? let selectedWalletSettings: SelectedWalletSettings let chainRegistry: ChainRegistryProtocol let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol let logger: LoggerProtocol? private(set) var assetBalanceSubscriptions: [AccountId: StreamableProvider] = [:] private(set) var assetBalanceIdMapping: [String: AssetBalanceId] = [:] + + private var crowdloansSubscriptions: [ChainModel.Id: StreamableProvider] = [:] + private var crowdloans: [ChainModel.Id: [CrowdloanContributionData]] = [:] + private var crowdloanChainIds = Set() + private(set) var priceSubscription: AnySingleValueProvider<[PriceData]>? private(set) var availableTokenPrice: [ChainAssetId: AssetModel.PriceId] = [:] private(set) var availableChains: [ChainModel.Id: ChainModel] = [:] @@ -23,6 +29,7 @@ class AssetListBaseInteractor { selectedWalletSettings: SelectedWalletSettings, chainRegistry: ChainRegistryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, currencyManager: CurrencyManagerProtocol, logger: LoggerProtocol? = nil @@ -30,6 +37,7 @@ class AssetListBaseInteractor { self.selectedWalletSettings = selectedWalletSettings self.chainRegistry = chainRegistry self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.crowdloansLocalSubscriptionFactory = crowdloansLocalSubscriptionFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory self.logger = logger self.currencyManager = currencyManager @@ -42,6 +50,13 @@ class AssetListBaseInteractor { assetBalanceIdMapping = [:] } + func clearCrowdloansSubscription() { + crowdloansSubscriptions.values.forEach { $0.removeObserver(self) } + crowdloansSubscriptions = [:] + crowdloans = [:] + crowdloanChainIds = .init() + } + private func handle(changes: [DataProviderChange]) { guard let selectedMetaAccount = selectedWalletSettings.value else { return @@ -67,6 +82,7 @@ class AssetListBaseInteractor { updateAvailableChains(from: allChanges) updateAccountInfoSubscription(from: accountDependentChanges) updatePriceSubscription(from: allChanges) + updateCrowdloansSubscription(from: Array(availableChains.values)) } func updateAvailableChains(from changes: [DataProviderChange]) { @@ -113,22 +129,11 @@ class AssetListBaseInteractor { } } - assetBalanceSubscriptions = changes.reduce(into: assetBalanceSubscriptions) { result, change in - switch change { - case let .insert(chain), let .update(chain): - guard let accountId = selectedMetaAccount.fetch( - for: chain.accountRequest() - )?.accountId else { - return - } - - if result[accountId] == nil { - result[accountId] = subscribeToAccountBalanceProvider(for: accountId) - } - case .delete: - // we might have the same account id used in other - break - } + assetBalanceSubscriptions = changes.reduce( + intitial: assetBalanceSubscriptions, + selectedMetaAccount: selectedMetaAccount + ) { [weak self] in + self?.subscribeToAccountBalanceProvider(for: $0) } } @@ -217,6 +222,32 @@ class AssetListBaseInteractor { ) } + func updateCrowdloansSubscription(from allChains: [ChainModel]) { + guard let selectedMetaAccount = selectedWalletSettings.value else { + return + } + + let crowdloanChains = allChains.filter { $0.hasCrowdloans } + let newCrowdloanChainIds = Set(crowdloanChains.map(\.chainId)) + + guard !crowdloanChains.isEmpty, crowdloanChainIds != newCrowdloanChainIds else { + return + } + + clearCrowdloansSubscription() + crowdloanChainIds = newCrowdloanChainIds + + for chain in crowdloanChains { + let request = chain.accountRequest() + + guard let accountId = selectedMetaAccount.fetch(for: request)?.accountId else { + return + } + + crowdloansSubscriptions[chain.identifier] = subscribeToCrowdloansProvider(for: accountId, chain: chain) + } + } + func subscribeChains() { chainRegistry.chainsSubscribe(self, runningInQueue: .main) { [weak self] changes in self?.handle(changes: changes) @@ -226,14 +257,26 @@ class AssetListBaseInteractor { func setup() { subscribeChains() } -} -extension AssetListBaseInteractor: AssetListBaseInteractorInputProtocol {} + func handleAccountBalance( + result: Result<[DataProviderChange], Error>, + accountId: AccountId + ) { + switch result { + case let .success(changes): + handleAccountBalanceChanges(changes, accountId: accountId) + case let .failure(error): + handleAccountBalanceError(error, accountId: accountId) + } + } -extension AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAccountLocks(result _: Result<[DataProviderChange], Error>, accountId _: AccountId) {} +} + +extension AssetListBaseInteractor { private func handleAccountBalanceError(_ error: Error, accountId: AccountId) { let results = assetBalanceIdMapping.values.reduce( - into: [ChainAssetId: Result]() + into: [ChainAssetId: Result]() ) { accum, assetBalanceId in guard assetBalanceId.accountId == accountId else { return @@ -256,7 +299,7 @@ extension AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubs ) { // prepopulate non existing balances with zeros let initialItems = assetBalanceIdMapping.values.reduce( - into: [ChainAssetId: Result]() + into: [ChainAssetId: Result]() ) { accum, assetBalanceId in guard assetBalanceId.accountId == accountId else { return @@ -286,7 +329,7 @@ extension AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubs assetId: assetBalanceId.assetId ) - accum[chainAssetId] = .success(balance.totalInPlank) + accum[chainAssetId] = .success(.init(balance: balance, total: balance.totalInPlank)) case let .delete(deletedIdentifier): guard let assetBalanceId = assetBalanceIdMapping[deletedIdentifier] else { return @@ -297,22 +340,48 @@ extension AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubs assetId: assetBalanceId.assetId ) - accum[chainAssetId] = .success(0) + accum[chainAssetId] = .success(.init(total: 0)) } } basePresenter?.didReceiveBalance(results: results) } +} - func handleAccountBalance( - result: Result<[DataProviderChange], Error>, - accountId: AccountId +extension AssetListBaseInteractor: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { + func handleCrowdloans( + result: Result<[DataProviderChange], Error>, + accountId: AccountId, + chain: ChainModel ) { + guard let selectedMetaAccount = selectedWalletSettings.value else { + return + } + guard let chainAccountId = selectedMetaAccount.fetch( + for: chain.accountRequest() + )?.accountId, chainAccountId == accountId else { + logger?.warning("Crowdloans updates can't be handled because account for selected wallet for chain: \(chain.name) is different") + return + } + switch result { - case let .success(changes): - handleAccountBalanceChanges(changes, accountId: accountId) case let .failure(error): - handleAccountBalanceError(error, accountId: accountId) + basePresenter?.didReceiveCrowdloans(result: .failure(error)) + case let .success(changes): + crowdloans = changes.reduce( + into: crowdloans + ) { result, change in + switch change { + case let .insert(crowdloan), let .update(crowdloan): + var items = result[chain.chainId] ?? [] + items.addOrReplaceSingle(crowdloan) + result[chain.chainId] = items + case let .delete(deletedIdentifier): + result[chain.chainId]?.removeAll(where: { $0.identifier == deletedIdentifier }) + } + } + + basePresenter?.didReceiveCrowdloans(result: .success(crowdloans)) } } } @@ -326,3 +395,33 @@ extension AssetListBaseInteractor: SelectedCurrencyDepending { updatePriceProvider(for: Set(availableTokenPrice.values), currency: selectedCurrency) } } + +extension Array where Element == DataProviderChange { + func reduce( + intitial: [AccountId: StreamableProvider], + selectedMetaAccount: MetaAccountModel, + subscription: @escaping (AccountId) -> StreamableProvider? + ) -> [AccountId: StreamableProvider] { + reduce(into: intitial) { result, change in + switch change { + case let .insert(chain), let .update(chain): + guard let accountId = selectedMetaAccount.fetch( + for: chain.accountRequest() + )?.accountId else { + return + } + + if result[accountId] == nil { + result[accountId] = subscription(accountId) + } + case .delete: + break + } + } + } +} + +struct CalculatedAssetBalance { + var balance: AssetBalance? + var total: BigUInt +} diff --git a/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift b/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift index a94dd76dd7..9c2c778fdd 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift @@ -8,6 +8,7 @@ protocol AssetListBaseInteractorInputProtocol: AnyObject { protocol AssetListBaseInteractorOutputProtocol: AnyObject { func didReceiveChainModelChanges(_ changes: [DataProviderChange]) - func didReceiveBalance(results: [ChainAssetId: Result]) + func didReceiveBalance(results: [ChainAssetId: Result]) + func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) func didReceivePrices(result: Result<[ChainAssetId: PriceData], Error>?) } diff --git a/novawallet/Modules/AssetList/Base/AssetListBasePresenter+Model.swift b/novawallet/Modules/AssetList/Base/AssetListBasePresenter+Model.swift index a3951e50f1..c2a6111841 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBasePresenter+Model.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBasePresenter+Model.swift @@ -1,5 +1,6 @@ import Foundation import RobinHood +import BigInt extension AssetListBasePresenter { func createGroupModel( @@ -7,7 +8,7 @@ extension AssetListBasePresenter { assets: [AssetListAssetModel] ) -> AssetListGroupModel { let value: Decimal = assets.reduce(0) { result, asset in - result + (asset.assetValue ?? 0) + result + (asset.totalValue ?? 0) } return AssetListGroupModel(chain: chain, chainValue: value) @@ -40,8 +41,8 @@ extension AssetListBasePresenter { let balance1 = (try? model1.balanceResult?.get()) ?? 0 let balance2 = (try? model2.balanceResult?.get()) ?? 0 - let assetValue1 = model1.assetValue ?? 0 - let assetValue2 = model2.assetValue ?? 0 + let assetValue1 = model1.totalValue ?? 0 + let assetValue2 = model2.totalValue ?? 0 if assetValue1 > 0, assetValue2 > 0 { return assetValue1 > assetValue2 @@ -89,6 +90,31 @@ extension AssetListBasePresenter { } }() + let crowdloanContributionsResult: Result? = { + do { + let allContributions = try crowdloansResult?.get() + + let contribution = allContributions?[chainModel.chainId]?.reduce(BigUInt(0)) { accum, contribution in + accum + contribution.amount + } + + return contribution.map { .success($0) } + } catch { + return .failure(error) + } + }() + + let maybeCrowdloanContributions: Decimal? = { + if let contributions = try? crowdloanContributionsResult?.get() { + return Decimal.fromSubstrateAmount( + contributions, + precision: Int16(bitPattern: assetModel.precision) + ) + } else { + return nil + } + }() + let maybePrice: Decimal? = { if let mapping = try? priceResult?.get(), let priceData = mapping[chainAssetId] { return Decimal(string: priceData.price) @@ -97,19 +123,28 @@ extension AssetListBasePresenter { } }() - if let balance = maybeBalance, let price = maybePrice { - let assetValue = balance * price - return AssetListAssetModel( - assetModel: assetModel, - balanceResult: balanceResult, - assetValue: assetValue - ) - } else { - return AssetListAssetModel( - assetModel: assetModel, - balanceResult: balanceResult, - assetValue: nil - ) - } + let balanceValue: Decimal? = { + if let balance = maybeBalance, let price = maybePrice { + return balance * price + } else { + return nil + } + }() + + let crowdloanContributionsValue: Decimal? = { + if let crowdloanContributions = maybeCrowdloanContributions, let price = maybePrice { + return crowdloanContributions * price + } else { + return nil + } + }() + + return AssetListAssetModel( + assetModel: assetModel, + balanceResult: balanceResult, + balanceValue: balanceValue, + crowdloanResult: crowdloanContributionsResult, + crowdloanValue: crowdloanContributionsValue + ) } } diff --git a/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift b/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift index 4cd80731e2..0c19c683d5 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift @@ -8,16 +8,35 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { private(set) var priceResult: Result<[ChainAssetId: PriceData], Error>? private(set) var balanceResults: [ChainAssetId: Result] = [:] + private(set) var balances: [ChainAssetId: Result] = [:] private(set) var allChains: [ChainModel.Id: ChainModel] = [:] + private(set) var crowdloansResult: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>? init() { groups = Self.createGroupsDiffCalculator(from: []) } + private func updateAssetModels() { + for chain in allChains.values { + let models = chain.assets.map { asset in + createAssetModel(for: chain, assetModel: asset) + } + + let changes: [DataProviderChange] = models.map { model in + .update(newItem: model) + } + + groupLists[chain.chainId]?.apply(changes: changes) + + let groupModel = createGroupModel(from: chain, assets: models) + groups.apply(changes: [.update(newItem: groupModel)]) + } + } + func resetStorages() { allChains = [:] balanceResults = [:] - + balances = [:] groups = Self.createGroupsDiffCalculator(from: []) groupLists = [:] } @@ -67,12 +86,10 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { priceData = nil } - let balance = try? asset.balanceResult?.get() - return AssetListAssetAccountInfo( assetId: asset.assetModel.assetId, assetInfo: assetInfo, - balance: balance, + balance: asset.totalAmount, priceData: priceData ) } @@ -84,20 +101,7 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { priceResult = result - for chain in allChains.values { - let models = chain.assets.map { asset in - createAssetModel(for: chain, assetModel: asset) - } - - let changes: [DataProviderChange] = models.map { model in - .update(newItem: model) - } - - groupLists[chain.chainId]?.apply(changes: changes) - - let groupModel = createGroupModel(from: chain, assets: models) - groups.apply(changes: [.update(newItem: groupModel)]) - } + updateAssetModels() } func didReceiveChainModelChanges(_ changes: [DataProviderChange]) { @@ -130,7 +134,7 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { groups.apply(changes: groupChanges) } - func didReceiveBalance(results: [ChainAssetId: Result]) { + func didReceiveBalance(results: [ChainAssetId: Result]) { var assetsChanges: [ChainModel.Id: [DataProviderChange]] = [:] var changedGroups: [ChainModel.Id: ChainModel] = [:] @@ -138,12 +142,16 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { switch result { case let .success(maybeAmount): if let amount = maybeAmount { - balanceResults[chainAssetId] = .success(amount) + balanceResults[chainAssetId] = .success(amount.total) + amount.balance.map { + balances[chainAssetId] = .success($0) + } } else if balanceResults[chainAssetId] == nil { balanceResults[chainAssetId] = .success(0) } case let .failure(error): balanceResults[chainAssetId] = .failure(error) + balances[chainAssetId] = .failure(error) } } @@ -180,4 +188,10 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { groups.apply(changes: groupChanges) } + + func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { + crowdloansResult = result + + updateAssetModels() + } } diff --git a/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift b/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift index 380e6ab412..d221549e2f 100644 --- a/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift +++ b/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift @@ -7,5 +7,26 @@ struct AssetListAssetModel: Identifiable { let assetModel: AssetModel let balanceResult: Result? - let assetValue: Decimal? + let balanceValue: Decimal? + + let crowdloanResult: Result? + let crowdloanValue: Decimal? + + var totalAmount: BigUInt? { + let maybeBalanceAmount = try? balanceResult?.get() + let maybeCrowdloanContribution = try? crowdloanResult?.get() + if let balanceAmount = maybeBalanceAmount, let crowdloanAmount = maybeCrowdloanContribution { + return balanceAmount + crowdloanAmount + } else { + return maybeBalanceAmount ?? maybeCrowdloanContribution + } + } + + var totalValue: Decimal? { + if let balanceValue = balanceValue, let crowdloanValue = crowdloanValue { + return balanceValue + crowdloanValue + } else { + return balanceValue ?? crowdloanValue + } + } } diff --git a/novawallet/Modules/AssetList/Models/Either.swift b/novawallet/Modules/AssetList/Models/Either.swift new file mode 100644 index 0000000000..ed329dfd60 --- /dev/null +++ b/novawallet/Modules/AssetList/Models/Either.swift @@ -0,0 +1,16 @@ +// https://github.com/apple/swift/blob/main/stdlib/public/core/EitherSequence.swift +enum Either { + case left(Left) + case right(Right) +} + +extension Either where Left == Right { + var value: Left { + switch self { + case let .left(left): + return left + case let .right(right): + return right + } + } +} diff --git a/novawallet/Modules/AssetList/Models/LoadableViewModelState+Addition.swift b/novawallet/Modules/AssetList/Models/LoadableViewModelState+Addition.swift new file mode 100644 index 0000000000..9824de6c7b --- /dev/null +++ b/novawallet/Modules/AssetList/Models/LoadableViewModelState+Addition.swift @@ -0,0 +1,12 @@ +extension LoadableViewModelState { + static func + (lhs: LoadableViewModelState<[T]>, rhs: [T]) -> LoadableViewModelState<[T]> { + switch lhs { + case let .cached(items): + return .cached(value: items + rhs) + case let .loaded(items): + return .loaded(value: items + rhs) + case .loading: + return lhs + } + } +} diff --git a/novawallet/Modules/AssetList/View/AssetListFlowLayout.swift b/novawallet/Modules/AssetList/View/AssetListFlowLayout.swift index cb8e6bd6a5..d74d30064e 100644 --- a/novawallet/Modules/AssetList/View/AssetListFlowLayout.swift +++ b/novawallet/Modules/AssetList/View/AssetListFlowLayout.swift @@ -2,7 +2,8 @@ import UIKit enum AssetListMeasurement { static let accountHeight: CGFloat = 56.0 - static let totalBalanceHeight: CGFloat = 96.0 + static let totalBalanceHeight: CGFloat = 103.0 + static let totalBalanceWithLocksHeight: CGFloat = 133.0 static let settingsHeight: CGFloat = 56.0 static let nftsHeight = 56.0 static let assetHeight: CGFloat = 56.0 @@ -13,6 +14,7 @@ enum AssetListMeasurement { final class AssetListFlowLayout: UICollectionViewFlowLayout { static let assetGroupDecoration = "assetGroupDecoration" + private var totalBalanceHeight: CGFloat = AssetListMeasurement.totalBalanceHeight enum SectionType: CaseIterable { case summary @@ -123,23 +125,6 @@ final class AssetListFlowLayout: UICollectionViewFlowLayout { return IndexPath(item: itemIndex, section: sectionIndex) } } - - var height: CGFloat { - switch self { - case .account: - return AssetListMeasurement.accountHeight - case .totalBalance: - return AssetListMeasurement.totalBalanceHeight - case .yourNfts: - return AssetListMeasurement.nftsHeight - case .settings: - return AssetListMeasurement.settingsHeight - case .emptyState: - return AssetListMeasurement.emptyStateCellHeight - case .asset: - return AssetListMeasurement.assetHeight - } - } } private var itemsDecorationAttributes: [UICollectionViewLayoutAttributes] = [] @@ -193,7 +178,7 @@ final class AssetListFlowLayout: UICollectionViewFlowLayout { if hasSummarySection { groupY = AssetListMeasurement.accountHeight + SectionType.summary.cellSpacing + - AssetListMeasurement.totalBalanceHeight + totalBalanceHeight } groupY += SectionType.summary.insets.top + SectionType.summary.insets.bottom @@ -203,7 +188,7 @@ final class AssetListFlowLayout: UICollectionViewFlowLayout { let hasNfts = collectionView.numberOfItems(inSection: SectionType.nfts.index) > 0 if hasNfts { - groupY += CellType.yourNfts.height + groupY += AssetListMeasurement.nftsHeight } groupY += SectionType.settings.insets.top + AssetListMeasurement.settingsHeight + @@ -245,4 +230,29 @@ final class AssetListFlowLayout: UICollectionViewFlowLayout { itemsDecorationAttributes = attributes } + + func updateTotalBalanceHeight(_ height: CGFloat) { + guard height != totalBalanceHeight else { + return + } + totalBalanceHeight = height + invalidateLayout() + } + + func cellHeight(for type: CellType) -> CGFloat { + switch type { + case .account: + return AssetListMeasurement.accountHeight + case .totalBalance: + return totalBalanceHeight + case .yourNfts: + return AssetListMeasurement.nftsHeight + case .settings: + return AssetListMeasurement.settingsHeight + case .emptyState: + return AssetListMeasurement.emptyStateCellHeight + case .asset: + return AssetListMeasurement.assetHeight + } + } } diff --git a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift index 9239aee052..fc5f585ddd 100644 --- a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift +++ b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift @@ -3,7 +3,7 @@ import SoraUI final class AssetListTotalBalanceCell: UICollectionViewCell { private enum Constants { - static let bottomInset: CGFloat = 16.0 + static let bottomInset: CGFloat = 20.0 } let backgroundBlurView: TriangularedBlurView = { @@ -20,9 +20,8 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { view.detailsLabel.textColor = R.color.colorTransparentText() view.detailsLabel.font = .regularSubheadline - view.imageView.image = R.image.iconInfoFilled()? - .withRenderingMode(.alwaysTemplate) - .tinted(with: R.color.colorWhite48()!) + view.imageView.image = R.image.iconInfoFilled()?.tinted(with: R.color.colorWhite48()!) + view.iconWidth = 16.0 view.spacing = 4.0 @@ -37,6 +36,16 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { return view }() + let locksView: BorderedIconLabelView = .create { + let color = R.color.colorWhite64()! + $0.iconDetailsView.imageView.image = R.image.iconBrowserSecurity()?.withTintColor(color) + $0.iconDetailsView.detailsLabel.font = .regularFootnote + $0.iconDetailsView.detailsLabel.textColor = color + $0.iconDetailsView.spacing = 4.0 + $0.contentInsets = UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6) + $0.isHidden = true + } + private var skeletonView: SkrullableView? var locale = Locale.current { @@ -72,14 +81,33 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { case let .loaded(value), let .cached(value): amountLabel.text = value + if let lockedAmount = viewModel.locksAmount { + setupStateWithLocks(amount: lockedAmount) + } else { + setupStateWithoutLocks() + } + stopLoadingIfNeeded() case .loading: amountLabel.text = "" - + setupStateWithoutLocks() startLoadingIfNeeded() } } + private func setupStateWithLocks(amount: String) { + locksView.isHidden = false + titleView.hidesIcon = false + + locksView.iconDetailsView.detailsLabel.text = amount + } + + private func setupStateWithoutLocks() { + locksView.iconDetailsView.detailsLabel.text = nil + locksView.isHidden = true + titleView.hidesIcon = true + } + private func setupLocalization() { titleView.detailsLabel.text = R.string.localizable.walletTotalBalance( preferredLanguages: locale.rLanguages @@ -96,11 +124,20 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { contentView.addSubview(titleView) titleView.snp.makeConstraints { make in make.centerX.equalToSuperview() - make.top.equalTo(backgroundBlurView.snp.top).offset(16.0) + make.top.equalTo(backgroundBlurView.snp.top).offset(20.0) } - contentView.addSubview(amountLabel) - amountLabel.snp.makeConstraints { make in + let amountView = UIStackView(arrangedSubviews: [ + amountLabel, + locksView + ]) + amountView.spacing = 8.0 + amountView.axis = .vertical + amountView.alignment = .center + + contentView.addSubview(amountView) + amountView.snp.makeConstraints { make in + make.top.greaterThanOrEqualTo(titleView.snp.bottom).offset(3) make.leading.equalTo(backgroundBlurView).offset(8.0) make.trailing.equalTo(backgroundBlurView).offset(-8.0) make.bottom.equalToSuperview().inset(Constants.bottomInset) diff --git a/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift b/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift index 12b2f0045f..42749adc7e 100644 --- a/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift +++ b/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift @@ -15,6 +15,7 @@ enum ValueDirection { struct AssetListHeaderViewModel { let title: String let amount: LoadableViewModelState + let locksAmount: String? let walletSwitch: WalletSwitchViewModel } diff --git a/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift b/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift index 347d947b75..f345dd1865 100644 --- a/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift +++ b/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift @@ -15,6 +15,7 @@ protocol AssetListViewModelFactoryProtocol: AssetListAssetViewModelFactoryProtoc walletIdenticon: Data?, walletType: MetaAccountModelType, prices: LoadableViewModelState<[AssetListAssetAccountPrice]>?, + locks: [AssetListAssetAccountPrice]?, locale: Locale ) -> AssetListHeaderViewModel @@ -84,6 +85,7 @@ extension AssetListViewModelFactory: AssetListViewModelFactoryProtocol { walletIdenticon: Data?, walletType: MetaAccountModelType, prices: LoadableViewModelState<[AssetListAssetAccountPrice]>?, + locks: [AssetListAssetAccountPrice]?, locale: Locale ) -> AssetListHeaderViewModel { let icon = walletIdenticon.flatMap { try? iconGenerator.generateFromAccountId($0) } @@ -97,12 +99,14 @@ extension AssetListViewModelFactory: AssetListViewModelFactoryProtocol { return AssetListHeaderViewModel( title: title, amount: totalPrice, + locksAmount: locks.map { formatTotalPrice(from: $0, locale: locale) }, walletSwitch: walletSwitch ) } else { return AssetListHeaderViewModel( title: title, amount: .loading, + locksAmount: nil, walletSwitch: walletSwitch ) } diff --git a/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift b/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift index 5e64cc4c85..931cc1edec 100644 --- a/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift +++ b/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift @@ -114,6 +114,7 @@ final class AssetsSearchPresenter: AssetListBasePresenter { private func provideAssetsViewModel() { let maybePrices = try? priceResult?.get() + let viewModels: [AssetListGroupViewModel] = groups.allItems.compactMap { groupModel in createGroupViewModel(from: groupModel, maybePrices: maybePrices) } @@ -152,7 +153,7 @@ final class AssetsSearchPresenter: AssetListBasePresenter { filterAndUpdateView() } - override func didReceiveBalance(results: [ChainAssetId: Result]) { + override func didReceiveBalance(results: [ChainAssetId: Result]) { super.didReceiveBalance(results: results) filterAndUpdateView() @@ -163,6 +164,12 @@ final class AssetsSearchPresenter: AssetListBasePresenter { filterAndUpdateView() } + + override func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { + super.didReceiveCrowdloans(result: result) + + filterAndUpdateView() + } } extension AssetsSearchPresenter: AssetsSearchPresenterProtocol { diff --git a/novawallet/Modules/AssetsSearch/AssetsSearchViewFactory.swift b/novawallet/Modules/AssetsSearch/AssetsSearchViewFactory.swift index 888366af9e..c2f86a41a0 100644 --- a/novawallet/Modules/AssetsSearch/AssetsSearchViewFactory.swift +++ b/novawallet/Modules/AssetsSearch/AssetsSearchViewFactory.swift @@ -13,6 +13,7 @@ struct AssetsSearchViewFactory { selectedWalletSettings: SelectedWalletSettings.shared, chainRegistry: ChainRegistryFacade.sharedRegistry, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, currencyManager: currencyManager, logger: Logger.shared diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListPresenter.swift b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListPresenter.swift index 259af92d16..9cce7911d1 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListPresenter.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListPresenter.swift @@ -46,12 +46,6 @@ final class CrowdloanListPresenter { self.localizationManager = localizationManager } - private func provideViewErrorState() { - let message = R.string.localizable - .commonErrorNoDataRetrieved(preferredLanguages: selectedLocale.rLanguages) - view?.didReceive(listState: .error(message: message)) - } - private func updateWalletSwitchView() { guard let wallet = wallet else { return @@ -73,7 +67,7 @@ final class CrowdloanListPresenter { guard case let .success(chain) = chainResult, let asset = chain.utilityAssets().first else { - provideViewErrorState() + provideViewError(chainAsset: nil) return } @@ -156,7 +150,7 @@ final class CrowdloanListPresenter { } guard case let .success(chain) = chainResult, let asset = chain.utilityAssets().first else { - provideViewErrorState() + provideViewError(chainAsset: nil) return } @@ -166,17 +160,11 @@ final class CrowdloanListPresenter { return } + let chainAsset = ChainAssetDisplayInfo(asset: asset.displayInfo, chain: chain.chainFormat) do { let crowdloans = try crowdloansResult.get() let priceData = try? priceDataResult?.get() ?? nil - - guard !crowdloans.isEmpty else { - view?.didReceive(listState: .empty) - return - } - let viewInfo = try viewInfoResult.get() - let chainAsset = ChainAssetDisplayInfo(asset: asset.displayInfo, chain: chain.chainFormat) let externalContributionsCount = externalContributions?.count ?? 0 let amount: Decimal? @@ -202,7 +190,7 @@ final class CrowdloanListPresenter { view?.didReceive(listState: .loaded(viewModel: viewModel)) } catch { - provideViewErrorState() + provideViewError(chainAsset: chainAsset) } } @@ -221,6 +209,14 @@ final class CrowdloanListPresenter { displayInfo: displayInfo ) } + + private func provideViewError(chainAsset: ChainAssetDisplayInfo?) { + let viewModel = viewModelFactory.createErrorViewModel( + chainAsset: chainAsset, + locale: selectedLocale + ) + view?.didReceive(listState: .loaded(viewModel: viewModel)) + } } extension CrowdloanListPresenter: CrowdloanListPresenterProtocol { diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewController.swift b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewController.swift index 3bab2a7b6f..92948b3513 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewController.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewController.swift @@ -76,6 +76,8 @@ final class CrowdloanListViewController: UIViewController, ViewHolder { rootView.tableView.registerClassForCell(YourContributionsTableViewCell.self) rootView.tableView.registerClassForCell(AboutCrowdloansTableViewCell.self) rootView.tableView.registerClassForCell(CrowdloanTableViewCell.self) + rootView.tableView.registerClassForCell(BlurredTableViewCell.self) + rootView.tableView.registerClassForCell(BlurredTableViewCell.self) rootView.tableView.registerHeaderFooterView(withClass: CrowdloanStatusSectionView.self) rootView.tableView.dataSource = self rootView.tableView.delegate = self @@ -107,19 +109,12 @@ final class CrowdloanListViewController: UIViewController, ViewHolder { switch state { case .loading: didStartLoading() - rootView.bringSubviewToFront(rootView.tableView) case .loaded: rootView.tableView.refreshControl?.endRefreshing() didStopLoading() - rootView.bringSubviewToFront(rootView.tableView) - case .empty, .error: - rootView.tableView.refreshControl?.endRefreshing() - didStopLoading() - rootView.bringSubviewToFront(rootView.statusView) } rootView.tableView.reloadData() - reloadEmptyState(animated: false) } @objc func actionRefresh() { @@ -140,7 +135,7 @@ extension CrowdloanListViewController: UITableViewDataSource { switch state { case let .loaded(viewModel): return viewModel.sections.count - case .loading, .empty, .error: + case .loading: return 0 } } @@ -154,10 +149,10 @@ extension CrowdloanListViewController: UITableViewDataSource { return cellViewModels.count case let .completed(_, cellViewModels): return cellViewModels.count - case .yourContributions, .about: + case .yourContributions, .about, .error, .empty: return 1 } - case .loading, .empty, .error: + case .loading: return 0 } } @@ -180,8 +175,25 @@ extension CrowdloanListViewController: UITableViewDataSource { let cell = tableView.dequeueReusableCellWithType(AboutCrowdloansTableViewCell.self)! cell.view.bind(model: model) return cell + case let .error(message): + let cell: BlurredTableViewCell = tableView.dequeueReusableCell(for: indexPath) + cell.view.errorDescriptionLabel.text = message + cell.view.delegate = self + cell.view.locale = selectedLocale + cell.applyStyle() + return cell + case .empty: + let cell: BlurredTableViewCell = tableView.dequeueReusableCell(for: indexPath) + let text = R.string.localizable + .crowdloanEmptyMessage_v3_9_1(preferredLanguages: selectedLocale.rLanguages) + cell.view.bind( + image: R.image.iconEmptyHistory(), + text: text + ) + cell.applyStyle() + return cell } - case .loading, .empty, .error: + case .loading: return UITableViewCell() } } @@ -218,6 +230,10 @@ extension CrowdloanListViewController: UITableViewDelegate { let headerView: CrowdloanStatusSectionView = tableView.dequeueReusableHeaderFooterView() headerView.bind(title: title, count: cells.count) return headerView + case let .empty(title): + let headerView: CrowdloanStatusSectionView = tableView.dequeueReusableHeaderFooterView() + headerView.bind(title: title, count: 0) + return headerView default: return nil } @@ -230,7 +246,7 @@ extension CrowdloanListViewController: UITableViewDelegate { let sectionModel = viewModel.sections[section] switch sectionModel { - case .active, .completed: + case .active, .completed, .empty: return UITableView.automaticDimension default: return 0.0 @@ -265,52 +281,11 @@ extension CrowdloanListViewController: Localizable { } } -extension CrowdloanListViewController: LoadableViewProtocol {} - -extension CrowdloanListViewController: EmptyStateViewOwnerProtocol { - var emptyStateDelegate: EmptyStateDelegate { self } - var emptyStateDataSource: EmptyStateDataSource { self } - var contentViewForEmptyState: UIView { rootView.statusView } -} - -extension CrowdloanListViewController: EmptyStateDataSource { - var viewForEmptyState: UIView? { - switch state { - case let .error(message): - let errorView = ErrorStateView() - errorView.errorDescriptionLabel.text = message - errorView.delegate = self - errorView.locale = selectedLocale - return errorView - case .empty: - let emptyView = EmptyStateView() - emptyView.image = R.image.iconEmptyHistory() - emptyView.title = R.string.localizable - .crowdloanEmptyMessage_v2_2_0(preferredLanguages: selectedLocale.rLanguages) - emptyView.titleColor = R.color.colorLightGray()! - emptyView.titleFont = .p2Paragraph - return emptyView - case .loading, .loaded: - return nil - } - } -} - -extension CrowdloanListViewController: EmptyStateDelegate { - var shouldDisplayEmptyState: Bool { - switch state { - case .error, .empty: - return true - case .loading, .loaded: - return false - } - } -} - extension CrowdloanListViewController: ErrorStateViewDelegate { func didRetry(errorView _: ErrorStateView) { presenter.refresh(shouldReset: true) } } +extension CrowdloanListViewController: LoadableViewProtocol {} extension CrowdloanListViewController: HiddableBarWhenPushed {} diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewLayout.swift b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewLayout.swift index e06426afc1..905e85ee20 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewLayout.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewLayout.swift @@ -17,8 +17,6 @@ final class CrowdloanListViewLayout: UIView { return view }() - let statusView = UIView() - override init(frame: CGRect) { super.init(frame: frame) @@ -51,13 +49,6 @@ final class CrowdloanListViewLayout: UIView { addSubview(backgroundView) backgroundView.snp.makeConstraints { $0.edges.equalToSuperview() } - addSubview(statusView) - statusView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(safeAreaLayoutGuide) - make.top.equalTo(safeAreaLayoutGuide).inset(253) - } - addSubview(tableView) tableView.snp.makeConstraints { make in make.top.equalToSuperview() diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanEmptyView.swift b/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanEmptyView.swift new file mode 100644 index 0000000000..4a952788a8 --- /dev/null +++ b/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanEmptyView.swift @@ -0,0 +1,52 @@ +import UIKit + +final class CrowdloanEmptyView: UIView { + var verticalSpacing: CGFloat = 8.0 { + didSet { + stackView.spacing = verticalSpacing + } + } + + let imageView = UIImageView() + + let titleLabel: UILabel = .create { + $0.numberOfLines = 0 + $0.textAlignment = .center + $0.backgroundColor = .clear + $0.textColor = R.color.colorWhite64() + $0.font = .p2Paragraph + } + + private lazy var stackView = UIStackView(arrangedSubviews: [ + imageView, + titleLabel + ]) + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + stackView.axis = .vertical + stackView.alignment = .center + stackView.spacing = verticalSpacing + + addSubview(stackView) + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + func bind(image: UIImage?, text: String?) { + imageView.image = image + titleLabel.text = text + } +} diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanStatusSectionView.swift b/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanStatusSectionView.swift index 7c9bcf734e..6fe6576cad 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanStatusSectionView.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanStatusSectionView.swift @@ -1,14 +1,16 @@ import UIKit final class CrowdloanStatusSectionView: UITableViewHeaderFooterView { - let titleLabel: UILabel = { - let label = UILabel() - label.textColor = R.color.colorWhite() - label.font = .h3Title - return label - }() + let titleLabel: UILabel = .create { + $0.textColor = R.color.colorWhite() + $0.font = .h3Title + } - private let countView = CountView() + let countView: BorderedLabelView = .create { + $0.titleLabel.textColor = R.color.colorWhite80() + $0.titleLabel.font = .semiBoldFootnote + $0.contentInsets = UIEdgeInsets(top: 2, left: 8, bottom: 3, right: 8) + } override init(reuseIdentifier: String?) { super.init(reuseIdentifier: reuseIdentifier) @@ -27,7 +29,7 @@ final class CrowdloanStatusSectionView: UITableViewHeaderFooterView { titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(24) make.leading.equalToSuperview().inset(20) - make.bottom.equalToSuperview().inset(8) + make.bottom.equalToSuperview().inset(16) } contentView.addSubview(countView) @@ -40,38 +42,6 @@ final class CrowdloanStatusSectionView: UITableViewHeaderFooterView { func bind(title: String, count: Int) { titleLabel.text = title - countView.countLabel.text = count.description - } -} - -private final class CountView: UIView { - let countLabel: UILabel = { - let label = UILabel() - label.textColor = R.color.colorWhite() - label.font = .p3Paragraph - return label - }() - - override func layoutSubviews() { - super.layoutSubviews() - - layer.cornerRadius = bounds.height / 2.0 - } - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = R.color.colorWhite()?.withAlphaComponent(0.24) - - addSubview(countLabel) - countLabel.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(8) - make.bottom.top.equalToSuperview().inset(2) - } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") + countView.titleLabel.text = count.description } } diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModel.swift b/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModel.swift index bb85620020..cfb9573ddb 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModel.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModel.swift @@ -5,8 +5,6 @@ import CommonWallet enum CrowdloanListState { case loading case loaded(viewModel: CrowdloansViewModel) - case error(message: String) - case empty } struct CrowdloansViewModel { @@ -18,6 +16,8 @@ enum CrowdloansSection { case about(AboutCrowdloansView.Model) case active(String, [CrowdloanCellViewModel]) case completed(String, [CrowdloanCellViewModel]) + case error(message: String) + case empty(title: String) } enum CrowdloanDescViewModel { diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift b/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift index b821bb5e92..d3bbd2f1f1 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift @@ -21,6 +21,11 @@ protocol CrowdloansViewModelFactoryProtocol { priceData: PriceData?, locale: Locale ) -> CrowdloansViewModel + + func createErrorViewModel( + chainAsset: ChainAssetDisplayInfo?, + locale: Locale + ) -> CrowdloansViewModel } final class CrowdloansViewModelFactory { @@ -338,6 +343,20 @@ extension CrowdloansViewModelFactory: CrowdloansViewModelFactoryProtocol { ) } + func createErrorViewModel( + chainAsset: ChainAssetDisplayInfo?, + locale: Locale + ) -> CrowdloansViewModel { + let message = R.string.localizable + .commonErrorNoDataRetrieved_v3_9_1(preferredLanguages: locale.rLanguages) + let errorSection = CrowdloansSection.error(message: message) + let aboutSection = createAboutSection(chainAsset: chainAsset, locale: locale) + return .init(sections: [ + aboutSection, + errorSection + ]) + } + func createViewModel( from crowdloans: [Crowdloan], viewInfo: CrowdloansViewInfo, @@ -347,6 +366,18 @@ extension CrowdloansViewModelFactory: CrowdloansViewModelFactoryProtocol { priceData: PriceData?, locale: Locale ) -> CrowdloansViewModel { + guard !crowdloans.isEmpty else { + let aboutSection = createAboutSection(chainAsset: chainAsset, locale: locale) + let activeTitle = R.string.localizable + .crowdloanActiveSection(preferredLanguages: locale.rLanguages) + let emptySection = CrowdloansSection.empty(title: activeTitle) + + return .init(sections: [ + aboutSection, + emptySection + ]) + } + let timeFormatter = TotalTimeFormatter() let quantityFormatter = NumberFormatter.quantity.localizableResource().value(for: locale) let tokenFormatter = amountFormatterFactory.createTokenFormatter( @@ -392,9 +423,10 @@ extension CrowdloansViewModelFactory: CrowdloansViewModelFactoryProtocol { return .init(sections: [contributionSection] + crowdloansSections) } - private func createAboutSection(chainAsset: ChainAssetDisplayInfo, locale: Locale) -> CrowdloansSection { + private func createAboutSection(chainAsset: ChainAssetDisplayInfo?, locale: Locale) -> CrowdloansSection { + let symbol = chainAsset?.asset.symbol ?? "" let description = R.string.localizable.crowdloanListSectionFormat_v2_2_0( - chainAsset.asset.symbol, + symbol, preferredLanguages: locale.rLanguages ) diff --git a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/AcalaContributionSource.swift b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/AcalaContributionSource.swift index ada8bc2e8b..838d2ee047 100644 --- a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/AcalaContributionSource.swift +++ b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/AcalaContributionSource.swift @@ -5,6 +5,7 @@ import BigInt final class AcalaContributionSource: ExternalContributionSourceProtocol { static let baseUrl = URL(string: "https://crowdloan.aca-api.network")! static let apiContribution = "/contribution" + var sourceName: String { "Acala Liquid" } let paraIdOperationFactory: ParaIdOperationFactoryProtocol let acalaChainId: ChainModel.Id @@ -37,7 +38,7 @@ final class AcalaContributionSource: ExternalContributionSourceProtocol { let paraIdWrapper = paraIdOperationFactory.createParaIdOperation(for: acalaChainId) - let mergeOperation = ClosureOperation<[ExternalContribution]> { + let mergeOperation = ClosureOperation<[ExternalContribution]> { [sourceName] in let response = try networkOperation.extractNoCancellableResultData() let paraId = try paraIdWrapper.targetOperation.extractNoCancellableResultData() @@ -45,7 +46,7 @@ final class AcalaContributionSource: ExternalContributionSourceProtocol { throw CrowdloanBonusServiceError.internalError } - return [ExternalContribution(source: "Liquid", amount: amount, paraId: paraId)] + return [ExternalContribution(source: sourceName, amount: amount, paraId: paraId)] } let dependencies = [networkOperation] + paraIdWrapper.allOperations diff --git a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ExternalContributionSource.swift b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ExternalContributionSource.swift index 0ea5ea9e5d..99f8635ab9 100644 --- a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ExternalContributionSource.swift +++ b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ExternalContributionSource.swift @@ -3,6 +3,7 @@ import RobinHood import SoraKeystore protocol ExternalContributionSourceProtocol { + var sourceName: String { get } func getContributions(accountId: AccountId, chain: ChainModel) -> CompoundOperationWrapper<[ExternalContribution]> } diff --git a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ParallelContributionSource.swift b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ParallelContributionSource.swift index f5ac1b2902..3556438b81 100644 --- a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ParallelContributionSource.swift +++ b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ParallelContributionSource.swift @@ -3,6 +3,7 @@ import RobinHood final class ParallelContributionSource: ExternalContributionSourceProtocol { static let baseURL = URL(string: "https://auction-service-prod.parallel.fi/crowdloan/rewards")! + var sourceName: String { "Parallel" } func getContributions(accountId: AccountId, chain: ChainModel) -> CompoundOperationWrapper<[ExternalContribution]> { guard let accountAddress = try? accountId.toAddress(using: chain.chainFormat) else { @@ -19,13 +20,13 @@ final class ParallelContributionSource: ExternalContributionSourceProtocol { return request } - let resultFactory = AnyNetworkResultFactory<[ExternalContribution]> { data in + let resultFactory = AnyNetworkResultFactory<[ExternalContribution]> { [sourceName] data in let resultData = try JSONDecoder().decode( [ParallelContributionResponse].self, from: data ) - return resultData.map { ExternalContribution(source: "Parallel", amount: $0.amount, paraId: $0.paraId) } + return resultData.map { ExternalContribution(source: sourceName, amount: $0.amount, paraId: $0.paraId) } } let operation = NetworkOperation(requestFactory: requestFactory, resultFactory: resultFactory) diff --git a/novawallet/Modules/Crowdloan/View/BlurredTableViewCell.swift b/novawallet/Modules/Crowdloan/View/BlurredTableViewCell.swift index f0a1abfad2..b8585e2493 100644 --- a/novawallet/Modules/Crowdloan/View/BlurredTableViewCell.swift +++ b/novawallet/Modules/Crowdloan/View/BlurredTableViewCell.swift @@ -10,6 +10,12 @@ class BlurredTableViewCell: UITableViewCell where TContentView: UI } } + var innerInsets: UIEdgeInsets = .zero { + didSet { + updateLayout() + } + } + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -35,7 +41,7 @@ class BlurredTableViewCell: UITableViewCell where TContentView: UI backgroundBlurView.addSubview(view) view.snp.makeConstraints { - $0.leading.top.trailing.bottom.equalToSuperview() + $0.edges.equalToSuperview().inset(innerInsets) } } @@ -43,6 +49,9 @@ class BlurredTableViewCell: UITableViewCell where TContentView: UI backgroundBlurView.snp.updateConstraints { $0.edges.equalToSuperview().inset(contentInsets) } + view.snp.updateConstraints { + $0.edges.equalToSuperview().inset(innerInsets) + } } } @@ -63,3 +72,21 @@ final class YourContributionsTableViewCell: BlurredTableViewCell + +extension BlurredTableViewCell where TContentView == ErrorStateView { + func applyStyle() { + view.errorDescriptionLabel.textColor = R.color.colorWhite64() + view.retryButton.titleLabel?.font = .semiBoldSubheadline + view.stackView.setCustomSpacing(0, after: view.iconImageView) + view.stackView.setCustomSpacing(8, after: view.errorDescriptionLabel) + contentInsets = .init(top: 8, left: 16, bottom: 0, right: 16) + innerInsets = .init(top: 4, left: 0, bottom: 16, right: 0) + } +} + +extension BlurredTableViewCell where TContentView == CrowdloanEmptyView { + func applyStyle() { + view.verticalSpacing = 0 + innerInsets = .init(top: 4, left: 0, bottom: 16, right: 0) + } +} diff --git a/novawallet/Modules/Currency/View/CurrencyViewLayout.swift b/novawallet/Modules/Currency/View/CurrencyViewLayout.swift index a8f0b4dfe5..453a73ac0b 100644 --- a/novawallet/Modules/Currency/View/CurrencyViewLayout.swift +++ b/novawallet/Modules/Currency/View/CurrencyViewLayout.swift @@ -34,6 +34,7 @@ final class CurrencyViewLayout: UIView { private func createCompositionalLayout() -> UICollectionViewCompositionalLayout { let settings = NSCollectionLayoutSection.Settings( estimatedRowHeight: Constants.estimatedRowHeight, + absoluteHeaderHeight: nil, estimatedHeaderHeight: Constants.estimatedHeaderHeight, sectionContentInsets: Constants.sectionContentInsets, sectionInterGroupSpacing: Constants.interGroupSpacing, diff --git a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift new file mode 100644 index 0000000000..3804448336 --- /dev/null +++ b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift @@ -0,0 +1,198 @@ +import BigInt +import Foundation + +protocol LocksBalanceViewModelFactoryProtocol { + func formatBalance( + balances: [AssetBalance], + chains: [ChainModel.Id: ChainModel], + prices: [ChainAssetId: PriceData], + crowdloans: [ChainModel.Id: [CrowdloanContributionData]], + locale: Locale + ) -> FormattedBalance + func formatPlankValue( + plank: BigUInt, + chainAssetId: ChainAssetId, + chains: [ChainModel.Id: ChainModel], + prices: [ChainAssetId: PriceData], + locale: Locale + ) -> FormattedPlank? +} + +struct FormattedBalance { + let total: String + let transferrable: String + let locks: String + + let totalPrice: Decimal + let transferrablePrice: Decimal + let locksPrice: Decimal +} + +struct FormattedPlank { + let amount: String + let price: String? + let priceValue: Decimal +} + +final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { + let priceAssetInfoFactory: PriceAssetInfoFactoryProtocol + let assetFormatterFactory: AssetBalanceFormatterFactoryProtocol + let currencyManager: CurrencyManagerProtocol + + init( + priceAssetInfoFactory: PriceAssetInfoFactoryProtocol, + assetFormatterFactory: AssetBalanceFormatterFactoryProtocol, + currencyManager: CurrencyManagerProtocol + ) { + self.priceAssetInfoFactory = priceAssetInfoFactory + self.assetFormatterFactory = assetFormatterFactory + self.currencyManager = currencyManager + } + + func formatBalance( + balances: [AssetBalance], + chains: [ChainModel.Id: ChainModel], + prices: [ChainAssetId: PriceData], + crowdloans: [ChainModel.Id: [CrowdloanContributionData]], + locale: Locale + ) -> FormattedBalance { + var totalPrice: Decimal = 0 + var transferrablePrice: Decimal = 0 + var locksPrice: Decimal = 0 + var lastPriceData: PriceData? + + for balance in balances { + let priceData = prices[balance.chainAssetId] ?? .zero() + + guard let assetPrecision = chains[balance.chainAssetId.chainId]? + .asset(for: balance.chainAssetId.assetId)? + .precision else { + continue + } + + let rate = Decimal(string: priceData.price) ?? 0.0 + + totalPrice += calculateAmount( + from: balance.totalInPlank, + precision: assetPrecision, + rate: rate + ) + transferrablePrice += calculateAmount( + from: balance.transferable, + precision: assetPrecision, + rate: rate + ) + locksPrice += calculateAmount( + from: balance.locked, + precision: assetPrecision, + rate: rate + ) + + lastPriceData = priceData + } + + let crowdloansTotalPrice: Decimal = crowdloans.reduce(0) { result, crowdloan in + guard let asset = chains[crowdloan.key]?.utilityAsset() else { + return result + } + let priceData = prices[.init(chainId: crowdloan.key, assetId: asset.assetId)] + let rate = priceData.map { Decimal(string: $0.price) ?? 0 } ?? 0 + return result + calculateAmount( + from: crowdloan.value.reduce(0) { $0 + $1.amount }, + precision: asset.precision, + rate: rate + ) + } + + let total = totalPrice + crowdloansTotalPrice + let formattedTotal = formatPrice( + amount: total, + priceData: lastPriceData, + locale: locale + ) + let formattedTransferrable = formatPrice(amount: transferrablePrice, priceData: lastPriceData, locale: locale) + let totalLocks = locksPrice + crowdloansTotalPrice + let formattedLocks = formatPrice( + amount: totalLocks, + priceData: lastPriceData, + locale: locale + ) + return .init( + total: formattedTotal, + transferrable: formattedTransferrable, + locks: formattedLocks, + totalPrice: total, + transferrablePrice: transferrablePrice, + locksPrice: totalLocks + ) + } + + func formatPlankValue( + plank: BigUInt, + chainAssetId: ChainAssetId, + chains: [ChainModel.Id: ChainModel], + prices: [ChainAssetId: PriceData], + locale: Locale + ) -> FormattedPlank? { + guard let chain = chains[chainAssetId.chainId], let asset = chain.asset(for: chainAssetId.assetId) else { + return nil + } + + let assetPrecision = asset.precision + + let priceData = prices[chainAssetId] + + let rate = priceData.map { Decimal(string: $0.price) ?? 0 } ?? 0 + + let price = calculateAmount( + from: plank, + precision: assetPrecision, + rate: rate + ) + + let amount = calculateAmount( + from: plank, + precision: assetPrecision, + rate: nil + ) + let formattedAmount = formatAmount( + amount, + assetDisplayInfo: ChainAsset(chain: chain, asset: asset).assetDisplayInfo, + locale: locale + ) + + let formattedPrice = formatPrice(amount: price, priceData: priceData, locale: locale) + return .init( + amount: formattedAmount, + price: formattedPrice, + priceValue: price + ) + } + + private func calculateAmount(from plank: BigUInt, precision: UInt16, rate: Decimal?) -> Decimal { + let amount = Decimal.fromSubstrateAmount( + plank, + precision: Int16(precision) + ) ?? 0.0 + + return rate.map { + amount * $0 + } ?? amount + } + + private func formatPrice(amount: Decimal, priceData: PriceData?, locale: Locale) -> String { + let currencyId = priceData?.currencyId ?? currencyManager.selectedCurrency.id + let assetDisplayInfo = priceAssetInfoFactory.createAssetBalanceDisplayInfo(from: currencyId) + let priceFormatter = assetFormatterFactory.createTokenFormatter(for: assetDisplayInfo) + return priceFormatter.value(for: locale).stringFromDecimal(amount) ?? "" + } + + private func formatAmount( + _ amount: Decimal, + assetDisplayInfo: AssetBalanceDisplayInfo, + locale: Locale + ) -> String { + let priceFormatter = assetFormatterFactory.createTokenFormatter(for: assetDisplayInfo) + return priceFormatter.value(for: locale).stringFromDecimal(amount) ?? "" + } +} diff --git a/novawallet/Modules/Locks/LocksPresenter.swift b/novawallet/Modules/Locks/LocksPresenter.swift new file mode 100644 index 0000000000..d42cc2be70 --- /dev/null +++ b/novawallet/Modules/Locks/LocksPresenter.swift @@ -0,0 +1,194 @@ +import Foundation +import BigInt +import SoraFoundation + +final class LocksPresenter { + weak var view: LocksViewProtocol? + let wireframe: LocksWireframeProtocol + let input: LocksViewInput + let priceViewModelFactory: LocksBalanceViewModelFactoryProtocol + lazy var formatter: NumberFormatter = { + let formatter = NumberFormatter.percent + formatter.roundingMode = .halfEven + return formatter + }() + + init( + input: LocksViewInput, + wireframe: LocksWireframeProtocol, + localizationManager: LocalizationManagerProtocol, + priceViewModelFactory: LocksBalanceViewModelFactoryProtocol + ) { + self.input = input + self.wireframe = wireframe + self.priceViewModelFactory = priceViewModelFactory + self.localizationManager = localizationManager + } + + private func updateView() { + let balanceModel = priceViewModelFactory.formatBalance( + balances: input.balances, + chains: input.chains, + prices: input.prices, + crowdloans: input.crowdloans, + locale: selectedLocale + ) + + let header = R.string.localizable.walletSendBalanceTotal( + preferredLanguages: selectedLocale.rLanguages + ) + + view?.updateHeader(title: header, value: balanceModel.total) + view?.update(viewModel: [ + createTranferrableSection(balanceModel: balanceModel), + createLocksSection(balanceModel: balanceModel) + ]) + } + + private func createTranferrableSection(balanceModel: FormattedBalance) -> LocksViewSectionModel { + let percent = balanceModel.totalPrice > 0 ? + balanceModel.transferrablePrice / balanceModel.totalPrice : 0 + let displayPercent = formatter.stringFromDecimal(percent) ?? "" + return LocksViewSectionModel( + header: .init( + icon: R.image.iconTransferable(), + title: R.string.localizable.walletBalanceAvailable( + preferredLanguages: selectedLocale.rLanguages + ), + details: displayPercent, + value: balanceModel.transferrable + ), + cells: [] + ) + } + + private func createLocksSection(balanceModel: FormattedBalance) -> LocksViewSectionModel { + let percent = balanceModel.totalPrice > 0 ? + balanceModel.locksPrice / balanceModel.totalPrice : 0 + let displayPercent = formatter.stringFromDecimal(percent) ?? "" + let locksCells = createLocksCells().sorted { + $0.priceValue > $1.priceValue + } + + return LocksViewSectionModel( + header: .init( + icon: R.image.iconLock(), + title: R.string.localizable.walletBalanceLocked( + preferredLanguages: selectedLocale.rLanguages + ), + details: displayPercent, + value: balanceModel.locks + ), + cells: locksCells + ) + } + + private func createLocksCells() -> [LocksViewSectionModel.CellViewModel] { + let locksCells: [LocksViewSectionModel.CellViewModel] = input.locks.compactMap { + createCell( + amountInPlank: $0.amount, + chainAssetId: $0.chainAssetId, + title: $0.lockType.map { $0.displayType.value(for: selectedLocale) } ?? $0.displayId?.capitalized ?? "", + identifier: $0.identifier + ) + } + + let reservedCells: [LocksViewSectionModel.CellViewModel] = input.balances.compactMap { + createCell( + amountInPlank: $0.reservedInPlank, + chainAssetId: $0.chainAssetId, + title: R.string.localizable.walletBalanceReserved( + preferredLanguages: selectedLocale.rLanguages + ), + identifier: $0.identifier + ) + } + + let crowdloanCells: [LocksViewSectionModel.CellViewModel] = input.crowdloans.compactMap { + guard let utilityAsset = input.chains[$0.key]?.utilityAsset() else { + return nil + } + return createCell( + amountInPlank: $0.value.reduce(0) { $0 + $1.amount }, + chainAssetId: ChainAssetId(chainId: $0.key, assetId: utilityAsset.assetId), + title: R.string.localizable.tabbarCrowdloanTitle( + preferredLanguages: selectedLocale.rLanguages + ), + identifier: $0.key + ) + } + + return locksCells + reservedCells + crowdloanCells + } + + private func createCell( + amountInPlank: BigUInt, + chainAssetId: ChainAssetId, + title: String, + identifier: String + ) -> LocksViewSectionModel.CellViewModel? { + guard amountInPlank > 0 else { + return nil + } + guard let chain = input.chains[chainAssetId.chainId] else { + return nil + } + guard let asset = chain.asset(for: chainAssetId.assetId) else { + return nil + } + let title = [asset.symbol, title].joined(separator: " ") + + guard let value = priceViewModelFactory.formatPlankValue( + plank: amountInPlank, + chainAssetId: chainAssetId, + chains: input.chains, + prices: input.prices, + locale: selectedLocale + ) else { + return nil + } + + return LocksViewSectionModel.CellViewModel( + id: identifier, + title: title, + amount: value.amount, + price: value.price, + priceValue: value.priceValue + ) + } + + var contentHeight: CGFloat { + let reservedCellsCount = input.balances.filter { + $0.reservedInPlank > 0 + }.count + let locksCellsCount = input.locks.filter { + $0.amount > 0 + }.count + let crowdloanCellsCount = input.crowdloans.filter { crowdloan in + crowdloan.value.first(where: { $0.amount > 0 }) != nil + }.count + return view?.calculateEstimatedHeight( + sections: 2, + items: locksCellsCount + reservedCellsCount + crowdloanCellsCount + ) ?? 0 + } +} + +extension LocksPresenter: LocksPresenterProtocol { + func setup() { + updateView() + } + + func didTapOnCell() { + wireframe.close(view: view) + } +} + +extension LocksPresenter: Localizable { + func applyLocalization() { + guard view?.isSetup == true else { + return + } + updateView() + } +} diff --git a/novawallet/Modules/Locks/LocksProtocols.swift b/novawallet/Modules/Locks/LocksProtocols.swift new file mode 100644 index 0000000000..0a4cbef99a --- /dev/null +++ b/novawallet/Modules/Locks/LocksProtocols.swift @@ -0,0 +1,36 @@ +import UIKit + +protocol LocksViewProtocol: ControllerBackedProtocol { + func update(viewModel: [LocksViewSectionModel]) + func updateHeader(title: String, value: String) + func calculateEstimatedHeight(sections: Int, items: Int) -> CGFloat +} + +protocol LocksPresenterProtocol: AnyObject { + func setup() + func didTapOnCell() +} + +protocol LocksWireframeProtocol: AnyObject { + func close(view: LocksViewProtocol?) +} + +struct LocksViewSectionModel: SectionProtocol, Hashable { + let header: HeaderViewModel + var cells: [CellViewModel] + + struct HeaderViewModel: Hashable { + let icon: UIImage? + let title: String + let details: String + let value: String + } + + struct CellViewModel: Hashable { + let id: String + let title: String + let amount: String + let price: String? + let priceValue: Decimal + } +} diff --git a/novawallet/Modules/Locks/LocksViewController.swift b/novawallet/Modules/Locks/LocksViewController.swift new file mode 100644 index 0000000000..5fe10c082c --- /dev/null +++ b/novawallet/Modules/Locks/LocksViewController.swift @@ -0,0 +1,115 @@ +import UIKit + +final class LocksViewController: UIViewController, ViewHolder, ModalSheetCollectionViewProtocol { + typealias RootViewType = LocksViewLayout + typealias DataSource = + UICollectionViewDiffableDataSource + + let presenter: LocksPresenterProtocol + var collectionView: UICollectionView { + rootView.collectionView + } + + private lazy var dataSource = createDataSource() + private lazy var delegate = createDelegate() + private var viewModel: [LocksViewSectionModel] = [] + + init(presenter: LocksPresenterProtocol) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = LocksViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupCollectionView() + presenter.setup() + } + + private func setupCollectionView() { + rootView.collectionView.dataSource = dataSource + rootView.collectionView.delegate = delegate + + rootView.collectionView.registerCellClass(LockCollectionViewCell.self) + rootView.collectionView.registerClass( + LocksHeaderView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader + ) + rootView.showHeader = { _ in true } + } + + private func createDataSource() -> DataSource { + let dataSource = DataSource( + collectionView: rootView.collectionView, + cellProvider: { collectionView, indexPath, model -> UICollectionViewCell? in + let cell: LockCollectionViewCell? = collectionView.dequeueReusableCell(for: indexPath) + cell?.bind(viewModel: .init( + title: model.title, + amount: model.amount, + price: model.price + ) + ) + return cell + } + ) + + dataSource.supplementaryViewProvider = { [weak self] collectionView, _, indexPath -> UICollectionReusableView? in + guard let headerModel = self? + .dataSource + .snapshot() + .sectionIdentifiers[indexPath.section] + .header else { + return nil + } + + let header: LocksHeaderView? = collectionView.dequeueReusableSupplementaryView( + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + for: indexPath + ) + header?.bind(viewModel: + .init( + icon: headerModel.icon, + title: headerModel.title, + details: headerModel.details, + value: headerModel.value + ) + ) + + return header + } + + return dataSource + } + + private func createDelegate() -> UICollectionViewDelegate { + ModalSheetCollectionViewDelegate { [weak self] _ in + self?.presenter.didTapOnCell() + } + } +} + +extension LocksViewController: LocksViewProtocol { + func update(viewModel: [LocksViewSectionModel]) { + self.viewModel = viewModel + + dataSource.apply(viewModel) + } + + func updateHeader(title: String, value: String) { + rootView.titleLabel.text = title + rootView.valueLabel.text = value + } + + func calculateEstimatedHeight(sections: Int, items: Int) -> CGFloat { + rootView.contentHeight(sections: sections, items: items) + } +} diff --git a/novawallet/Modules/Locks/LocksViewFactory.swift b/novawallet/Modules/Locks/LocksViewFactory.swift new file mode 100644 index 0000000000..0cbb70b3cb --- /dev/null +++ b/novawallet/Modules/Locks/LocksViewFactory.swift @@ -0,0 +1,36 @@ +import Foundation +import SoraUI +import SoraFoundation + +struct LocksViewFactory { + static func createView(input: LocksViewInput) -> LocksViewProtocol? { + guard let currencyManager = CurrencyManager.shared else { + return nil + } + let wireframe = LocksWireframe() + let viewModelFactory = LocksBalanceViewModelFactory( + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager), + assetFormatterFactory: AssetBalanceFormatterFactory(), + currencyManager: currencyManager + ) + let presenter = LocksPresenter( + input: input, + wireframe: wireframe, + localizationManager: LocalizationManager.shared, + priceViewModelFactory: viewModelFactory + ) + + let view = LocksViewController(presenter: presenter) + + presenter.view = view + let maxHeight = ModalSheetPresentationConfiguration.maximumContentHeight + let preferredContentSize = min(presenter.contentHeight, maxHeight) + + view.preferredContentSize = .init( + width: 0, + height: preferredContentSize + ) + + return view + } +} diff --git a/novawallet/Modules/Locks/LocksViewInput.swift b/novawallet/Modules/Locks/LocksViewInput.swift new file mode 100644 index 0000000000..8154f2c3ea --- /dev/null +++ b/novawallet/Modules/Locks/LocksViewInput.swift @@ -0,0 +1,7 @@ +struct LocksViewInput { + let prices: [ChainAssetId: PriceData] + let balances: [AssetBalance] + let chains: [ChainModel.Id: ChainModel] + let locks: [AssetLock] + let crowdloans: [ChainModel.Id: [CrowdloanContributionData]] +} diff --git a/novawallet/Modules/Locks/LocksViewLayout.swift b/novawallet/Modules/Locks/LocksViewLayout.swift new file mode 100644 index 0000000000..292a82779b --- /dev/null +++ b/novawallet/Modules/Locks/LocksViewLayout.swift @@ -0,0 +1,22 @@ +import UIKit + +final class LocksViewLayout: GenericCollectionViewLayout> { + let titleLabel: UILabel = .create { + $0.font = .semiBoldBody + $0.textColor = R.color.colorWhite() + } + + let valueLabel: UILabel = .create { + $0.font = .regularSubheadline + $0.textColor = R.color.colorWhite() + } + + override init(frame _: CGRect = .zero) { + let settings = GenericCollectionViewLayoutSettings( + pinToVisibleBounds: false, + estimatedRowHeight: 44, + absoluteHeaderHeight: 48 + ) + super.init(header: .init(titleView: titleLabel, valueView: valueLabel), settings: settings) + } +} diff --git a/novawallet/Modules/Locks/LocksWireframe.swift b/novawallet/Modules/Locks/LocksWireframe.swift new file mode 100644 index 0000000000..08ca80463f --- /dev/null +++ b/novawallet/Modules/Locks/LocksWireframe.swift @@ -0,0 +1,7 @@ +import Foundation + +final class LocksWireframe: LocksWireframeProtocol { + func close(view: LocksViewProtocol?) { + view?.controller.presentingViewController?.dismiss(animated: true, completion: nil) + } +} diff --git a/novawallet/Modules/Locks/View/LockCollectionViewCell.swift b/novawallet/Modules/Locks/View/LockCollectionViewCell.swift new file mode 100644 index 0000000000..fd99959ae2 --- /dev/null +++ b/novawallet/Modules/Locks/View/LockCollectionViewCell.swift @@ -0,0 +1,50 @@ +import UIKit + +final class LockCollectionViewCell: UICollectionViewCell { + lazy var view = GenericTitleValueView( + titleView: titleLabel, + valueView: valueLabel + ) + private let titleLabel: UILabel = .create { + $0.font = .regularFootnote + $0.textColor = R.color.colorWhite64() + } + + private let valueLabel: MultiValueView = .create { + $0.valueTop.font = .regularFootnote + $0.valueBottom.font = .caption1 + $0.valueTop.textColor = R.color.colorWhite64() + $0.valueBottom.textColor = R.color.colorWhite64() + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + contentView.addSubview(view) + view.snp.makeConstraints { + $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 14, left: 24, bottom: 14, right: 0)) + } + } +} + +extension LockCollectionViewCell { + struct Model { + let title: String + let amount: String + let price: String? + } + + func bind(viewModel: Model) { + view.titleView.text = viewModel.title + view.valueView.bind(topValue: viewModel.amount, bottomValue: viewModel.price) + } +} diff --git a/novawallet/Modules/Locks/View/LocksHeaderView.swift b/novawallet/Modules/Locks/View/LocksHeaderView.swift new file mode 100644 index 0000000000..c78da190d6 --- /dev/null +++ b/novawallet/Modules/Locks/View/LocksHeaderView.swift @@ -0,0 +1,47 @@ +import UIKit + +final class LocksHeaderView: UICollectionReusableView { + private typealias TitleView = IconDetailsGenericView> + private typealias PercentView = GenericTitleValueView + + private let view = GenericTitleValueView() + + struct ViewModel { + let icon: UIImage? + let title: String + let details: String + let value: String + } + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(view) + view.snp.makeConstraints { + $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 14, left: 0, bottom: 14, right: 0)) + } + + setup() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup() { + view.titleView.imageView.contentMode = .center + view.titleView.imageView.tintColor = .white + view.titleView.detailsView.titleView.font = .regularSubheadline + view.titleView.detailsView.valueView.titleView.contentInsets = .init(top: 2, left: 8, bottom: 3, right: 8) + view.titleView.detailsView.valueView.titleView.titleLabel.textColor = R.color.colorWhite80() + view.valueView.font = .regularSubheadline + } + + func bind(viewModel: ViewModel) { + view.titleView.imageView.image = viewModel.icon?.withRenderingMode(.alwaysTemplate) + view.titleView.detailsView.titleView.text = viewModel.title + view.titleView.detailsView.valueView.titleView.titleLabel.text = viewModel.details + view.valueView.text = viewModel.value + } +} diff --git a/novawallet/Modules/Root/RootInteractor.swift b/novawallet/Modules/Root/RootInteractor.swift index 6dbde34ec8..46b9cfe093 100644 --- a/novawallet/Modules/Root/RootInteractor.swift +++ b/novawallet/Modules/Root/RootInteractor.swift @@ -9,7 +9,7 @@ final class RootInteractor { let settings: SelectedWalletSettings let keystore: KeystoreProtocol let applicationConfig: ApplicationConfigProtocol - let chainRegistry: ChainRegistryProtocol + let chainRegistryClosure: ChainRegistryLazyClosure let eventCenter: EventCenterProtocol let migrators: [Migrating] let logger: LoggerProtocol? @@ -18,7 +18,7 @@ final class RootInteractor { settings: SelectedWalletSettings, keystore: KeystoreProtocol, applicationConfig: ApplicationConfigProtocol, - chainRegistry: ChainRegistryProtocol, + chainRegistryClosure: @escaping ChainRegistryLazyClosure, eventCenter: EventCenterProtocol, migrators: [Migrating], logger: LoggerProtocol? = nil @@ -26,7 +26,7 @@ final class RootInteractor { self.settings = settings self.keystore = keystore self.applicationConfig = applicationConfig - self.chainRegistry = chainRegistry + self.chainRegistryClosure = chainRegistryClosure self.eventCenter = eventCenter self.migrators = migrators self.logger = logger @@ -103,6 +103,6 @@ extension RootInteractor: RootInteractorInputProtocol { } } - chainRegistry.syncUp() + chainRegistryClosure().syncUp() } } diff --git a/novawallet/Modules/Root/RootPresenterFactory.swift b/novawallet/Modules/Root/RootPresenterFactory.swift index 035375b6e7..429e075fe4 100644 --- a/novawallet/Modules/Root/RootPresenterFactory.swift +++ b/novawallet/Modules/Root/RootPresenterFactory.swift @@ -9,7 +9,7 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { let keychain = Keychain() let settings = SettingsManager.shared - let dbMigrator = UserStorageMigrator( + let userStorageMigrator = UserStorageMigrator( targetVersion: UserStorageParams.modelVersion, storeURL: UserStorageParams.storageURL, modelDirectory: UserStorageParams.modelDirectory, @@ -18,13 +18,20 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { fileManager: FileManager.default ) + let substrateStorageMigrator = SubstrateStorageMigrator( + storeURL: SubstrateStorageParams.storageURL, + modelDirectory: SubstrateStorageParams.modelDirectory, + model: SubstrateStorageParams.modelVersion, + fileManager: FileManager.default + ) + let interactor = RootInteractor( settings: SelectedWalletSettings.shared, keystore: keychain, applicationConfig: ApplicationConfig.shared, - chainRegistry: ChainRegistryFacade.sharedRegistry, + chainRegistryClosure: { ChainRegistryFacade.sharedRegistry }, eventCenter: EventCenter.shared, - migrators: [dbMigrator], + migrators: [userStorageMigrator, substrateStorageMigrator], logger: Logger.shared ) diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewController.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewController.swift index bbdc308ce0..1288c07ddb 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewController.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewController.swift @@ -2,7 +2,7 @@ import UIKit import CommonWallet import SoraFoundation -final class ParaStkYieldBoostSetupViewController: UIViewController, ViewHolder { +final class ParaStkYieldBoostSetupViewController: UIViewController, ViewHolder, ImportantViewProtocol { typealias RootViewType = ParaStkYieldBoostSetupViewLayout let presenter: ParaStkYieldBoostSetupPresenterProtocol diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewController.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewController.swift index 4c4117c545..dbfca6af7a 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewController.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewController.swift @@ -1,7 +1,7 @@ import UIKit import SoraFoundation -final class ParaStkYieldBoostStartViewController: UIViewController, ViewHolder { +final class ParaStkYieldBoostStartViewController: UIViewController, ViewHolder, ImportantViewProtocol { typealias RootViewType = ParaStkYieldBoostStartViewLayout let presenter: ParaStkYieldBoostStartPresenterProtocol diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewController.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewController.swift index b91f2a13da..3469d63462 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewController.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewController.swift @@ -1,7 +1,7 @@ import UIKit import SoraFoundation -final class ParaStkYieldBoostStopViewController: UIViewController, ViewHolder { +final class ParaStkYieldBoostStopViewController: UIViewController, ViewHolder, ImportantViewProtocol { typealias RootViewType = ParaStkYieldBoostStopViewLayout let presenter: ParaStkYieldBoostStopPresenterProtocol diff --git a/novawallet/Modules/Staking/Parachain/YieldBoost/ParaStkYieldBoostProviderFactory.swift b/novawallet/Modules/Staking/Parachain/YieldBoost/ParaStkYieldBoostProviderFactory.swift index a56677eda0..2c5e049dda 100644 --- a/novawallet/Modules/Staking/Parachain/YieldBoost/ParaStkYieldBoostProviderFactory.swift +++ b/novawallet/Modules/Staking/Parachain/YieldBoost/ParaStkYieldBoostProviderFactory.swift @@ -63,7 +63,7 @@ final class ParaStkYieldBoostProviderFactory: ParaStkYieldBoostProviderFactoryPr operationManager: OperationManager(operationQueue: operationQueue) ) - let wrapperTrigger: DataProviderEventTrigger = [.onInitialization] + let wrapperTrigger: DataProviderEventTrigger = [.onInitialization, .onAddObserver] let trigger = AccountAssetBalanceTrigger( chainAssetId: chainAssetId, eventCenter: eventCenter, diff --git a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift index ab334b167b..46abc2b770 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift @@ -227,9 +227,129 @@ class OnChainTransferInteractor: RuntimeConstantFetching { } } + func addingOrmlTransferCommand( + to builder: ExtrinsicBuilderProtocol, + amount: OnChainTransferAmount, + recepient: AccountId, + tokenStorageInfo: OrmlTokenStorageInfo + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + switch amount { + case let .concrete(value): + return try addingOrmlTransferValueCommand( + to: builder, + recepient: recepient, + tokenStorageInfo: tokenStorageInfo, + value: value + ) + case let .all(value): + if tokenStorageInfo.canTransferAll { + return try addingOrmlTransferAllCommand( + to: builder, + recepient: recepient, + tokenStorageInfo: tokenStorageInfo + ) + } else { + return try addingOrmlTransferValueCommand( + to: builder, + recepient: recepient, + tokenStorageInfo: tokenStorageInfo, + value: value + ) + } + } + } + + func addingOrmlTransferValueCommand( + to builder: ExtrinsicBuilderProtocol, + recepient: AccountId, + tokenStorageInfo: OrmlTokenStorageInfo, + value: BigUInt + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + let call = callFactory.ormlTransfer( + in: tokenStorageInfo.module, + currencyId: tokenStorageInfo.currencyId, + receiverId: recepient, + amount: value + ) + + let newBuilder = try builder.adding(call: call) + return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + } + + func addingOrmlTransferAllCommand( + to builder: ExtrinsicBuilderProtocol, + recepient: AccountId, + tokenStorageInfo: OrmlTokenStorageInfo + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + let call = callFactory.ormlTransferAll( + in: tokenStorageInfo.module, + currencyId: tokenStorageInfo.currencyId, + receiverId: recepient + ) + let newBuilder = try builder.adding(call: call) + return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + } + + func addingNativeTransferCommand( + to builder: ExtrinsicBuilderProtocol, + amount: OnChainTransferAmount, + recepient: AccountId, + canTransferAll: Bool + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + switch amount { + case let .concrete(value): + return try addingNativeTransferValueCommand( + to: builder, + recepient: recepient, + value: value + ) + case let .all(value): + if canTransferAll { + return try addingNativeTransferAllCommand(to: builder, recepient: recepient) + } else { + return try addingNativeTransferValueCommand(to: builder, recepient: recepient, value: value) + } + } + } + + func addingNativeTransferValueCommand( + to builder: ExtrinsicBuilderProtocol, + recepient: AccountId, + value: BigUInt + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + let call = callFactory.nativeTransfer(to: recepient, amount: value) + let newBuilder = try builder.adding(call: call) + return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + } + + func addingNativeTransferAllCommand( + to builder: ExtrinsicBuilderProtocol, + recepient: AccountId + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + let call = callFactory.nativeTransferAll(to: recepient) + let newBuilder = try builder.adding(call: call) + return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + } + + func addingAssetsTransferCommand( + to builder: ExtrinsicBuilderProtocol, + amount: OnChainTransferAmount, + recepient: AccountId, + extras: StatemineAssetExtras + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + let call = callFactory.assetsTransfer( + to: recepient, + extras: extras, + amount: amount.value + ) + + let newBuilder = try builder.adding(call: call) + return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + } + func addingTransferCommand( to builder: ExtrinsicBuilderProtocol, - amount: BigUInt, + amount: OnChainTransferAmount, recepient: AccountId ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { guard let sendingAssetInfo = sendingAssetInfo else { @@ -237,29 +357,27 @@ class OnChainTransferInteractor: RuntimeConstantFetching { } switch sendingAssetInfo { - case let .orml(currencyId, _, module, _): - let call = callFactory.ormlTransfer( - in: module, - currencyId: currencyId, - receiverId: recepient, - amount: amount + case let .orml(info): + return try addingOrmlTransferCommand( + to: builder, + amount: amount, + recepient: recepient, + tokenStorageInfo: info ) - - let newBuilder = try builder.adding(call: call) - return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) case let .statemine(extras): - let call = callFactory.assetsTransfer( - to: recepient, - extras: extras, - amount: amount + return try addingAssetsTransferCommand( + to: builder, + amount: amount, + recepient: recepient, + extras: extras + ) + case let .native(canTransferAll): + return try addingNativeTransferCommand( + to: builder, + amount: amount, + recepient: recepient, + canTransferAll: canTransferAll ) - - let newBuilder = try builder.adding(call: call) - return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) - case .native: - let call = callFactory.nativeTransfer(to: recepient, amount: amount) - let newBuilder = try builder.adding(call: call) - return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) } } @@ -393,10 +511,10 @@ extension OnChainTransferInteractor { operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) } - func estimateFee(for amount: BigUInt, recepient: AccountId?) { + func estimateFee(for amount: OnChainTransferAmount, recepient: AccountId?) { let recepientAccountId = recepient ?? AccountId.zeroAccountId(of: chain.accountIdSize) - let identifier = String(amount) + "-" + recepientAccountId.toHex() + let identifier = String(amount.value) + "-" + recepientAccountId.toHex() + "-" + amount.name feeProxy.estimateFee( using: extrinsicService, diff --git a/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift b/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift index 92de27799b..fbf2a3f9db 100644 --- a/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift +++ b/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift @@ -128,8 +128,8 @@ extension AssetStorageInfoOperationFactory: AssetStorageInfoOperationFactoryProt storage: storage, runtimeService: runtimeService ) - case let .orml(_, _, _, existentialDeposit): - let assetExistence = AssetBalanceExistence(minBalance: existentialDeposit, isSelfSufficient: true) + case let .orml(info): + let assetExistence = AssetBalanceExistence(minBalance: info.existentialDeposit, isSelfSufficient: true) return CompoundOperationWrapper.createWithResult(assetExistence) } } diff --git a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmInteractor.swift b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmInteractor.swift index 383c44f979..0250eee15e 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmInteractor.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmInteractor.swift @@ -68,7 +68,7 @@ final class TransferOnChainConfirmInteractor: OnChainTransferInteractor { } extension TransferOnChainConfirmInteractor: TransferConfirmOnChainInteractorInputProtocol { - func submit(amount: BigUInt, recepient: AccountAddress, lastFee: BigUInt?) { + func submit(amount: OnChainTransferAmount, recepient: AccountAddress, lastFee: BigUInt?) { do { let accountId = try recepient.toAccountId(using: chain.chainFormat) @@ -105,7 +105,7 @@ extension TransferOnChainConfirmInteractor: TransferConfirmOnChainInteractorInpu let details = PersistTransferDetails( sender: sender, receiver: recepient, - amount: amount, + amount: amount.value, txHash: txHashData, callPath: callCodingPath, fee: lastFee diff --git a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift index 78bbabe027..f265bad7c9 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift @@ -12,7 +12,7 @@ final class TransferOnChainConfirmPresenter: OnChainTransferPresenter { let recepientAccountAddress: AccountAddress let wallet: MetaAccountModel - let amount: Decimal + let amount: OnChainTransferAmount private lazy var walletIconGenerator = NovaIconGenerator() @@ -21,7 +21,7 @@ final class TransferOnChainConfirmPresenter: OnChainTransferPresenter { wireframe: TransferConfirmWireframeProtocol, wallet: MetaAccountModel, recepient: AccountAddress, - amount: Decimal, + amount: OnChainTransferAmount, displayAddressViewModelFactory: DisplayAddressViewModelFactoryProtocol, chainAsset: ChainAsset, networkViewModelFactory: NetworkViewModelFactoryProtocol, @@ -104,7 +104,7 @@ final class TransferOnChainConfirmPresenter: OnChainTransferPresenter { private func provideAmountViewModel() { let viewModel = sendingBalanceViewModelFactory.spendingAmountFromPrice( - amount, + amount.value, priceData: sendingAssetPrice ).value(for: selectedLocale) @@ -129,11 +129,11 @@ final class TransferOnChainConfirmPresenter: OnChainTransferPresenter { override func refreshFee() { let assetInfo = chainAsset.assetDisplayInfo - guard let amountValue = amount.toSubstrateAmount(precision: assetInfo.assetPrecision) else { + guard let amountInPlank = amount.flatMap({ $0.toSubstrateAmount(precision: assetInfo.assetPrecision) }) else { return } - interactor.estimateFee(for: amountValue, recepient: getRecepientAccountId()) + interactor.estimateFee(for: amountInPlank, recepient: getRecepientAccountId()) } override func askFeeRetry() { @@ -202,7 +202,7 @@ extension TransferOnChainConfirmPresenter: TransferConfirmPresenterProtocol { func submit() { let assetPrecision = chainAsset.assetDisplayInfo.assetPrecision guard - let amountValue = amount.toSubstrateAmount(precision: assetPrecision), + let amountInPlank = amount.flatMap({ $0.toSubstrateAmount(precision: assetPrecision) }), let utilityAsset = chainAsset.chain.utilityAsset() else { return } @@ -210,7 +210,7 @@ extension TransferOnChainConfirmPresenter: TransferConfirmPresenterProtocol { let utilityAssetInfo = ChainAsset(chain: chainAsset.chain, asset: utilityAsset).assetDisplayInfo let validators: [DataValidating] = baseValidators( - for: amount, + for: amount.value, recepientAddress: recepientAccountAddress, utilityAssetInfo: utilityAssetInfo, selectedLocale: selectedLocale @@ -224,7 +224,7 @@ extension TransferOnChainConfirmPresenter: TransferConfirmPresenterProtocol { strongSelf.view?.didStartLoading() strongSelf.interactor.submit( - amount: amountValue, + amount: amountInPlank, recepient: strongSelf.recepientAccountAddress, lastFee: strongSelf.fee ) diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift index b21a80fba7..b8e08b1e99 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift @@ -7,7 +7,7 @@ struct TransferConfirmOnChainViewFactory { static func createView( chainAsset: ChainAsset, recepient: AccountAddress, - amount: Decimal + amount: OnChainTransferAmount ) -> TransferConfirmOnChainViewProtocol? { let walletSettings = SelectedWalletSettings.shared diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmProtocols.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmProtocols.swift index 7de0eb4d17..8bb0710f11 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmProtocols.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmProtocols.swift @@ -24,7 +24,7 @@ protocol TransferConfirmPresenterProtocol: AnyObject { } protocol TransferConfirmOnChainInteractorInputProtocol: OnChainTransferSetupInteractorInputProtocol { - func submit(amount: BigUInt, recepient: AccountAddress, lastFee: BigUInt?) + func submit(amount: OnChainTransferAmount, recepient: AccountAddress, lastFee: BigUInt?) } protocol TransferConfirmCrossChainInteractorInputProtocol: CrossChainTransferSetupInteractorInputProtocol { diff --git a/novawallet/Modules/Transfer/TransferSetup/Model/OnChainTransferAmount.swift b/novawallet/Modules/Transfer/TransferSetup/Model/OnChainTransferAmount.swift new file mode 100644 index 0000000000..76c7bfbe04 --- /dev/null +++ b/novawallet/Modules/Transfer/TransferSetup/Model/OnChainTransferAmount.swift @@ -0,0 +1,46 @@ +import Foundation + +enum OnChainTransferAmount { + case concrete(value: T) + case all(value: T) + + var value: T { + switch self { + case let .concrete(value): + return value + case let .all(value): + return value + } + } + + var name: String { + switch self { + case .concrete: + return "concrete" + case .all: + return "all" + } + } + + func flatMap(_ closure: (T) -> V?) -> OnChainTransferAmount? { + guard let newValue = closure(value) else { + return nil + } + + switch self { + case .concrete: + return .concrete(value: newValue) + case .all: + return .all(value: newValue) + } + } + + func map(_ closure: (T) -> V) -> OnChainTransferAmount { + switch self { + case let .concrete(value): + return .concrete(value: closure(value)) + case let .all(value): + return .all(value: closure(value)) + } + } +} diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift index 6c41f9160f..53c0ae67c0 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift @@ -199,12 +199,20 @@ final class OnChainTransferSetupPresenter: OnChainTransferPresenter, OnChainTran let inputAmount = inputResult?.absoluteValue(from: balanceMinusFee()) ?? 0 let assetInfo = chainAsset.assetDisplayInfo - guard let amount = inputAmount.toSubstrateAmount( + guard let amountValue = inputAmount.toSubstrateAmount( precision: assetInfo.assetPrecision ) else { return } + let amount: OnChainTransferAmount + + if let inputResult = inputResult, inputResult.isMax { + amount = .all(value: amountValue) + } else { + amount = .concrete(value: amountValue) + } + updateFee(nil) updateFeeView() @@ -339,7 +347,7 @@ extension OnChainTransferSetupPresenter: TransferSetupChildPresenterProtocol { DataValidationRunner(validators: validators).runValidation { [weak self] in guard - let amount = sendingAmount, + let amountValue = sendingAmount, let recepient = self?.partialRecepientAddress, let chainAsset = self?.chainAsset else { return @@ -347,6 +355,14 @@ extension OnChainTransferSetupPresenter: TransferSetupChildPresenterProtocol { self?.logger?.debug("Did complete validation") + let amount: OnChainTransferAmount + + if let inputResult = self?.inputResult, inputResult.isMax { + amount = .all(value: amountValue) + } else { + amount = .concrete(value: amountValue) + } + self?.wireframe.showConfirmation( from: self?.view, chainAsset: chainAsset, diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift index cde52c5109..5f1c8647d5 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift @@ -1,8 +1,9 @@ import BigInt +import Foundation protocol OnChainTransferSetupInteractorInputProtocol: AnyObject { func setup() - func estimateFee(for amount: BigUInt, recepient: AccountId?) + func estimateFee(for amount: OnChainTransferAmount, recepient: AccountId?) func change(recepient: AccountId?) } @@ -25,7 +26,7 @@ protocol OnChainTransferSetupWireframeProtocol: AlertPresentable, ErrorPresentab func showConfirmation( from view: TransferSetupChildViewProtocol?, chainAsset: ChainAsset, - sendingAmount: Decimal, + sendingAmount: OnChainTransferAmount, recepient: AccountAddress ) } diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift index 56b5424a96..c63759b863 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift @@ -7,7 +7,7 @@ final class OnChainTransferSetupWireframe: OnChainTransferSetupWireframeProtocol func showConfirmation( from _: TransferSetupChildViewProtocol?, chainAsset: ChainAsset, - sendingAmount: Decimal, + sendingAmount: OnChainTransferAmount, recepient: AccountAddress ) { guard let confirmView = TransferConfirmOnChainViewFactory.createView( diff --git a/novawallet/Modules/Wallet/AccountDetails/View/AssetDetailsContainingViewFactory.swift b/novawallet/Modules/Wallet/AccountDetails/View/AssetDetailsContainingViewFactory.swift index cb7182811a..5517393da3 100644 --- a/novawallet/Modules/Wallet/AccountDetails/View/AssetDetailsContainingViewFactory.swift +++ b/novawallet/Modules/Wallet/AccountDetails/View/AssetDetailsContainingViewFactory.swift @@ -9,7 +9,7 @@ class AssetDetailsContainingViewFactory: AccountDetailsContainingViewFactoryProt let selectedAccountId: AccountId let selectedAccountType: MetaAccountModelType - var commandFactory: WalletCommandFactoryProtocol? + weak var commandFactory: WalletCommandFactoryProtocol? init( chainAsset: ChainAsset, diff --git a/novawallet/Modules/Wallet/Model/BalanceContext.swift b/novawallet/Modules/Wallet/Model/BalanceContext.swift index 64df7660bb..a7f4ff6494 100644 --- a/novawallet/Modules/Wallet/Model/BalanceContext.swift +++ b/novawallet/Modules/Wallet/Model/BalanceContext.swift @@ -8,19 +8,21 @@ struct BalanceContext { static let priceChangeKey = "account.balance.price.change.key" static let priceIdKey = "account.balance.price.id.key" static let balanceLocksKey = "account.balance.locks.key" + static let crowdloans = "account.balance.crowdloan.key" let free: Decimal let reserved: Decimal let frozen: Decimal + let crowdloans: Decimal let price: Decimal let priceChange: Decimal let priceId: Int? - let balanceLocks: BalanceLocks + let balanceLocks: [AssetLock] } extension BalanceContext { - var total: Decimal { free + reserved } - var locked: Decimal { reserved + frozen } + var total: Decimal { free + reserved + crowdloans } + var locked: Decimal { reserved + frozen + crowdloans } var available: Decimal { free >= frozen ? free - frozen : 0.0 } } @@ -34,6 +36,7 @@ extension BalanceContext { priceChange = Self.parseContext(key: BalanceContext.priceChangeKey, context: context) priceId = context[BalanceContext.priceIdKey].flatMap { Int($0) } + crowdloans = Self.parseContext(key: BalanceContext.crowdloans, context: context) balanceLocks = Self.parseJSONContext(key: BalanceContext.balanceLocksKey, context: context) } @@ -50,6 +53,7 @@ extension BalanceContext { BalanceContext.freeKey: free.stringWithPointSeparator, BalanceContext.reservedKey: reserved.stringWithPointSeparator, BalanceContext.frozen: frozen.stringWithPointSeparator, + BalanceContext.crowdloans: crowdloans.stringWithPointSeparator, BalanceContext.priceKey: price.stringWithPointSeparator, BalanceContext.priceChangeKey: priceChange.stringWithPointSeparator, BalanceContext.balanceLocksKey: locksStringRepresentation @@ -70,7 +74,7 @@ extension BalanceContext { } } - private static func parseJSONContext(key: String, context: [String: String]) -> [BalanceLock] { + private static func parseJSONContext(key: String, context: [String: String]) -> [AssetLock] { guard let locksStringRepresentation = context[key] else { return [] } guard let JSONData = locksStringRepresentation.data(using: .utf8) else { @@ -78,7 +82,7 @@ extension BalanceContext { } let balanceLocks = try? JSONDecoder().decode( - BalanceLocks.self, + [AssetLock].self, from: JSONData ) @@ -101,6 +105,7 @@ extension BalanceContext { free: free, reserved: reserved, frozen: max(miscFrozen, feeFrozen), + crowdloans: crowdloans, price: price, priceChange: priceChange, priceId: priceId, @@ -120,6 +125,7 @@ extension BalanceContext { free: free, reserved: reserved, frozen: frozen, + crowdloans: crowdloans, price: price, priceChange: priceChange, priceId: priceId, @@ -128,12 +134,13 @@ extension BalanceContext { } func byChangingBalanceLocks( - _ updatedLocks: BalanceLocks + _ updatedLocks: [AssetLock] ) -> BalanceContext { BalanceContext( free: free, reserved: reserved, frozen: frozen, + crowdloans: crowdloans, price: price, priceChange: priceChange, priceId: priceId, @@ -146,10 +153,24 @@ extension BalanceContext { free: free, reserved: reserved, frozen: frozen, + crowdloans: crowdloans, price: newPrice, priceChange: newPriceChange, priceId: newPriceId, balanceLocks: balanceLocks ) } + + func byChangingCrowdloans(_ newCrowdloans: Decimal) -> BalanceContext { + BalanceContext( + free: free, + reserved: reserved, + frozen: frozen, + crowdloans: newCrowdloans, + price: price, + priceChange: priceChange, + priceId: priceId, + balanceLocks: balanceLocks + ) + } } diff --git a/novawallet/Modules/Wallet/WalletDetailsUpdater.swift b/novawallet/Modules/Wallet/WalletDetailsUpdater.swift index a44985c7a6..bc58197d14 100644 --- a/novawallet/Modules/Wallet/WalletDetailsUpdater.swift +++ b/novawallet/Modules/Wallet/WalletDetailsUpdater.swift @@ -1,27 +1,128 @@ import Foundation import CommonWallet +import RobinHood protocol WalletDetailsUpdating: AnyObject { - var context: CommonWalletContextProtocol? { get set } + var context: CommonWalletContextProtocol? { get } + + func setup(context: CommonWalletContextProtocol, chainAsset: ChainAsset) } -final class WalletDetailsUpdater: WalletDetailsUpdating, EventVisitorProtocol { - static let shared = WalletDetailsUpdater(eventCenter: EventCenter.shared) +/** + * Class is responsible for monitoring balance or transaction changes + * and ask CommonWallet to update itself. + * + * Note: Currently there is no way to know whether CommonWallet was closed. + * So, before processing the event we should manually check whether context + * exists and clear observers otherwise. + */ +final class WalletDetailsUpdater: WalletDetailsUpdating, EventVisitorProtocol { weak var context: CommonWalletContextProtocol? + let eventCenter: EventCenterProtocol + let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol + let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let walletSettings: SelectedWalletSettings - init(eventCenter: EventCenterProtocol) { + private var crowdloanContributionsDataProvider: StreamableProvider? + private var assetsLockDataProvider: StreamableProvider? + private var balanceDataProvider: StreamableProvider? + + init( + eventCenter: EventCenterProtocol, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + walletSettings: SelectedWalletSettings + ) { self.eventCenter = eventCenter + self.crowdloansLocalSubscriptionFactory = crowdloansLocalSubscriptionFactory + self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.walletSettings = walletSettings eventCenter.add(observer: self, dispatchIn: .main) } - func processBalanceChanged(event _: WalletBalanceChanged) { - try? context?.prepareAccountUpdateCommand().execute() + func setup(context: CommonWalletContextProtocol, chainAsset: ChainAsset) { + clearProviders() + + self.context = context + + if let wallet = walletSettings.value { + subscribe(for: wallet, chainAsset: chainAsset) + } } func processNewTransaction(event _: WalletNewTransactionInserted) { try? context?.prepareAccountUpdateCommand().execute() } + + private func subscribe(for wallet: MetaAccountModel, chainAsset: ChainAsset) { + guard let accountId = wallet.fetch(for: chainAsset.chain.accountRequest())?.accountId else { + return + } + + balanceDataProvider = subscribeToAssetBalanceProvider( + for: accountId, + chainId: chainAsset.chain.chainId, + assetId: chainAsset.asset.assetId + ) + + assetsLockDataProvider = subscribeToLocksProvider( + for: accountId, + chainId: chainAsset.chain.chainId, + assetId: chainAsset.asset.assetId + ) + + crowdloanContributionsDataProvider = subscribeToCrowdloansProvider(for: accountId, chain: chainAsset.chain) + } + + private func updateAccount() { + try? context?.prepareAccountUpdateCommand().execute() + } + + private func clearProvidersIfNeeded() { + if context == nil { + clearProviders() + } + } + + private func clearProviders() { + balanceDataProvider = nil + crowdloanContributionsDataProvider = nil + assetsLockDataProvider = nil + } +} + +extension WalletDetailsUpdater: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAssetBalance( + result _: Result, + accountId _: AccountId, + chainId _: ChainModel.Id, + assetId _: AssetModel.Id + ) { + clearProvidersIfNeeded() + updateAccount() + } + + func handleAccountLocks( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId, + chainId _: ChainModel.Id, + assetId _: AssetModel.Id + ) { + clearProvidersIfNeeded() + updateAccount() + } +} + +extension WalletDetailsUpdater: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { + func handleCrowdloans( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId, + chain _: ChainModel + ) { + clearProvidersIfNeeded() + updateAccount() + } } diff --git a/novawallet/Modules/WalletsList/Common/CrowdloanContributionId.swift b/novawallet/Modules/WalletsList/Common/CrowdloanContributionId.swift new file mode 100644 index 0000000000..c3003357ad --- /dev/null +++ b/novawallet/Modules/WalletsList/Common/CrowdloanContributionId.swift @@ -0,0 +1,7 @@ +import BigInt + +struct CrowdloanContributionId { + let chainId: ChainModel.Id + let accountId: AccountId + let amount: BigUInt +} diff --git a/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift b/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift index 2bc0125169..04ce057717 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift @@ -8,10 +8,12 @@ class WalletsListInteractor { let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let walletListLocalSubscriptionFactory: WalletListLocalSubscriptionFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol private(set) var priceSubscription: AnySingleValueProvider<[PriceData]>? private(set) var assetsSubscription: StreamableProvider? private(set) var walletsSubscription: StreamableProvider? + private(set) var crowdloansSubscription: StreamableProvider? private(set) var availableTokenPrice: [ChainAssetId: AssetModel.PriceId] = [:] init( @@ -19,12 +21,14 @@ class WalletsListInteractor { walletListLocalSubscriptionFactory: WalletListLocalSubscriptionFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, - currencyManager: CurrencyManagerProtocol + currencyManager: CurrencyManagerProtocol, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol ) { self.chainRegistry = chainRegistry self.walletListLocalSubscriptionFactory = walletListLocalSubscriptionFactory self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.crowdloansLocalSubscriptionFactory = crowdloansLocalSubscriptionFactory self.currencyManager = currencyManager } @@ -36,6 +40,10 @@ class WalletsListInteractor { assetsSubscription = subscribeAllBalancesProvider() } + private func subscribeToCrowdloans() { + crowdloansSubscription = subscribeToAllCrowdloansProvider() + } + private func subscribeChains() { chainRegistry.chainsSubscribe(self, runningInQueue: .main) { [weak self] changes in self?.basePresenter?.didReceiveChainChanges(changes) @@ -132,6 +140,7 @@ extension WalletsListInteractor: WalletsListInteractorInputProtocol { subscribeChains() subscribeAssets() subscribeWallets() + subscribeToCrowdloans() } } @@ -166,3 +175,14 @@ extension WalletsListInteractor: SelectedCurrencyDepending { updatePriceProvider(for: Set(availableTokenPrice.values), currency: selectedCurrency) } } + +extension WalletsListInteractor: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { + func handleAllCrowdloans(result: Result<[DataProviderChange], Error>) { + switch result { + case let .success(changes): + basePresenter?.didReceiveCrowdloanContributionChanges(changes) + case let .failure(error): + basePresenter?.didReceiveError(error) + } + } +} diff --git a/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift b/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift index bd02eeb879..f7719b8282 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift @@ -24,6 +24,8 @@ class WalletsListPresenter { private var identifierMapping: [String: AssetBalanceId] = [:] private var balances: [AccountId: [ChainAssetId: BigUInt]] = [:] + private var crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]] = [:] + private var crowdloanContributionsMapping: [String: CrowdloanContributionId] = [:] private var prices: [ChainAssetId: PriceData] = [:] private var chains: [ChainModel.Id: ChainModel] = [:] @@ -51,6 +53,7 @@ class WalletsListPresenter { for: walletsList.allItems, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: selectedLocale ) @@ -127,6 +130,40 @@ extension WalletsListPresenter: WalletsListInteractorOutputProtocol { updateViewModels() } + func didReceiveCrowdloanContributionChanges(_ changes: [DataProviderChange]) { + for change in changes { + switch change { + case let .insert(item), let .update(item): + let previousAmount = crowdloanContributionsMapping[item.identifier]?.amount ?? 0 + var accountCrowdloan = crowdloanContributions[item.accountId] ?? [:] + let value: BigUInt = accountCrowdloan[item.chainId] ?? 0 + accountCrowdloan[item.chainId] = value - previousAmount + item.amount + crowdloanContributions[item.accountId] = accountCrowdloan + crowdloanContributionsMapping[item.identifier] = CrowdloanContributionId( + chainId: item.chainId, + accountId: item.accountId, + amount: item.amount + ) + case let .delete(deletedIdentifier): + if let accountContributionId = crowdloanContributionsMapping[deletedIdentifier] { + var accountContributions = crowdloanContributions[accountContributionId.accountId] + if let contribution = accountContributions?[accountContributionId.chainId], + contribution > accountContributionId.amount { + let newAmount = contribution - accountContributionId.amount + accountContributions?[accountContributionId.chainId] = newAmount + } else { + accountContributions?[accountContributionId.chainId] = nil + } + crowdloanContributions[accountContributionId.accountId] = accountContributions + } + + crowdloanContributionsMapping[deletedIdentifier] = nil + } + } + + updateViewModels() + } + func didReceiveError(_ error: Error) { logger.error("Did receive error: \(error)") diff --git a/novawallet/Modules/WalletsList/Common/WalletsListProtocols.swift b/novawallet/Modules/WalletsList/Common/WalletsListProtocols.swift index 5cc28a4a8a..d53d5b1d15 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListProtocols.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListProtocols.swift @@ -23,6 +23,7 @@ protocol WalletsListInteractorOutputProtocol: AnyObject { func didReceiveBalancesChanges(_ changes: [DataProviderChange]) func didReceiveChainChanges(_ changes: [DataProviderChange]) func didReceivePrices(_ prices: [ChainAssetId: PriceData]) + func didReceiveCrowdloanContributionChanges(_ changes: [DataProviderChange]) func didReceiveError(_ error: Error) } diff --git a/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift b/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift index 182664fed5..ca7aac71f6 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift @@ -8,6 +8,7 @@ protocol WalletsListViewModelFactoryProtocol { for wallets: [ManagedMetaAccountModel], chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData], locale: Locale ) -> [WalletsListSectionViewModel] @@ -16,6 +17,7 @@ protocol WalletsListViewModelFactoryProtocol { for wallet: ManagedMetaAccountModel, chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData], locale: Locale ) -> WalletsListViewModel @@ -69,6 +71,7 @@ final class WalletsListViewModelFactory { for wallet: ManagedMetaAccountModel, chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData] ) -> Decimal { let chainAccountIds = wallet.info.chainAccounts.map(\.chainId) @@ -83,6 +86,13 @@ final class WalletsListViewModelFactory { includingChainIds: Set(), excludingChainIds: Set(chainAccountIds) ) + + let contributions = crowdloanContributions[substrateAccountId]?.filter { !chainAccountIds.contains($0.key) } ?? [:] + totalValue += calculateCrowdloanContribution( + contributions, + chains: chains, + prices: prices + ) } if let ethereumAddress = wallet.info.ethereumAddress { @@ -93,6 +103,13 @@ final class WalletsListViewModelFactory { includingChainIds: Set(), excludingChainIds: Set(chainAccountIds) ) + + let contributions = crowdloanContributions[ethereumAddress]?.filter { !chainAccountIds.contains($0.key) } ?? [:] + totalValue += calculateCrowdloanContribution( + contributions, + chains: chains, + prices: prices + ) } wallet.info.chainAccounts.forEach { chainAccount in @@ -103,6 +120,12 @@ final class WalletsListViewModelFactory { includingChainIds: [chainAccount.chainId], excludingChainIds: Set() ) + let contributions = crowdloanContributions[chainAccount.accountId] ?? [:] + totalValue += calculateCrowdloanContribution( + contributions, + chains: chains, + prices: prices + ) } return totalValue @@ -113,6 +136,7 @@ final class WalletsListViewModelFactory { wallets: [ManagedMetaAccountModel], chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData], locale: Locale ) -> WalletsListSectionViewModel? { @@ -125,6 +149,7 @@ final class WalletsListViewModelFactory { for: $0, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: locale ) @@ -136,6 +161,28 @@ final class WalletsListViewModelFactory { return nil } } + + private func calculateCrowdloanContribution( + _ contributions: [ChainModel.Id: BigUInt], + chains: [ChainModel.Id: ChainModel], + prices: [ChainAssetId: PriceData] + ) -> Decimal { + contributions.reduce(0) { result, contribution in + guard let asset = chains[contribution.key]?.utilityAsset(), + let priceData = prices[ChainAssetId(chainId: contribution.key, assetId: asset.assetId)], + let price = Decimal(string: priceData.price) else { + return result + } + guard let decimalAmount = Decimal.fromSubstrateAmount( + contribution.value, + precision: Int16(bitPattern: asset.precision) + ) else { + return result + } + + return result + decimalAmount * price + } + } } extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { @@ -143,6 +190,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { for wallet: ManagedMetaAccountModel, chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData], locale: Locale ) -> WalletsListViewModel { @@ -150,6 +198,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { for: wallet, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices ) @@ -176,6 +225,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { for wallets: [ManagedMetaAccountModel], chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData], locale: Locale ) -> [WalletsListSectionViewModel] { @@ -187,6 +237,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { wallets: wallets, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: locale ) { @@ -199,6 +250,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { wallets: wallets, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: locale ) { @@ -211,6 +263,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { wallets: wallets, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: locale ) { @@ -223,6 +276,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { wallets: wallets, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: locale ) { diff --git a/novawallet/Modules/WalletsList/Manage/WalletManageInteractor.swift b/novawallet/Modules/WalletsList/Manage/WalletManageInteractor.swift index 8620e68255..a61fb57bcc 100644 --- a/novawallet/Modules/WalletsList/Manage/WalletManageInteractor.swift +++ b/novawallet/Modules/WalletsList/Manage/WalletManageInteractor.swift @@ -26,6 +26,7 @@ final class WalletManageInteractor: WalletsListInteractor { selectedWalletSettings: SelectedWalletSettings, eventCenter: EventCenterProtocol, currencyManager: CurrencyManagerProtocol, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol, operationQueue: OperationQueue ) { self.repository = repository @@ -38,7 +39,8 @@ final class WalletManageInteractor: WalletsListInteractor { walletListLocalSubscriptionFactory: walletListLocalSubscriptionFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, - currencyManager: currencyManager + currencyManager: currencyManager, + crowdloansLocalSubscriptionFactory: crowdloansLocalSubscriptionFactory ) } diff --git a/novawallet/Modules/WalletsList/Manage/WalletManageViewFactory.swift b/novawallet/Modules/WalletsList/Manage/WalletManageViewFactory.swift index 1d64b497f7..951ac05398 100644 --- a/novawallet/Modules/WalletsList/Manage/WalletManageViewFactory.swift +++ b/novawallet/Modules/WalletsList/Manage/WalletManageViewFactory.swift @@ -61,6 +61,7 @@ final class WalletManageViewFactory { selectedWalletSettings: SelectedWalletSettings.shared, eventCenter: EventCenter.shared, currencyManager: currencyManager, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactory.shared, operationQueue: OperationManagerFacade.sharedDefaultQueue ) } diff --git a/novawallet/Modules/WalletsList/Selection/WalletSelectionInteractor.swift b/novawallet/Modules/WalletsList/Selection/WalletSelectionInteractor.swift index 350e84f1da..b3d63f8fe9 100644 --- a/novawallet/Modules/WalletsList/Selection/WalletSelectionInteractor.swift +++ b/novawallet/Modules/WalletsList/Selection/WalletSelectionInteractor.swift @@ -31,7 +31,8 @@ final class WalletSelectionInteractor: WalletsListInteractor { walletListLocalSubscriptionFactory: walletListLocalSubscriptionFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, - currencyManager: currencyManager + currencyManager: currencyManager, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactory.shared ) } } diff --git a/novawallet/Modules/YourWallets/CollectionViewDelegate.swift b/novawallet/Modules/YourWallets/CollectionViewDelegate.swift new file mode 100644 index 0000000000..63cc745c42 --- /dev/null +++ b/novawallet/Modules/YourWallets/CollectionViewDelegate.swift @@ -0,0 +1,15 @@ +import UIKit +import SoraUI + +class CollectionViewDelegate: NSObject, UICollectionViewDelegate { + private let selectItemClosure: ((IndexPath) -> Void)? + + init(selectItemClosure: ((IndexPath) -> Void)? = nil) { + self.selectItemClosure = selectItemClosure + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + collectionView.deselectItem(at: indexPath, animated: true) + selectItemClosure?(indexPath) + } +} diff --git a/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift b/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift new file mode 100644 index 0000000000..6e24c6852f --- /dev/null +++ b/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift @@ -0,0 +1,123 @@ +import UIKit + +class GenericCollectionViewLayout: UIView { + var header: THeaderView = .init() + + lazy var collectionView: UICollectionView = { + let view = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) + view.backgroundColor = .clear + view.contentInsetAdjustmentBehavior = .always + view.contentInset = settings.collectionViewContentInset + return view + }() + + var showHeader: (Int) -> Bool = { _ in false } + + private var settings = GenericCollectionViewLayoutSettings() + private lazy var compositionalLayout: UICollectionViewCompositionalLayout = { + .init { [weak self] sectionIndex, _ -> NSCollectionLayoutSection? in + let showHeader = self?.showHeader(sectionIndex) ?? false + return self?.createCompositionalLayout(showHeader: showHeader) + } + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = R.color.color0x1D1D20() + setupLayout() + } + + init(header: THeaderView, settings: GenericCollectionViewLayoutSettings = .init()) { + super.init(frame: .zero) + + self.header = header + self.settings = settings + + backgroundColor = R.color.color0x1D1D20() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + addSubview(header) + addSubview(collectionView) + + header.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide.snp.top).offset(settings.headerContentInsets.top) + $0.leading.trailing.equalToSuperview().inset(settings.horizontalInset) + } + + header.setContentHuggingPriority(.defaultLow, for: .vertical) + + collectionView.snp.makeConstraints { + $0.top.equalTo(header.snp.bottom).offset(settings.headerContentInsets.bottom) + $0.leading.trailing.bottom.equalToSuperview() + } + } + + private func createCompositionalLayout(showHeader: Bool) -> NSCollectionLayoutSection { + .createSectionLayoutWithFullWidthRow(settings: + .init( + estimatedRowHeight: settings.estimatedRowHeight, + absoluteHeaderHeight: settings.absoluteHeaderHeight, + estimatedHeaderHeight: settings.estimatedSectionHeaderHeight, + sectionContentInsets: settings.sectionContentInsets, + sectionInterGroupSpacing: settings.interGroupSpacing, + header: showHeader ? .init(pinToVisibleBounds: settings.pinToVisibleBounds) : nil + )) + } +} + +// MARK: - Settings + +struct GenericCollectionViewLayoutSettings { + var horizontalInset: CGFloat = UIConstants.horizontalInset + var pinToVisibleBounds: Bool = true + var estimatedHeaderHeight: CGFloat = 36 + var estimatedRowHeight: CGFloat = 56 + var absoluteHeaderHeight: CGFloat? + var estimatedSectionHeaderHeight: CGFloat = 46 + var sectionContentInsets = NSDirectionalEdgeInsets( + top: 0, + leading: 16, + bottom: 0, + trailing: 16 + ) + var interGroupSpacing: CGFloat = 0 + var collectionViewContentInset = UIEdgeInsets( + top: 0, + left: 0, + bottom: 0, + right: 0 + ) + var headerContentInsets = UIEdgeInsets( + top: 3, + left: 0, + bottom: 12, + right: 0 + ) +} + +// MARK: - ContentHeight + +extension GenericCollectionViewLayout { + func contentHeight(sections: Int, items: Int) -> CGFloat { + let itemHeight = settings.estimatedRowHeight + + let sectionsHeight = settings.estimatedSectionHeaderHeight + + settings.sectionContentInsets.top + + settings.sectionContentInsets.bottom + + let estimatedListHeight = settings.collectionViewContentInset.top + + CGFloat(items) * itemHeight + + CGFloat(sections) * sectionsHeight + + settings.collectionViewContentInset.bottom + + return settings.estimatedHeaderHeight + estimatedListHeight + } +} diff --git a/novawallet/Modules/YourWallets/ModalSheetCollectionViewProtocol.swift b/novawallet/Modules/YourWallets/ModalSheetCollectionViewProtocol.swift new file mode 100644 index 0000000000..7f37adc78e --- /dev/null +++ b/novawallet/Modules/YourWallets/ModalSheetCollectionViewProtocol.swift @@ -0,0 +1,19 @@ +import SoraUI +import UIKit + +protocol ModalSheetCollectionViewProtocol: ModalSheetPresenterDelegate { + var collectionView: UICollectionView { get } +} + +extension ModalSheetCollectionViewProtocol { + func presenterCanDrag(_: ModalPresenterProtocol) -> Bool { + let offset = collectionView.contentOffset.y + collectionView.contentInset.top + return offset == 0 + } +} + +final class ModalSheetCollectionViewDelegate: CollectionViewDelegate { + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + scrollView.bounces = scrollView.contentOffset.y > UIConstants.bouncesOffset + } +} diff --git a/novawallet/Modules/YourWallets/YourWalletsViewController.swift b/novawallet/Modules/YourWallets/YourWalletsViewController.swift index c9fe40e8de..84acfa9052 100644 --- a/novawallet/Modules/YourWallets/YourWalletsViewController.swift +++ b/novawallet/Modules/YourWallets/YourWalletsViewController.swift @@ -3,13 +3,18 @@ import SoraFoundation import SoraUI import SubstrateSdk -final class YourWalletsViewController: UIViewController, ViewHolder { +final class YourWalletsViewController: UIViewController, ViewHolder, ModalSheetCollectionViewProtocol { + var collectionView: UICollectionView { + rootView.collectionView + } + typealias RootViewType = YourWalletsViewLayout typealias DataSource = UICollectionViewDiffableDataSource let presenter: YourWalletsPresenterProtocol private lazy var dataSource = createDataSource() + private lazy var delegate = createDelegate() private var viewModel: [YourWalletsViewSectionModel] = [] init(presenter: YourWalletsPresenterProtocol) { @@ -41,7 +46,8 @@ final class YourWalletsViewController: UIViewController, ViewHolder { private func setupCollectionView() { rootView.collectionView.dataSource = dataSource - rootView.collectionView.delegate = self + rootView.collectionView.delegate = delegate + rootView.collectionView.registerCellClass(SelectableIconSubtitleCollectionViewCell.self) rootView.collectionView.registerClass( RoundedIconTitleCollectionHeaderView.self, @@ -89,6 +95,21 @@ final class YourWalletsViewController: UIViewController, ViewHolder { return dataSource } + private func createDelegate() -> UICollectionViewDelegate { + ModalSheetCollectionViewDelegate( + selectItemClosure: { [weak self] indexPath in + guard let self = self else { + return + } + guard let item = self.dataSource.itemIdentifier(for: indexPath), + case let .common(viewModel) = item else { + return + } + self.presenter.didSelect(viewModel: viewModel) + } + ) + } + private static func mapWarningModel(_ model: YourWalletsCellViewModel.WarningModel) -> SelectableIconSubtitleCollectionViewCell.Model { .init( @@ -120,13 +141,7 @@ extension YourWalletsViewController: YourWalletsViewProtocol { func update(viewModel: [YourWalletsViewSectionModel]) { self.viewModel = viewModel - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections(viewModel) - viewModel.forEach { section in - snapshot.appendItems(section.cells, toSection: section) - } - - dataSource.apply(snapshot) + dataSource.apply(viewModel) } func update(header: String) { @@ -134,33 +149,6 @@ extension YourWalletsViewController: YourWalletsViewProtocol { } func calculateEstimatedHeight(sections: Int, items: Int) -> CGFloat { - RootViewType.contentHeight(sections: sections, items: items) - } -} - -// MARK: - UICollectionViewDelegate - -extension YourWalletsViewController: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - collectionView.deselectItem(at: indexPath, animated: true) - guard let item = dataSource.itemIdentifier(for: indexPath), - case let .common(viewModel) = item else { - return - } - - presenter.didSelect(viewModel: viewModel) - } - - func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - scrollView.bounces = scrollView.contentOffset.y > UIConstants.bouncesOffset - } -} - -// MARK: - ModalSheetPresenterDelegate - -extension YourWalletsViewController: ModalSheetPresenterDelegate { - func presenterCanDrag(_: ModalPresenterProtocol) -> Bool { - let offset = rootView.collectionView.contentOffset.y + rootView.collectionView.contentInset.top - return offset == 0 + rootView.contentHeight(sections: sections, items: items) } } diff --git a/novawallet/Modules/YourWallets/YourWalletsViewLayout.swift b/novawallet/Modules/YourWallets/YourWalletsViewLayout.swift index 5412b85da6..cf1abc4bdc 100644 --- a/novawallet/Modules/YourWallets/YourWalletsViewLayout.swift +++ b/novawallet/Modules/YourWallets/YourWalletsViewLayout.swift @@ -1,111 +1,12 @@ import UIKit -final class YourWalletsViewLayout: UIView { - lazy var header: UILabel = .create { +final class YourWalletsViewLayout: GenericCollectionViewLayout { + let titleLabel: UILabel = .create { $0.font = .semiBoldBody $0.textColor = R.color.colorWhite() } - lazy var collectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) - view.backgroundColor = .clear - view.contentInsetAdjustmentBehavior = .always - view.contentInset = Constants.collectionViewContentInset - return view - }() - - var showHeader: (Int) -> Bool = { _ in false } - - private lazy var compositionalLayout: UICollectionViewCompositionalLayout = { - .init { [weak self] sectionIndex, _ -> NSCollectionLayoutSection? in - let showHeader = self?.showHeader(sectionIndex) ?? false - return Self.createCompositionalLayout(showHeader: showHeader) - } - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = R.color.color0x1D1D20() - setupLayout() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupLayout() { - addSubview(header) - addSubview(collectionView) - - header.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide.snp.top).offset(Constants.headerContentInsets.top) - $0.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) - } - - header.setContentHuggingPriority(.defaultLow, for: .vertical) - - collectionView.snp.makeConstraints { - $0.top.equalTo(header.snp.bottom).offset(Constants.headerContentInsets.bottom) - $0.leading.trailing.bottom.equalToSuperview() - } - } - - private static func createCompositionalLayout(showHeader: Bool) -> NSCollectionLayoutSection { - .createSectionLayoutWithFullWidthRow(settings: - .init( - estimatedRowHeight: Constants.estimatedRowHeight, - estimatedHeaderHeight: Constants.estimatedSectionHeaderHeight, - sectionContentInsets: Constants.sectionContentInsets, - sectionInterGroupSpacing: Constants.interGroupSpacing, - header: showHeader ? .init(pinToVisibleBounds: true) : nil - )) - } -} - -// MARK: - Constants - -extension YourWalletsViewLayout { - private enum Constants { - static let estimatedHeaderHeight: CGFloat = 36 - static let estimatedRowHeight: CGFloat = 56 - static let estimatedSectionHeaderHeight: CGFloat = 46 - static let sectionContentInsets = NSDirectionalEdgeInsets( - top: 0, - leading: 16, - bottom: 0, - trailing: 16 - ) - static let interGroupSpacing: CGFloat = 0 - static let collectionViewContentInset = UIEdgeInsets( - top: 0, - left: 0, - bottom: 16, - right: 0 - ) - static let headerContentInsets = UIEdgeInsets( - top: 3.0, - left: 0, - bottom: 12.0, - right: 0 - ) - } -} - -extension YourWalletsViewLayout { - static func contentHeight(sections: Int, items: Int) -> CGFloat { - let itemHeight = Constants.estimatedRowHeight - - let sectionsHeight = Constants.estimatedSectionHeaderHeight + - Constants.sectionContentInsets.top + - Constants.sectionContentInsets.bottom - - let estimatedListHeight = Constants.collectionViewContentInset.top + - CGFloat(items) * itemHeight + - CGFloat(sections) * sectionsHeight + - Constants.collectionViewContentInset.bottom - - return Constants.estimatedHeaderHeight + estimatedListHeight + override init(frame _: CGRect = .zero) { + super.init(header: titleLabel) } } diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index e9dfea879e..2c9a8a70cc 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -288,6 +288,7 @@ "common.available.format" = "Available: %@"; "staking.rewards.learn.more" = "Learn more about rewards"; "common.error.no.data.retrieved" = "No data retrieved."; +"common.error.no.data.retrieved_v3_9_1" = "No data retrieved"; "staking.reward.payouts.empty.rewards" = "Perfect! All rewards are paid."; "staking.reward.details.validator" = "Validator"; "common.insufficient.balance" = "Insufficient balance"; @@ -575,6 +576,7 @@ "account.create.details_v2_2_0" = "Do not use clipboard or screenshots on your mobile device, try to find secure methods for backup (e.g. paper)"; "crowdloan.list.section.format_v2_2_0" = "Choose parachains to contribute your %@. You'll get back your contributed tokens, and if parachain wins a slot, you'll receive rewards after the end of the auction"; "crowdloan.empty.message_v2_2_0" = "Crowdloans will be displayed here"; +"crowdloan.empty.message_v3_9_1" = "Crowdloans information\nwill appear here when they start"; "common.existential.warning.message_v2_2_0" = "Your account will be removed from blockchain after this operation cause it makes total balance lower than minimal"; "wallet.send.existential.warning_v2_2_0" = "Your account will be removed from blockchain after transfer cause it makes total balance lower than minimal"; "account.backup.mnemonic.title" = "Write down the phrase and store it in a safe place"; @@ -973,4 +975,5 @@ "yield.boost.time.not.loaded.message" = "Please, wait until execution time is calculated"; "yield.boost.task.not.found.title" = "Yield boost already disabled"; "yield.boost.task.not.found.message" = "Yield boost already disabled for the selected collator"; -"common.not.available" = "N/A"; \ No newline at end of file +"common.not.available" = "N/A"; +"wallet.account.locks.crowdloans" = "Crowdloans"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 01d1108192..62af0c73c2 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -288,6 +288,7 @@ "common.available.format" = "Доступно: %@"; "staking.rewards.learn.more" = "Подробнее о вознаграждениях"; "common.error.no.data.retrieved" = "Данные не получены."; +"common.error.no.data.retrieved_v3_9_1" = "Данные не получены"; "staking.reward.payouts.empty.rewards" = "Прекрасно! Все вознаграждения выплачены."; "staking.reward.details.validator" = "Валидатор"; "common.insufficient.balance" = "Недостаточный баланс"; @@ -575,6 +576,7 @@ "account.create.details_v2_2_0" = "Не используйте буфер обмена или скриншоты на вашем мобильном устройстве, постарайтесь найти безопасные способы резервного копирования (например на бумагу)"; "crowdloan.list.section.format_v2_2_0" = "Выберите парачейны для внесения своих %@. Вы получите внесенные токены обратно, и если парачейн выиграет слот вы получите награду после окончания аукциона"; "crowdloan.empty.message_v2_2_0" = "Краудлоуны появятся здесь"; +"crowdloan.empty.message_v3_9_1" = "Информация о краудлоунах\nпоявится здесь, когда они начнутся"; "common.existential.warning.message_v2_2_0" = "Ваша учетная запись будет удалена из сети после операции, так как ваш баланс опустится ниже минимального"; "wallet.send.existential.warning_v2_2_0" = "Ваша учетная запись будет удалена из сети после перевода, так как опустит общий баланс ниже минимального"; "account.backup.mnemonic.title" = "Запишите фразу и храните её в надежном месте"; @@ -973,4 +975,5 @@ "yield.boost.time.not.loaded.message" = "Пожалуйста, подождите пока подсчитается время исполнения операции"; "yield.boost.task.not.found.title" = "Yield boost уже отключен"; "yield.boost.task.not.found.message" = "Yield boost уже отключен для выбранного коллатора"; -"common.not.available" = "N/A"; \ No newline at end of file +"common.not.available" = "N/A"; +"wallet.account.locks.crowdloans" = "Краудлоуны"; diff --git a/novawalletIntegrationTests/ChainRegistry+Setup.swift b/novawalletIntegrationTests/ChainRegistry+Setup.swift index 12e56e6782..ee77dc655a 100644 --- a/novawalletIntegrationTests/ChainRegistry+Setup.swift +++ b/novawalletIntegrationTests/ChainRegistry+Setup.swift @@ -8,9 +8,11 @@ extension ChainRegistryFacade { let chainRegistry = ChainRegistryFactory.createDefaultRegistry(from: storageFacade) chainRegistry.syncUp() + let target = NSObject() + let semaphore = DispatchSemaphore(value: 0) chainRegistry.chainsSubscribe( - self, runningInQueue: .global() + target, runningInQueue: .global() ) { changes in if !changes.isEmpty { semaphore.signal() @@ -19,6 +21,8 @@ extension ChainRegistryFacade { semaphore.wait() + chainRegistry.chainsUnsubscribe(target) + return chainRegistry } } diff --git a/novawalletTests/Common/Migration/SubstrateStorageMigrationTests.swift b/novawalletTests/Common/Migration/SubstrateStorageMigrationTests.swift new file mode 100644 index 0000000000..a8edf0bdcb --- /dev/null +++ b/novawalletTests/Common/Migration/SubstrateStorageMigrationTests.swift @@ -0,0 +1,203 @@ +import XCTest +import RobinHood +import CoreData + +@testable import novawallet + +final class SubstrateStorageMigrationTests: XCTestCase { + + let databaseDirectoryURL = FileManager + .default + .temporaryDirectory + .appendingPathComponent("CoreData") + .appendingPathComponent("SubstrateStorageMigrationTests") + + let databaseName = SubstrateStorageParams.databaseName + let modelDirectory = SubstrateStorageParams.modelDirectory + var storeURL: URL { databaseDirectoryURL.appendingPathComponent(databaseName) } + let mapper = ChainModelMapper() + + override func setUpWithError() throws { + try super.setUpWithError() + + try removeDirectory(at: databaseDirectoryURL) + try FileManager.default.createDirectory(at: databaseDirectoryURL, withIntermediateDirectories: true) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + + try removeDirectory(at: databaseDirectoryURL) + } + + func testMigrationVersion1ToVersion2() { + let timeout: TimeInterval = 5 + let generatedChains = generateChainsWithTimeout(timeout) + XCTAssertGreaterThan(generatedChains.count, 0) + + let migrator = SubstrateStorageMigrator(storeURL: storeURL, + modelDirectory: modelDirectory, + model: .version2, + fileManager: FileManager.default) + + XCTAssertTrue(migrator.requiresMigration(), "Migration is not required") + + let migrateExpectation = XCTestExpectation(description: "Migration expectation") + migrator.migrate { + migrateExpectation.fulfill() + } + + wait(for: [migrateExpectation], timeout: timeout) + + let fetchedChains = fetchChainsWithTimeout(timeout) + + let sortedChainsBeforeMigration = generatedChains.sorted { $0.identifier < $1.identifier } + let sortedChainsAfterMigration = fetchedChains.sorted { $0.identifier < $1.identifier } + XCTAssertEqual(sortedChainsBeforeMigration, sortedChainsAfterMigration) + } + + private func generateChainsWithTimeout(_ timeout: TimeInterval) -> [ChainModel] { + var generatedChains: [ChainModel] = [] + let expectation = XCTestExpectation(description: "Generate chains expectation") + + generateChains { result in + switch result { + case .failure(let error): + XCTFail(error.localizedDescription) + case .success(let chains): + generatedChains = chains + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + return generatedChains + } + + private func fetchChainsWithTimeout(_ timeout: TimeInterval) -> [ChainModel] { + var fetchedChains: [ChainModel] = [] + let expectation = XCTestExpectation(description: "Fetch chains expectation") + + fetchChains { result in + switch result { + case .failure(let error): + XCTFail(error.localizedDescription) + case .success(let chains): + fetchedChains = chains + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + return fetchedChains + } + + private func generateChains(completion: @escaping (Result<[ChainModel], Error>) -> Void) { + let chains = ChainModelGenerator.generate(count: 5) + let dbService = createCoreDataService(for: .version1) + + dbService.performAsync { [unowned self] (context, error) in + if let error = error { + completion(.failure(error)) + return + } + + guard let context = context else { + completion(.failure(TestError.noCoreDataContext)) + return + } + + do { + try chains.forEach { + let insertedObject = NSEntityDescription.insertNewObject(forEntityName: "CDChain", + into: context) + guard let newChain = insertedObject as? CDChain else { + throw TestError.unexpectedEntity + } + + try mapper.populate(entity: newChain, + from: $0, + using: context) + + } + + try context.save() + + completion(.success(chains)) + } catch { + completion(.failure(error)) + } + } + } + + private func fetchChains(completion: @escaping (Result<[ChainModel], Error>) -> Void) { + let dbService = createCoreDataService(for: .version2) + + dbService.performAsync { [unowned self] (context, error) in + if let error = error { + completion(.failure(error)) + return + } + + guard let context = context else { + completion(.failure(TestError.noCoreDataContext)) + return + } + do { + let request = NSFetchRequest(entityName: "CDChain") + guard let results = try context.fetch(request) as? [CDChain] else { + throw TestError.unexpectedEntity + } + let chains = try results.map(mapper.transform) + completion(.success(chains)) + } catch { + completion(.failure(error)) + } + } + + } + + private func createCoreDataService(for version: SubstrateStorageVersion) -> CoreDataServiceProtocol { + let modelURL = Bundle.main.url( + forResource: version.rawValue, + withExtension: "mom", + subdirectory: modelDirectory + )! + + let persistentSettings = CoreDataPersistentSettings( + databaseDirectory: databaseDirectoryURL, + databaseName: databaseName, + incompatibleModelStrategy: .ignore + ) + + let configuration = CoreDataServiceConfiguration( + modelURL: modelURL, + storageType: .persistent(settings: persistentSettings) + ) + + return CoreDataService(configuration: configuration) + } + + private func removeDirectory(at directoryURL: URL) throws { + let fileManager = FileManager.default + + guard let tmpFiles = try? fileManager.contentsOfDirectory(at: directoryURL, + includingPropertiesForKeys: nil, + options: .skipsHiddenFiles) else { + return + } + + try tmpFiles.forEach(fileManager.removeItem) + try fileManager.removeItem(at: directoryURL) + } + +} + +// MARK: - Errors + +extension SubstrateStorageMigrationTests { + enum TestError: String, Error { + case noCoreDataContext + case unexpectedEntity + } +} diff --git a/novawalletTests/Helper/ChainModelGenerator.swift b/novawalletTests/Helper/ChainModelGenerator.swift index 5685e8f03c..b90335453a 100644 --- a/novawalletTests/Helper/ChainModelGenerator.swift +++ b/novawalletTests/Helper/ChainModelGenerator.swift @@ -130,6 +130,8 @@ enum ChainModelGenerator { ) ] + let rawOptions = options.compactMap { $0.rawValue } + return RemoteChainModel( chainId: chainId, parentId: nil, @@ -139,7 +141,7 @@ enum ChainModelGenerator { addressPrefix: UInt16(index), types: types, icon: URL(string: "https://github.com")!, - options: options.isEmpty ? nil : options, + options: rawOptions.isEmpty ? nil : rawOptions, externalApi: externalApi, explorers: explorers, additional: nil diff --git a/novawalletTests/Mocks/CommonMocks.swift b/novawalletTests/Mocks/CommonMocks.swift index f45cdcbf7f..86fc0d81b1 100644 --- a/novawalletTests/Mocks/CommonMocks.swift +++ b/novawalletTests/Mocks/CommonMocks.swift @@ -5732,6 +5732,7 @@ import Cuckoo @testable import SoraKeystore import Foundation +import RobinHood import SubstrateSdk @@ -5760,9 +5761,9 @@ import SubstrateSdk - func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -5820,9 +5821,9 @@ import SubstrateSdk - func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -5857,9 +5858,9 @@ import SubstrateSdk } - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromAccountInfo(for subscriptionId: M1, accountId: M2, chainId: M3, queue: M4, closure: M5) -> Cuckoo.ProtocolStubNoReturnFunction<(UUID, AccountId, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure { @@ -5877,9 +5878,9 @@ import SubstrateSdk return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "detachFromAsset(for: UUID, accountId: AccountId, extras: StatemineAssetExtras, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?)", parameterMatchers: matchers)) } - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromOrmlToken(for subscriptionId: M1, accountId: M2, currencyId: M3, chainId: M4, queue: M5, closure: M6) -> Cuckoo.ProtocolStubNoReturnFunction<(UUID, AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == Data, M4.MatchedType == ChainModel.Id, M5.OptionalMatchedType == DispatchQueue, M6.OptionalMatchedType == RemoteSubscriptionClosure { @@ -5904,9 +5905,9 @@ import SubstrateSdk @discardableResult - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -5928,9 +5929,9 @@ import SubstrateSdk } @discardableResult - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -5950,7 +5951,7 @@ import SubstrateSdk - func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } @@ -5974,7 +5975,7 @@ import SubstrateSdk - func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } @@ -6013,9 +6014,9 @@ import SubstrateSdk - override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -6073,9 +6074,9 @@ import SubstrateSdk - override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -6110,9 +6111,9 @@ import SubstrateSdk } - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromAccountInfo(for subscriptionId: M1, accountId: M2, chainId: M3, queue: M4, closure: M5) -> Cuckoo.ClassStubNoReturnFunction<(UUID, AccountId, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure { @@ -6130,9 +6131,9 @@ import SubstrateSdk return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "detachFromAsset(for: UUID, accountId: AccountId, extras: StatemineAssetExtras, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?)", parameterMatchers: matchers)) } - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromOrmlToken(for subscriptionId: M1, accountId: M2, currencyId: M3, chainId: M4, queue: M5, closure: M6) -> Cuckoo.ClassStubNoReturnFunction<(UUID, AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == Data, M4.MatchedType == ChainModel.Id, M5.OptionalMatchedType == DispatchQueue, M6.OptionalMatchedType == RemoteSubscriptionClosure { @@ -6157,9 +6158,9 @@ import SubstrateSdk @discardableResult - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -6181,9 +6182,9 @@ import SubstrateSdk } @discardableResult - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -6203,7 +6204,7 @@ import SubstrateSdk - override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } @@ -6227,7 +6228,7 @@ import SubstrateSdk - override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } diff --git a/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift b/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift index 1a564a7387..0812f98505 100644 --- a/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift +++ b/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift @@ -60,4 +60,16 @@ final class WalletLocalSubscriptionFactoryStub: WalletLocalSubscriptionFactoryPr func getAllBalancesProvider() throws -> StreamableProvider { throw CommonError.undefined } + + func getLocksProvider(for accountId: AccountId) throws -> StreamableProvider { + throw CommonError.undefined + } + + func getLocksProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) throws -> StreamableProvider { + throw CommonError.undefined + } } diff --git a/novawalletTests/Modules/BalanceLocks/BalanceLocksTests.swift b/novawalletTests/Modules/BalanceLocks/BalanceLocksTests.swift deleted file mode 100644 index c743429db0..0000000000 --- a/novawalletTests/Modules/BalanceLocks/BalanceLocksTests.swift +++ /dev/null @@ -1,117 +0,0 @@ -import XCTest -@testable import novawallet - -class BalanceLocksTest: XCTestCase { - func testLocksRestoration() throws { - - // given - let context = ["account.balance.price.change.key": "0", - "account.balance.fee.frozen.key": "2.3657237", - "account.balance.locks.key":"[{\"id\":[\"115\",\"116\",\"97\",\"107\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2}]", - "account.balance.reserved.key": "1.00205", - "account.balance.misc.frozen.key": "2.3657237", - "account.balance.price.key": "0", - "account.balance.minimal.key": "0.01", - "account.balance.free.key": "5.06791402788"] - - // when - - let balanceContext = BalanceContext.init(context: context) - - // then - - XCTAssertEqual(balanceContext.balanceLocks.count, 1) - } - - func testMainLocksSeparation() throws { - // given - let context = ["account.balance.price.change.key": "0", - "account.balance.fee.frozen.key": "2.3657237", - "account.balance.locks.key":"[{\"id\":[\"115\",\"116\",\"97\",\"107\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2},{\"id\":[\"115\",\"116\",\"97\",\"115\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2}]", - "account.balance.reserved.key": "1.00205", - "account.balance.misc.frozen.key": "2.3657237", - "account.balance.price.key": "0", - "account.balance.minimal.key": "0.01", - "account.balance.free.key": "5.06791402788"] - - // when - - let balanceContext = BalanceContext.init(context: context) - let mainLocks = balanceContext.balanceLocks.mainLocks() - - // then - - XCTAssertEqual(mainLocks.count, 1) - - XCTAssertEqual(LockType(rawValue: mainLocks.first?.displayId ?? ""), .staking) - } - - func testAuxLocksSeparation() throws { - // given - let context = ["account.balance.price.change.key": "0", - "account.balance.fee.frozen.key": "2.3657237", - "account.balance.locks.key":"[{\"id\":[\"115\",\"116\",\"97\",\"107\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2},{\"id\":[\"115\",\"116\",\"97\",\"115\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2}]", - "account.balance.reserved.key": "1.00205", - "account.balance.misc.frozen.key": "2.3657237", - "account.balance.price.key": "0", - "account.balance.minimal.key": "0.01", - "account.balance.free.key": "5.06791402788"] - - // when - - let balanceContext = BalanceContext.init(context: context) - let auxLocks = balanceContext.balanceLocks.auxLocks() - - // then - - XCTAssertEqual(auxLocks.count, 1) - - XCTAssertEqual(auxLocks.first?.displayId, "stasing") - } - - func testMainLocksOrder() throws { - // given - let context = ["account.balance.price.change.key": "0", - "account.balance.fee.frozen.key": "2.3657237", - "account.balance.locks.key":"[{\"id\":[\"115\",\"116\",\"97\",\"107\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2},{\"id\":[\"118\",\"101\",\"115\",\"116\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2}]", - "account.balance.reserved.key": "1.00205", - "account.balance.misc.frozen.key": "2.3657237", - "account.balance.price.key": "0", - "account.balance.minimal.key": "0.01", - "account.balance.free.key": "5.06791402788"] - - // when - - let balanceContext = BalanceContext.init(context: context) - let mainLocks = balanceContext.balanceLocks.mainLocks() - - // then - - XCTAssertEqual(mainLocks.count, 2) - - XCTAssertEqual(LockType(rawValue: mainLocks.first?.displayId ?? ""), .vesting) - } - - func testAuxLocksOrder() throws { - // given - let context = ["account.balance.price.change.key": "0", - "account.balance.fee.frozen.key": "2.3657237", - "account.balance.locks.key":"[{\"id\":[\"107\",\"105\",\"115\",\"115\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2},{\"id\":[\"115\",\"116\",\"97\",\"115\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2375723700000\",\"reasons\":2}]", - "account.balance.reserved.key": "1.00205", - "account.balance.misc.frozen.key": "2.3657237", - "account.balance.price.key": "0", - "account.balance.minimal.key": "0.01", - "account.balance.free.key": "5.06791402788"] - - // when - - let balanceContext = BalanceContext.init(context: context) - let auxLocks = balanceContext.balanceLocks.auxLocks() - - // then - - XCTAssertEqual(auxLocks.count, 2) - - XCTAssertEqual(auxLocks.first?.displayId, "stasing") - } -} diff --git a/novawalletTests/Modules/Root/RootTests.swift b/novawalletTests/Modules/Root/RootTests.swift index 71b9ffc548..282849c1fd 100644 --- a/novawalletTests/Modules/Root/RootTests.swift +++ b/novawalletTests/Modules/Root/RootTests.swift @@ -128,7 +128,7 @@ class RootTests: XCTestCase { let interactor = RootInteractor(settings: settings, keystore: keystore, applicationConfig: ApplicationConfig.shared, - chainRegistry: chainRegistry, + chainRegistryClosure: { chainRegistry }, eventCenter: MockEventCenterProtocol(), migrators: migrators) let presenter = RootPresenter()